From 448caf7b6c6c86bd8fa02783d4b7e70064725d11 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 08:37:08 +0100 Subject: [PATCH 1/8] feat: add subcommand to get graveyard path --- src/args.rs | 6 ++++-- src/lib.rs | 34 ++++++++++++++++------------------ src/main.rs | 7 ++++++- tests/unit_tests.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/args.rs b/src/args.rs index 1c1b961..861a631 100644 --- a/src/args.rs +++ b/src/args.rs @@ -17,7 +17,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, @@ -39,12 +39,14 @@ pub struct Args { #[derive(Subcommand, Debug)] pub enum Commands { /// Generate shell completions file - /// for the specified shell Completions { /// The shell to generate completions for #[arg(value_name = "SHELL")] shell: String, }, + + /// Print the graveyard path + Graveyard, } struct IsDefault { diff --git a/src/lib.rs b/src/lib.rs index 7b3d045..438e0ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)?; @@ -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 { + 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)) + } } diff --git a/src/main.rs b/src/main.rs index 0beb816..1d3250e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; use std::io; use std::process::ExitCode; +use rip2::args::Commands; use rip2::{args, completions, util}; fn main() -> ExitCode { @@ -10,7 +11,7 @@ fn main() -> ExitCode { 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()); @@ -18,6 +19,10 @@ fn main() -> ExitCode { } return ExitCode::SUCCESS; } + Some(Commands::Graveyard) => { + println!("{}", rip2::get_graveyard(None).display()); + return ExitCode::SUCCESS; + } None => {} } diff --git a/tests/unit_tests.rs b/tests/unit_tests.rs index ffbdad5..8a9021b 100644 --- a/tests/unit_tests.rs +++ b/tests/unit_tests.rs @@ -1,3 +1,4 @@ +use lazy_static::lazy_static; use rip2::args::{validate_args, Args, Commands}; use rip2::completions; use rip2::util::TestMode; @@ -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)] @@ -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 { @@ -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())) + ); +} From 77107577bb7896a7b7e435e73831440501b021f6 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 08:48:56 +0100 Subject: [PATCH 2/8] docs: update usage on readme --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3c2b734..408b86b 100644 --- a/README.md +++ b/README.md @@ -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: @@ -44,11 +44,7 @@ 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] [TARGETS]... [SUB-COMMAND] Arguments: [TARGETS]... File or directory to remove @@ -56,11 +52,16 @@ Arguments: Options: --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 @@ -134,6 +135,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: From a1a9262aefb9bfe5a7664813b941d2d616babb41 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 08:49:12 +0100 Subject: [PATCH 3/8] test: graveyard subcommand cli --- tests/integration_tests.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index eac4904..582bd8c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -676,3 +676,13 @@ fn issue_0018() { return; } + +#[rstest] +fn test_graveyard_subcommand() { + let expected_graveyard = rip2::get_graveyard(None); + let expected_graveyard_str = format!("{}\n", expected_graveyard.display()); + cli_runner(["graveyard"], None) + .assert() + .success() + .stdout(is_match(expected_graveyard_str).unwrap()); +} From ad85c0fd517f476bf4d141a8a1c30e173c43152d Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 09:02:36 +0100 Subject: [PATCH 4/8] feat: add seance option to graveyard subcommand --- src/args.rs | 7 ++++++- src/main.rs | 12 ++++++++++-- tests/integration_tests.rs | 22 ++++++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/args.rs b/src/args.rs index 861a631..7b770d9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -46,7 +46,12 @@ pub enum Commands { }, /// Print the graveyard path - Graveyard, + Graveyard { + /// Get the graveyard subdirectory + /// of the current directory + #[arg(short, long)] + seance: bool, + }, } struct IsDefault { diff --git a/src/main.rs b/src/main.rs index 1d3250e..063e268 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use std::env; use std::io; use std::process::ExitCode; @@ -19,8 +20,15 @@ fn main() -> ExitCode { } return ExitCode::SUCCESS; } - Some(Commands::Graveyard) => { - println!("{}", rip2::get_graveyard(None).display()); + 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 => {} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 582bd8c..6916a9a 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -678,11 +678,25 @@ fn issue_0018() { } #[rstest] -fn test_graveyard_subcommand() { +fn test_graveyard_subcommand( + #[values(false, true)] seance: bool, +) { + let _env_lock = aquire_lock(); + let expected_graveyard = rip2::get_graveyard(None); - let expected_graveyard_str = format!("{}\n", expected_graveyard.display()); - cli_runner(["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(is_match(expected_graveyard_str).unwrap()); + .stdout(expected_str); } From 527bc01b031d7d00942f5da448cbc5aa34ec9351 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 10:27:52 +0100 Subject: [PATCH 5/8] docs: reorder help command --- src/args.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/args.rs b/src/args.rs index 7b770d9..86a4be8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -3,9 +3,27 @@ use std::io::{Error, ErrorKind}; use std::path::PathBuf; #[derive(Parser, Debug, Default)] -#[command(version, about, long_about = None)] +#[command( + name = "rip", + version, + about, + long_about = None, + help_template = "\ +Usage: rip [OPTIONS] [FILES]... + rip [SUBCOMMAND] + +Arguments: + [FILES]... Files or directories to remove + +Options: +{options} + +Subcommands: +{subcommands} +" +)] pub struct Args { - /// File or directory to remove + /// Files or directories to remove pub targets: Vec, /// Directory where deleted files rest From 96ac9adb932736bc0d187e165b783181ca4405fb Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 10:38:20 +0100 Subject: [PATCH 6/8] refactor: use clap::Command with derive on top --- src/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 063e268..343e1d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use clap::Parser; +use clap::{Args as _, ColorChoice, Command, FromArgMatches as _}; use std::env; use std::io; use std::process::ExitCode; @@ -7,7 +7,10 @@ use rip2::args::Commands; use rip2::{args, completions, util}; fn main() -> ExitCode { - let cli = args::Args::parse(); + let base_cmd = Command::new("rip").color(ColorChoice::Never); + 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; From 261e69d7d3671b5b131ac8458eb10c462098ea34 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 11:51:33 +0100 Subject: [PATCH 7/8] feat: use colors in help menus --- Cargo.lock | 1 + Cargo.toml | 1 + src/args.rs | 89 +++++++++++++++++++++++++++++++------- src/main.rs | 4 +- tests/integration_tests.rs | 7 ++- 5 files changed, 81 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4685ce5..fe96478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,6 +635,7 @@ checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" name = "rip2" version = "0.5.0" dependencies = [ + "anstyle", "assert_cmd", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 11eaa45..8c6973c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/args.rs b/src/args.rs index 86a4be8..5f86f69 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,26 +1,85 @@ +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( name = "rip", version, about, long_about = None, - help_template = "\ -Usage: rip [OPTIONS] [FILES]... - rip [SUBCOMMAND] - -Arguments: - [FILES]... Files or directories to remove - -Options: -{options} - -Subcommands: -{subcommands} -" + styles=STYLES, + help_template = help_template("rip"), )] pub struct Args { /// Files or directories to remove @@ -56,14 +115,14 @@ pub struct Args { #[derive(Subcommand, Debug)] pub enum Commands { - /// Generate shell completions file + #[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 diff --git a/src/main.rs b/src/main.rs index 343e1d2..423a765 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use clap::{Args as _, ColorChoice, Command, FromArgMatches as _}; +use clap::{Args as _, Command, FromArgMatches as _}; use std::env; use std::io; use std::process::ExitCode; @@ -7,7 +7,7 @@ use rip2::args::Commands; use rip2::{args, completions, util}; fn main() -> ExitCode { - let base_cmd = Command::new("rip").color(ColorChoice::Never); + 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(); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 6916a9a..69cc21f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -678,14 +678,13 @@ fn issue_0018() { } #[rstest] -fn test_graveyard_subcommand( - #[values(false, true)] seance: bool, -) { +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_gravepath = + util::join_absolute(&expected_graveyard, dunce::canonicalize(cwd).unwrap()); let expected_str = if seance { format!("{}\n", expected_gravepath.display()) } else { From 3cc27505193c3ff3e34521d5870c75d4e48c3d9a Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Mon, 15 Apr 2024 11:55:08 +0100 Subject: [PATCH 8/8] docs: fix readme usage --- README.md | 5 +++-- src/args.rs | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 408b86b..e29cd6d 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,11 @@ No binaries are made available at this time. ## Usage ```text -Usage: rip [OPTIONS] [TARGETS]... [SUB-COMMAND] +Usage: rip [OPTIONS] [FILES]... + rip [SUBCOMMAND] Arguments: - [TARGETS]... File or directory to remove + [FILES]... Files and directories to remove Options: --graveyard Directory where deleted files rest diff --git a/src/args.rs b/src/args.rs index 5f86f69..9ad3707 100644 --- a/src/args.rs +++ b/src/args.rs @@ -82,7 +82,7 @@ const STYLES: Styles = Styles::styled() help_template = help_template("rip"), )] pub struct Args { - /// Files or directories to remove + /// Files and directories to remove pub targets: Vec, /// Directory where deleted files rest @@ -115,6 +115,7 @@ pub struct Args { #[derive(Subcommand, Debug)] pub enum Commands { + /// Generate shell completions file #[command(styles=STYLES, help_template=help_template("completions"))] Completions { /// The shell to generate completions for @@ -122,6 +123,7 @@ pub enum Commands { shell: String, }, + /// Print the graveyard path #[command(styles=STYLES, help_template=help_template("graveyard"))] Graveyard { /// Get the graveyard subdirectory