diff --git a/Cargo.lock b/Cargo.lock index ad61d98..cd8f96a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,9 +27,9 @@ checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" [[package]] name = "clap" -version = "4.0.29" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" +checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2" dependencies = [ "bitflags", "clap_lex", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.0.6" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da" +checksum = "ce8955d4e8cd4f28f9a01c93a050194c4d131e73ca02f6636bcddbed867014d7" dependencies = [ "clap", ] @@ -59,18 +59,18 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" dependencies = [ "os_str_bytes", ] [[package]] name = "clap_mangen" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e503c3058af0a0854668ea01db55c622482a080092fede9dd2e00a00a9436504" +checksum = "eb258c6232b4d728d13d6072656627924c16707aae6267cd5a1ea05abff9a25c" dependencies = [ "clap", "roff", @@ -134,9 +134,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" dependencies = [ "libc", "windows-sys", @@ -144,9 +144,9 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" dependencies = [ "hermit-abi 0.2.6", "io-lifetimes", @@ -196,9 +196,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rustix" -version = "0.36.5" +version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" dependencies = [ "bitflags", "errno", @@ -239,9 +239,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -303,42 +303,42 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 9270fe3..55da2c9 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -46,7 +46,7 @@ pub use { self::hostname::Hostname, self::shitbox::Shitbox, base32::Base32, base64::Base64, basename::Basename, bootstrap::Bootstrap, clear::Clear, cut::Cut, dirname::Dirname, echo::Echo, factor::Factor, fold::Fold, groups::Groups, head::Head, link::Link, mountpoint::Mountpoint, - nologin::Nologin, nproc::Nproc, r#false::False, r#true::True, rev::Rev, rmdir::Rmdir, + nologin::Nologin, nproc::Nproc, r#false::False, r#true::True, rev::Rev, rm::Rm, rmdir::Rmdir, sleep::Sleep, sync::Sync as SyncCmd, unlink::Unlink, which::Which, whoami::Whoami, yes::Yes, }; @@ -87,6 +87,7 @@ pub fn get(name: &str) -> Option> { "nologin" => Some(Box::new(Nologin::default())), "nproc" => Some(Box::new(Nproc::default())), "rev" => Some(Box::new(Rev::default())), + "rm" => Some(Box::new(Rm::default())), "rmdir" => Some(Box::new(Rmdir::default())), "shitbox" => Some(Box::new(Shitbox::default())), "sleep" => Some(Box::new(Sleep::default())), @@ -100,7 +101,7 @@ pub fn get(name: &str) -> Option> { } } -pub static COMMANDS: [&str; 28] = [ +pub static COMMANDS: [&str; 29] = [ "base32", "base64", "basename", @@ -120,6 +121,7 @@ pub static COMMANDS: [&str; 28] = [ "nologin", "nproc", "rev", + "rm", "rmdir", "sleep", "shitbox", diff --git a/src/cmd/rm/mod.rs b/src/cmd/rm/mod.rs index 8b13789..e5c45ce 100644 --- a/src/cmd/rm/mod.rs +++ b/src/cmd/rm/mod.rs @@ -1 +1,330 @@ +use super::Cmd; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint}; +use std::{ + error::Error, + fmt, + fs::{self, File, Metadata}, + io, + path::PathBuf, + str::FromStr, +}; +#[derive(Debug, Default)] +pub struct Rm; + +impl Cmd for Rm { + fn cli(&self) -> clap::Command { + Command::new("rm") + .about("remove files or directories") + .author("Nathan Fisher") + .version(env!("CARGO_PKG_VERSION")) + .args([ + Arg::new("fullnag") + .short('i') + .help("prompt before every removal") + .action(ArgAction::SetTrue), + Arg::new("softnag") + .short('I') + .help( + "prompt once before removing more than three files, or \ + when removing recursively;\nless intrusive than -i, while \ + still giving protection against most mistakes", + ) + .next_line_help(true) + .action(ArgAction::SetTrue), + Arg::new("interactive") + .long("interactive") + .help("when to prompt") + .value_parser(["never", "once", "always"]) + .value_name("WHEN") + .num_args(0..=1) + .require_equals(true) + .default_missing_value("always"), + Arg::new("force") + .short('f') + .long("force") + .help("ignore nonexistent files and arguments, never prompt") + .action(ArgAction::SetTrue), + Arg::new("recursive") + .short('r') + .long("recursive") + .visible_short_alias('R') + .help("remove directories and their contents recursively") + .action(ArgAction::SetTrue), + Arg::new("verbose") + .short('v') + .long("verbose") + .help("explain what is being done") + .action(ArgAction::SetTrue), + Arg::new("file") + .value_name("FILE") + .value_hint(ValueHint::AnyPath) + .num_args(1..) + .required(true), + ]) + .group( + ArgGroup::new("nag") + .args(["fullnag", "softnag", "interactive", "force"]) + .required(false) + .multiple(false), + ) + } + + fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + let Some(matches) = matches else { + return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input"))); + }; + let mut actions = AllActions::from(matches); + let proceed = match actions.prompt { + When::Always => true, + When::Once => { + if actions.items.len() > 2 { + let res = actions.prompt(); + if res { + actions.prompt = When::Never; + } + res + } else { + true + } + } + _ => true, + }; + if proceed { + for act in actions.items { + if let Err(e) = act.apply() { + if !act.force { + return Err(e.into()); + } + } + } + } + Ok(()) + } + + fn path(&self) -> Option { + Some(crate::Path::Bin) + } +} + +#[derive(Clone, Copy, PartialEq)] +enum When { + Never, + Once, + Always, +} + +impl Default for When { + fn default() -> Self { + Self::Once + } +} + +#[derive(Debug)] +pub struct ParseWhenError; + +impl fmt::Display for ParseWhenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl FromStr for When { + type Err = ParseWhenError; + + fn from_str(s: &str) -> Result { + match s { + "never" => Ok(Self::Never), + "once" => Ok(Self::Once), + "always" => Ok(Self::Always), + _ => Err(ParseWhenError), + } + } +} + +enum Filetype { + File, + Dir, + Symlink, +} + +impl From for Filetype { + fn from(meta: Metadata) -> Self { + let ft = meta.file_type(); + if ft.is_dir() { + Self::Dir + } else if ft.is_symlink() { + Self::Symlink + } else { + Self::File + } + } +} + +impl fmt::Display for Filetype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::File => "file", + Self::Symlink => "symlink", + Self::Dir => "directory", + } + ) + } +} + +struct Action { + path: String, + prompt: When, + force: bool, + recursive: bool, + verbose: bool, +} + +impl Action { + fn apply(&self) -> Result<(), Box> { + let ft = if self.prompt == When::Always { + match self.prompt() { + Ok(Some(ft)) => ft, + Ok(None) => return Ok(()), + Err(e) => { + if !self.force { + return Err(e.into()); + } else { + return Ok(()); + } + } + } + } else { + match File::open(&self.path) + .and_then(|fd| fd.metadata()) + .and_then(|meta| Ok(Filetype::from(meta))) + { + Ok(ft) => ft, + Err(e) => { + if !self.force { + return Err(e.into()); + } else { + return Ok(()); + } + } + } + }; + match ft { + Filetype::File | Filetype::Symlink => { + fs::remove_file(&self.path)?; + if self.verbose { + println!("removed '{}'", &self.path); + } + } + Filetype::Dir => { + if self.recursive { + match fs::read_dir(&self.path) { + Ok(items) => { + for entry in items { + let act = Action { + path: entry?.path().to_string_lossy().to_string(), + prompt: self.prompt, + force: self.force, + recursive: self.recursive, + verbose: self.verbose, + }; + act.apply()?; + } + } + Err(e) => { + if !self.force { + return Err(e.into()); + } + } + } + fs::remove_dir(&self.path)?; + if self.verbose { + println!("removed directory '{}'", &self.path); + } + } else if !self.force { + let msg = format!("cannot remove '{}': is a directory", &self.path); + return Err(Box::new(io::Error::new(io::ErrorKind::Other, msg))); + } + } + } + Ok(()) + } + + fn prompt(&self) -> Result, Box> { + let path = PathBuf::from(&self.path); + let ft: Filetype = if path.is_dir() { + if self.recursive { + return Ok(Some(Filetype::Dir)); + } else { + return Ok(None); + } + } else { + File::open(path) + .and_then(|fd| fd.metadata()) + .and_then(|meta| Ok(Filetype::from(meta)))? + }; + print!("rm: remove {ft} '{}'? ", &self.path); + let mut reply = String::new(); + let stdin = io::stdin(); + let _read = stdin.read_line(&mut reply); + match reply.as_str() { + "y" | "Y" | "yes" | "Yes" => Ok(Some(ft)), + _ => Ok(None), + } + } +} + +struct AllActions { + items: Vec, + prompt: When, +} + +impl From<&ArgMatches> for AllActions { + fn from(matches: &ArgMatches) -> Self { + let force = matches.get_flag("force"); + let prompt = if force { + When::Never + } else if matches.get_flag("fullnag") { + When::Always + } else if matches.get_flag("softnag") { + When::Once + } else { + match matches.get_one::("interactive") { + Some(w) => w.parse().unwrap_or_default(), + None => When::default(), + } + }; + let mut items: Vec = vec![]; + if let Some(files) = matches.get_many::("file") { + for f in files { + items.push(Action { + path: f.to_owned(), + prompt, + force, + recursive: matches.get_flag("recursive"), + verbose: matches.get_flag("verbose"), + }); + } + } + AllActions { items, prompt } + } +} + +impl AllActions { + fn prompt(&self) -> bool { + let len = self.items.len(); + if len > 3 { + print!("rm: remove {len} arguments? "); + } + let mut reply = String::new(); + let stdin = io::stdin(); + let _read = stdin.read_line(&mut reply); + match reply.as_str() { + "y" | "Y" | "yes" | "Yes" => true, + _ => false, + } + } +} diff --git a/src/cmd/rmdir/mod.rs b/src/cmd/rmdir/mod.rs index 91288e8..ee6d7d6 100644 --- a/src/cmd/rmdir/mod.rs +++ b/src/cmd/rmdir/mod.rs @@ -1,6 +1,6 @@ -use std::{io, error::Error, fs, path::Path}; use super::Cmd; use clap::{Arg, ArgAction, Command, ValueHint}; +use std::{error::Error, fs, io, path::Path}; #[derive(Debug, Default)] pub struct Rmdir;