shitbox/src/cmd/rm/mod.rs

333 lines
9.8 KiB
Rust

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<dyn std::error::Error>> {
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<crate::Path> {
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<Self, Self::Err> {
match s {
"never" => Ok(Self::Never),
"once" => Ok(Self::Once),
"always" => Ok(Self::Always),
_ => Err(ParseWhenError),
}
}
}
enum Filetype {
File,
Dir,
Symlink,
}
impl From<Metadata> 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<dyn Error>> {
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<Option<Filetype>, Box<dyn Error>> {
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)))?
};
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<Action>,
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::<String>("interactive") {
Some(w) => w.parse().unwrap_or_default(),
None => When::default(),
}
};
let mut items: Vec<Action> = vec![];
if let Some(files) = matches.get_many::<String>("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);
match reply.as_str() {
"y" | "Y" | "yes" | "Yes" => true,
_ => false,
}
} else {
true
}
}
}