Skip to content

Commit

Permalink
Merge pull request #25 from MilesCranmer/path-to-graveyard
Browse files Browse the repository at this point in the history
add graveyard subcommand + help colors
  • Loading branch information
MilesCranmer authored Apr 15, 2024
2 parents 5822b68 + 3cc2750 commit ce65c96
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 33 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ categories = ["command-line-utilities"]
autobins = false

[dependencies]
anstyle = "1.0.6"
clap = { version = "4.4", features = ["derive"] }
clap_complete = "4.4"
clap_complete_nushell = "4.4"
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

`rip` is a rust-based `rm` with a focus on safety, ergonomics, and performance. It favors a simple interface, and does *not* implement the xdg-trash spec or attempt to achieve the same goals.

Deleted files get sent to the graveyard 🪦 (Usually `/tmp/graveyard-$USER`, see [notes](#notes) on changing this) under their absolute path, giving you a chance to recover them 🧟. No data is overwritten. If files that share the same path are deleted, they will be renamed as numbered backups.
Deleted files get sent to the graveyard 🪦 (typically `/tmp/graveyard-$USER`, see [notes](#notes) on changing this) under their absolute path, giving you a chance to recover them 🧟. No data is overwritten. If files that share the same path are deleted, they will be renamed as numbered backups.

This version, "rip2", is a fork-of-a-fork:

Expand Down Expand Up @@ -44,23 +44,25 @@ No binaries are made available at this time.
## Usage

```text
Usage: rip [OPTIONS] [TARGETS]... [COMMAND]
Commands:
completions Generate shell completions file for the specified shell
help Print this message or the help of the given subcommand(s)
Usage: rip [OPTIONS] [FILES]...
rip [SUBCOMMAND]
Arguments:
[TARGETS]... File or directory to remove
[FILES]... Files and directories to remove
Options:
--graveyard <GRAVEYARD> Directory where deleted files rest
-d, --decompose Permanently deletes the graveyard
-s, --seance Prints files that were deleted in the current working directory
-s, --seance Prints files that were deleted in the current directory
-u, --unbury Restore the specified files or the last file if none are specified
-i, --inspect Print some info about TARGET before burying
-h, --help Print help
-V, --version Print version
Sub-commands:
completions Generate shell completions file
graveyard Print the graveyard path
help Print this message or the help of the given subcommand(s)
```

Basic usage -- easier than rm
Expand Down Expand Up @@ -134,6 +136,7 @@ alias rm="echo Use 'rip' instead of rm."

**Graveyard location.**

You can see the current graveyard location by running `rip graveyard`.
If you have `$XDG_DATA_HOME` environment variable set, `rip` will use `$XDG_DATA_HOME/graveyard` instead of the `$TMPDIR/graveyard-$USER`.

If you want to put the graveyard somewhere else (like `~/.local/share/Trash`), you have two options, in order of precedence:
Expand Down
94 changes: 90 additions & 4 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,88 @@
use anstyle::{AnsiColor, Color::Ansi, Style};
use clap::builder::styling::Styles;
use clap::{Parser, Subcommand};

use std::io::{Error, ErrorKind};
use std::path::PathBuf;

const CMD_STYLE: Style = Style::new().bold();
const HEADER_STYLE: Style = Style::new()
.bold()
.underline()
.fg_color(Some(Ansi(AnsiColor::Blue)));
const PLACEHOLDER_STYLE: Style = Style::new().fg_color(Some(Ansi(AnsiColor::Green)));

const OPTIONS_PLACEHOLDER: &str = "{options}";
const SUBCOMMANDS_PLACEHOLDER: &str = "{subcommands}";

fn help_template(template: &str) -> String {
let header = HEADER_STYLE.render();
let rheader = HEADER_STYLE.render_reset();
let rip_s = CMD_STYLE.render();
let rrip_s = CMD_STYLE.render_reset();
let place = PLACEHOLDER_STYLE.render();
let rplace = PLACEHOLDER_STYLE.render_reset();

match template {
"rip" => format!(
"\
rip: a safe and ergonomic alternative to rm
{header}Usage{rheader}: {rip_s}rip{rrip_s} [{place}OPTIONS{rplace}] [{place}FILES{rplace}]...
{rip_s}rip{rrip_s} [{place}SUBCOMMAND{rplace}]
{header}Arguments{rheader}:
[{place}FILES{rplace}]... Files or directories to remove
{header}Options{rheader}:
{OPTIONS_PLACEHOLDER}
{header}Subcommands{rheader}:
{SUBCOMMANDS_PLACEHOLDER}
"
),
"completions" => format!(
"\
Generate the shell completions file
{header}Usage{rheader}: {rip_s}rip completions{rrip_s} <{place}SHELL{rplace}>
{header}Arguments{rheader}:
<{place}SHELL{rplace}> The shell to generate completions for
{header}Options{rheader}:
{OPTIONS_PLACEHOLDER}
"
),
"graveyard" => format!(
"\
Print the graveyard path
{header}Usage{rheader}: {rip_s}rip graveyard{rrip_s} [{place}OPTIONS{rplace}]
{header}Options{rheader}:
{OPTIONS_PLACEHOLDER}
"
),
_ => unreachable!(),
}
}

const STYLES: Styles = Styles::styled()
.literal(AnsiColor::Magenta.on_default())
.placeholder(AnsiColor::Green.on_default());

#[derive(Parser, Debug, Default)]
#[command(version, about, long_about = None)]
#[command(
name = "rip",
version,
about,
long_about = None,
styles=STYLES,
help_template = help_template("rip"),
)]
pub struct Args {
/// File or directory to remove
/// Files and directories to remove
pub targets: Vec<PathBuf>,

/// Directory where deleted files rest
Expand All @@ -17,7 +94,7 @@ pub struct Args {
pub decompose: bool,

/// Prints files that were deleted
/// in the current working directory
/// in the current directory
#[arg(short, long)]
pub seance: bool,

Expand All @@ -39,12 +116,21 @@ pub struct Args {
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Generate shell completions file
/// for the specified shell
#[command(styles=STYLES, help_template=help_template("completions"))]
Completions {
/// The shell to generate completions for
#[arg(value_name = "SHELL")]
shell: String,
},

/// Print the graveyard path
#[command(styles=STYLES, help_template=help_template("graveyard"))]
Graveyard {
/// Get the graveyard subdirectory
/// of the current directory
#[arg(short, long)]
seance: bool,
},
}

struct IsDefault {
Expand Down
34 changes: 16 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,7 @@ pub fn run(cli: Args, mode: impl util::TestingMode, stream: &mut impl Write) ->
// 2. Path pointed by the $GRAVEYARD variable
// 3. $XDG_DATA_HOME/graveyard (only if XDG_DATA_HOME is defined)
// 4. /tmp/graveyard-user
let graveyard: &PathBuf = &{
if let Some(flag) = cli.graveyard {
flag
} else if let Ok(env_graveyard) = env::var("RIP_GRAVEYARD") {
PathBuf::from(env_graveyard)
} else if let Ok(mut env_graveyard) = env::var("XDG_DATA_HOME") {
if !env_graveyard.ends_with(std::path::MAIN_SEPARATOR) {
env_graveyard.push(std::path::MAIN_SEPARATOR);
}
env_graveyard.push_str("graveyard");
PathBuf::from(env_graveyard)
} else {
default_graveyard()
}
};
let graveyard: &PathBuf = &get_graveyard(cli.graveyard);

if !graveyard.exists() {
fs::create_dir_all(graveyard)?;
Expand Down Expand Up @@ -439,7 +425,19 @@ pub fn copy_file(
}
}

fn default_graveyard() -> PathBuf {
let user = util::get_user();
env::temp_dir().join(format!("graveyard-{}", user))
pub fn get_graveyard(graveyard: Option<PathBuf>) -> PathBuf {
if let Some(flag) = graveyard {
flag
} else if let Ok(env_graveyard) = env::var("RIP_GRAVEYARD") {
PathBuf::from(env_graveyard)
} else if let Ok(mut env_graveyard) = env::var("XDG_DATA_HOME") {
if !env_graveyard.ends_with(std::path::MAIN_SEPARATOR) {
env_graveyard.push(std::path::MAIN_SEPARATOR);
}
env_graveyard.push_str("graveyard");
PathBuf::from(env_graveyard)
} else {
let user = util::get_user();
env::temp_dir().join(format!("graveyard-{}", user))
}
}
22 changes: 19 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
use clap::Parser;
use clap::{Args as _, Command, FromArgMatches as _};
use std::env;
use std::io;
use std::process::ExitCode;

use rip2::args::Commands;
use rip2::{args, completions, util};

fn main() -> ExitCode {
let cli = args::Args::parse();
let base_cmd = Command::new("rip");
let cmd = args::Args::augment_args(base_cmd);
let cli = args::Args::from_arg_matches(&cmd.get_matches()).unwrap();

let mut stream = io::stdout();
let mode = util::ProductionMode;

match &cli.command {
Some(args::Commands::Completions { shell }) => {
Some(Commands::Completions { shell }) => {
let result = completions::generate_shell_completions(shell, &mut io::stdout());
if result.is_err() {
eprintln!("{}", result.unwrap_err());
return ExitCode::FAILURE;
}
return ExitCode::SUCCESS;
}
Some(Commands::Graveyard { seance }) => {
let graveyard = rip2::get_graveyard(None);
if *seance {
let cwd = &env::current_dir().unwrap();
let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd).unwrap());
println!("{}", gravepath.display());
} else {
println!("{}", graveyard.display());
}
return ExitCode::SUCCESS;
}
None => {}
}

Expand Down
23 changes: 23 additions & 0 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,3 +676,26 @@ fn issue_0018() {

return;
}

#[rstest]
fn test_graveyard_subcommand(#[values(false, true)] seance: bool) {
let _env_lock = aquire_lock();

let expected_graveyard = rip2::get_graveyard(None);
let cwd = &env::current_dir().unwrap();
let expected_gravepath =
util::join_absolute(&expected_graveyard, dunce::canonicalize(cwd).unwrap());
let expected_str = if seance {
format!("{}\n", expected_gravepath.display())
} else {
format!("{}\n", expected_graveyard.display())
};
let mut args = vec!["graveyard"];
if seance {
args.push("-s");
}
cli_runner(args, None)
.assert()
.success()
.stdout(expected_str);
}
26 changes: 26 additions & 0 deletions tests/unit_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use lazy_static::lazy_static;
use rip2::args::{validate_args, Args, Commands};
use rip2::completions;
use rip2::util::TestMode;
Expand All @@ -6,6 +7,7 @@ use std::fs;
use std::io::{Cursor, ErrorKind};
use std::path::PathBuf;
use std::process;
use std::sync::{Mutex, MutexGuard};
use tempfile::tempdir;

#[cfg(unix)]
Expand All @@ -20,6 +22,14 @@ use std::os::unix::net::UnixListener;
#[cfg(target_os = "macos")]
use std::os::unix::fs::FileTypeExt;

lazy_static! {
static ref GLOBAL_LOCK: Mutex<()> = Mutex::new(());
}

fn aquire_lock() -> MutexGuard<'static, ()> {
GLOBAL_LOCK.lock().unwrap()
}

#[rstest]
fn test_validation() {
let bad_completions = Args {
Expand Down Expand Up @@ -203,3 +213,19 @@ fn test_completions(
_ => {}
}
}

#[rstest]
fn test_graveyard_path() {
let _env_lock = aquire_lock();

// Clear env:
std::env::remove_var("RIP_GRAVEYARD");
std::env::remove_var("XDG_DATA_HOME");

// Check default graveyard path
let graveyard = rip2::get_graveyard(None);
assert_eq!(
graveyard,
std::env::temp_dir().join(format!("graveyard-{}", rip2::util::get_user()))
);
}

0 comments on commit ce65c96

Please sign in to comment.