diff --git a/Cargo.lock b/Cargo.lock index 846c4a88..f14bc59a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -596,7 +596,9 @@ version = "0.1.1" dependencies = [ "clap", "gettext-rs", + "libc", "plib", + "walkdir", ] [[package]] @@ -629,9 +631,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -720,6 +722,15 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.197" @@ -823,18 +834,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -923,6 +934,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -945,6 +966,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/README.md b/README.md index 4d939e49..114d64e9 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ https://github.com/jgarzik/posixutils - [ ] qstat (Batch cat.) - [ ] qsub (Batch cat.) - [x] renice - - [ ] rm + - [x] rm - [ ] rmdel (SCCS) - [x] rmdir - [ ] sact (SCCS) diff --git a/tree/Cargo.toml b/tree/Cargo.toml index 6ecb86ff..658d3d9b 100644 --- a/tree/Cargo.toml +++ b/tree/Cargo.toml @@ -10,6 +10,12 @@ repository = "https://github.com/rustcoreutils/posixutils-rs.git" plib = { path = "../plib" } clap = { version = "4", features = ["derive"] } gettext-rs = { version = "0.7", features = ["gettext-system"] } +walkdir = "2.5" +libc = "0.2" + +[[bin]] +name = "rm" +path = "src/rm.rs" [[bin]] name = "rmdir" diff --git a/tree/src/rm.rs b/tree/src/rm.rs new file mode 100644 index 00000000..d5e6ce7f --- /dev/null +++ b/tree/src/rm.rs @@ -0,0 +1,153 @@ +// +// Copyright (c) 2024 Jeff Garzik +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +extern crate clap; +extern crate libc; +extern crate plib; + +use clap::Parser; +use gettextrs::{bind_textdomain_codeset, gettext, textdomain}; +use plib::PROJECT_NAME; +use std::ffi::OsStr; +use std::fs; +use std::io::{self, BufRead, Write}; +use walkdir::WalkDir; + +/// rm - remove directory entries +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// Do not prompt for confirmation. + #[arg(short, long)] + force: bool, + + /// Prompt for confirmation. + #[arg(short, long)] + interactive: bool, + + /// Remove file hierarchies. + #[arg(short, short_alias = 'r', long)] + recurse: bool, + + /// Filepaths to remove + files: Vec, +} + +struct RmConfig { + args: Args, + is_tty: bool, +} + +fn prompt(cfg: &RmConfig, filepath: &OsStr, metadata: &fs::Metadata) -> bool { + let writable = !metadata.permissions().readonly(); + + let do_prompt = cfg.args.interactive || (!cfg.args.force && !writable && cfg.is_tty); + if !do_prompt { + return true; + } + + let prompt = format!( + "{} {}? ", + gettext("remove read-only file"), + filepath.to_string_lossy() + ); + io::stdout() + .write_all(prompt.as_bytes()) + .expect("stdout failure"); + io::stdout().flush().expect("stdout failure"); + + let mut line = String::new(); + io::stdin() + .lock() + .read_line(&mut line) + .expect("stdin read failure"); + + line.starts_with("y") || line.starts_with("Y") +} + +fn rm_file(cfg: &RmConfig, filepath: &OsStr, metadata: &fs::Metadata) -> io::Result<()> { + if prompt(cfg, filepath, metadata) { + fs::remove_file(filepath) + } else { + Ok(()) + } +} + +fn rm_dir_simple(cfg: &RmConfig, filepath: &OsStr, metadata: &fs::Metadata) -> io::Result<()> { + if prompt(cfg, filepath, metadata) { + fs::remove_dir(filepath) + } else { + Ok(()) + } +} + +fn rm_directory(cfg: &RmConfig, filepath: &OsStr) -> io::Result<()> { + if !cfg.args.recurse { + eprintln!( + "{} {}", + filepath.to_string_lossy(), + gettext("is a directory; not following") + ); + return Ok(()); + } + + for entry in WalkDir::new(filepath) + .contents_first(true) + .follow_links(false) + { + let entry = entry?; + let subname = entry.path().as_os_str(); + let sub_metadata = entry.metadata()?; + if sub_metadata.is_dir() { + rm_dir_simple(cfg, subname, &sub_metadata)?; + } else { + rm_file(cfg, subname, &sub_metadata)?; + } + } + + Ok(()) +} + +fn rm_path(cfg: &RmConfig, filepath: &OsStr) -> io::Result<()> { + let metadata = fs::metadata(filepath)?; + + if metadata.is_dir() { + rm_directory(cfg, filepath) + } else { + rm_file(cfg, filepath, &metadata) + } +} + +fn main() -> Result<(), Box> { + // parse command line arguments + let args = Args::parse(); + + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + + let is_tty = unsafe { libc::isatty(libc::STDIN_FILENO) != 0 }; + let cfg = RmConfig { args, is_tty }; + + let mut exit_code = 0; + + for filepath_str in &cfg.args.files { + let filepath = OsStr::new(filepath_str); + match rm_path(&cfg, &filepath) { + Ok(()) => {} + Err(e) => { + exit_code = 1; + if !cfg.args.force { + eprintln!("{}: {}", filepath.to_string_lossy(), e); + } + } + } + } + + std::process::exit(exit_code) +}