use super::Cmd; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint}; use shitbox::{args, fs::FileType}; use std::{ error::Error, fmt, fs::{self, File}, io, path::PathBuf, str::FromStr, }; #[derive(Debug)] 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), args::recursive(), args::verbose(), 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: &clap::ArgMatches) -> Result<(), Box> { 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); } } } } Ok(()) } fn path(&self) -> Option { Some(shitbox::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), } } } 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); } else { return Ok(()); } } } } else { match File::open(&self.path) .and_then(|fd| fd.metadata()) .map(FileType::from) { 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()) .map(FileType::from)? }; eprint!("rm: remove {ft} '{}'? ", &self.path); let mut reply = String::new(); let stdin = io::stdin(); let _read = stdin.read_line(&mut reply); match reply.trim_end() { "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 { eprint!("rm: remove {len} arguments? "); let mut reply = String::new(); let stdin = io::stdin(); let _read = stdin.read_line(&mut reply); matches!(reply.trim_end(), "y" | "Y" | "yes" | "Yes") } else { true } } }