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, } } }