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 6c8ec24..972fc8a 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/README.md b/README.md index 3c2b734..e29cd6d 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,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 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 +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: diff --git a/src/args.rs b/src/args.rs index 1c1b961..9ad3707 100644 --- a/src/args.rs +++ b/src/args.rs @@ -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, /// Directory where deleted files rest @@ -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, @@ -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 { 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..423a765 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,21 @@ -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()); @@ -18,6 +23,17 @@ fn main() -> ExitCode { } 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 => {} } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index eac4904..69cc21f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -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); +} 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())) + ); +}