diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 733b4dfc4..863336f5d 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -8,6 +8,7 @@ use clap::{Arg, ArgAction, Command, builder::ValueParser, parser::ValueSource}; use std::ffi::{OsStr, OsString}; use std::fs::{self, Metadata}; +use std::io::{IsTerminal, stdin}; use std::ops::BitOr; #[cfg(not(windows))] use std::os::unix::ffi::OsStrExt; @@ -68,6 +69,25 @@ pub struct Options { pub dir: bool, /// `-v`, `--verbose` pub verbose: bool, + #[doc(hidden)] + /// `---presume-input-tty` + /// Always use `None`; GNU flag for testing use only + pub __presume_input_tty: Option, +} + +impl Default for Options { + fn default() -> Self { + Self { + force: false, + interactive: InteractiveMode::PromptProtected, + one_fs: false, + preserve_root: true, + recursive: false, + dir: false, + verbose: false, + __presume_input_tty: None, + } + } } const ABOUT: &str = help_about!("rm.md"); @@ -145,6 +165,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { recursive: matches.get_flag(OPT_RECURSIVE), dir: matches.get_flag(OPT_DIR), verbose: matches.get_flag(OPT_VERBOSE), + __presume_input_tty: if matches.get_flag(PRESUME_INPUT_TTY) { + Some(true) + } else { + None + }, }; if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) { let msg: String = format!( @@ -608,13 +633,15 @@ fn prompt_file(path: &Path, options: &Options) -> bool { prompt_yes!("remove file {}?", path.quote()) }; } - prompt_file_permission_readonly(path) + prompt_file_permission_readonly(path, options) } -fn prompt_file_permission_readonly(path: &Path) -> bool { - match fs::metadata(path) { - Ok(_) if is_writable(path) => true, - Ok(metadata) if metadata.len() == 0 => prompt_yes!( +fn prompt_file_permission_readonly(path: &Path, options: &Options) -> bool { + let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal(); + match (stdin_ok, fs::metadata(path), options.interactive) { + (false, _, InteractiveMode::PromptProtected) => true, + (_, Ok(_), _) if is_writable(path) => true, + (_, Ok(metadata), _) if metadata.len() == 0 => prompt_yes!( "remove write-protected regular empty file {}?", path.quote() ), @@ -622,26 +649,29 @@ fn prompt_file_permission_readonly(path: &Path) -> bool { } } -// For directories finding if they are writable or not is a hassle. In Unix we can use the built-in rust crate to to check mode bits. But other os don't have something similar afaik +// For directories finding if they are writable or not is a hassle. In Unix we can use the built-in rust crate to check mode bits. But other os don't have something similar afaik // Most cases are covered by keep eye out for edge cases #[cfg(unix)] fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool { + let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal(); match ( + stdin_ok, is_readable_metadata(metadata), is_writable_metadata(metadata), options.interactive, ) { - (false, false, _) => prompt_yes!( + (false, _, _, InteractiveMode::PromptProtected) => true, + (_, false, false, _) => prompt_yes!( "attempt removal of inaccessible directory {}?", path.quote() ), - (false, true, InteractiveMode::Always) => prompt_yes!( + (_, false, true, InteractiveMode::Always) => prompt_yes!( "attempt removal of inaccessible directory {}?", path.quote() ), - (true, false, _) => prompt_yes!("remove write-protected directory {}?", path.quote()), - (_, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()), - (_, _, _) => true, + (_, true, false, _) => prompt_yes!("remove write-protected directory {}?", path.quote()), + (_, _, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()), + (_, _, _, _) => true, } } @@ -666,12 +696,12 @@ fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata use std::os::windows::prelude::MetadataExt; use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_READONLY; let not_user_writable = (metadata.file_attributes() & FILE_ATTRIBUTE_READONLY) != 0; - if not_user_writable { - prompt_yes!("remove write-protected directory {}?", path.quote()) - } else if options.interactive == InteractiveMode::Always { - prompt_yes!("remove directory {}?", path.quote()) - } else { - true + let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal(); + match (stdin_ok, not_user_writable, options.interactive) { + (false, _, InteractiveMode::PromptProtected) => true, + (_, true, _) => prompt_yes!("remove write-protected directory {}?", path.quote()), + (_, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()), + (_, _, _) => true, } } diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index e61f4196b..5ce3a6107 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -579,6 +579,50 @@ fn test_rm_prompts() { assert!(!at.dir_exists("a")); } +#[cfg(feature = "chmod")] +#[test] +fn test_rm_prompts_no_tty() { + // This test ensures InteractiveMode.PromptProtected proceeds silently with non-interactive stdin + + use std::io::Write; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("a/"); + + let file_1 = "a/empty"; + let file_2 = "a/empty-no-write"; + let file_3 = "a/f-no-write"; + + at.touch(file_1); + at.touch(file_2); + at.make_file(file_3) + .write_all(b"not-empty") + .expect("Couldn't write to a/f-no-write"); + + at.symlink_dir("a/empty-f", "a/slink"); + at.symlink_dir(".", "a/slink-dot"); + + let dir_1 = "a/b/"; + let dir_2 = "a/b-no-write/"; + + at.mkdir(dir_1); + at.mkdir(dir_2); + + scene + .ccmd("chmod") + .arg("u-w") + .arg(file_3) + .arg(dir_2) + .arg(file_2) + .succeeds(); + + scene.ucmd().arg("-r").arg("a").succeeds().no_output(); + + assert!(!at.dir_exists("a")); +} + #[test] fn test_rm_force_prompts_order() { // Needed for talking with stdin on platforms where CRLF or LF matters @@ -646,7 +690,13 @@ fn test_prompt_write_protected_yes() { scene.ccmd("chmod").arg("0").arg(file_1).succeeds(); - scene.ucmd().arg(file_1).pipe_in("y").succeeds(); + scene + .ucmd() + .arg("---presume-input-tty") + .arg(file_1) + .pipe_in("y") + .succeeds() + .stderr_contains("rm: remove write-protected regular empty file"); assert!(!at.file_exists(file_1)); } @@ -661,7 +711,13 @@ fn test_prompt_write_protected_no() { scene.ccmd("chmod").arg("0").arg(file_2).succeeds(); - scene.ucmd().arg(file_2).pipe_in("n").succeeds(); + scene + .ucmd() + .arg("---presume-input-tty") + .arg(file_2) + .pipe_in("n") + .succeeds() + .stderr_contains("rm: remove write-protected regular empty file"); assert!(at.file_exists(file_2)); }