289 lines
8.6 KiB
Rust
289 lines
8.6 KiB
Rust
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, 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),
|
|
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<dyn std::error::Error>> {
|
|
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<shitbox::Path> {
|
|
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<Self, Self::Err> {
|
|
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<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.trim_end() {
|
|
"y" | "Y" | "yes" | "Yes" => true,
|
|
_ => false,
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|