Add rm applet

This commit is contained in:
Nathan Fisher 2023-01-17 17:27:41 -05:00
parent f7b7fcca16
commit 8a14cfb590
4 changed files with 364 additions and 33 deletions

60
Cargo.lock generated
View File

@ -27,9 +27,9 @@ checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.0.29" version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"clap_lex", "clap_lex",
@ -40,9 +40,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.0.6" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da" checksum = "ce8955d4e8cd4f28f9a01c93a050194c4d131e73ca02f6636bcddbed867014d7"
dependencies = [ dependencies = [
"clap", "clap",
] ]
@ -59,18 +59,18 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]] [[package]]
name = "clap_mangen" name = "clap_mangen"
version = "0.2.5" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e503c3058af0a0854668ea01db55c622482a080092fede9dd2e00a00a9436504" checksum = "eb258c6232b4d728d13d6072656627924c16707aae6267cd5a1ea05abff9a25c"
dependencies = [ dependencies = [
"clap", "clap",
"roff", "roff",
@ -134,9 +134,9 @@ dependencies = [
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys",
@ -144,9 +144,9 @@ dependencies = [
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
dependencies = [ dependencies = [
"hermit-abi 0.2.6", "hermit-abi 0.2.6",
"io-lifetimes", "io-lifetimes",
@ -196,9 +196,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.36.5" version = "0.36.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -239,9 +239,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [ dependencies = [
"winapi-util", "winapi-util",
] ]
@ -303,42 +303,42 @@ dependencies = [
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.0" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"

View File

@ -46,7 +46,7 @@ pub use {
self::hostname::Hostname, self::shitbox::Shitbox, base32::Base32, base64::Base64, self::hostname::Hostname, self::shitbox::Shitbox, base32::Base32, base64::Base64,
basename::Basename, bootstrap::Bootstrap, clear::Clear, cut::Cut, dirname::Dirname, echo::Echo, basename::Basename, bootstrap::Bootstrap, clear::Clear, cut::Cut, dirname::Dirname, echo::Echo,
factor::Factor, fold::Fold, groups::Groups, head::Head, link::Link, mountpoint::Mountpoint, 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, sleep::Sleep, sync::Sync as SyncCmd, unlink::Unlink, which::Which, whoami::Whoami, yes::Yes,
}; };
@ -87,6 +87,7 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
"nologin" => Some(Box::new(Nologin::default())), "nologin" => Some(Box::new(Nologin::default())),
"nproc" => Some(Box::new(Nproc::default())), "nproc" => Some(Box::new(Nproc::default())),
"rev" => Some(Box::new(Rev::default())), "rev" => Some(Box::new(Rev::default())),
"rm" => Some(Box::new(Rm::default())),
"rmdir" => Some(Box::new(Rmdir::default())), "rmdir" => Some(Box::new(Rmdir::default())),
"shitbox" => Some(Box::new(Shitbox::default())), "shitbox" => Some(Box::new(Shitbox::default())),
"sleep" => Some(Box::new(Sleep::default())), "sleep" => Some(Box::new(Sleep::default())),
@ -100,7 +101,7 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
} }
} }
pub static COMMANDS: [&str; 28] = [ pub static COMMANDS: [&str; 29] = [
"base32", "base32",
"base64", "base64",
"basename", "basename",
@ -120,6 +121,7 @@ pub static COMMANDS: [&str; 28] = [
"nologin", "nologin",
"nproc", "nproc",
"rev", "rev",
"rm",
"rmdir", "rmdir",
"sleep", "sleep",
"shitbox", "shitbox",

View File

@ -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<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)))?
};
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<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 {
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,
}
}
}

View File

@ -1,6 +1,6 @@
use std::{io, error::Error, fs, path::Path};
use super::Cmd; use super::Cmd;
use clap::{Arg, ArgAction, Command, ValueHint}; use clap::{Arg, ArgAction, Command, ValueHint};
use std::{error::Error, fs, io, path::Path};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Rmdir; pub struct Rmdir;