diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index f05e5d12c..5406979f7 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -303,13 +303,36 @@ fn handle_dir(path: &Path, options: &Options) -> bool { } } else { let mut dirs: VecDeque = VecDeque::new(); + // The Paths to not descend into. We need to this because WalkDir doesn't have a way, afaik, to not descend into a directory + // So we have to just ignore paths as they come up if they start with a path we aren't descending into + let mut not_descended: Vec = Vec::new(); - for entry in WalkDir::new(path) { + 'outer: for entry in WalkDir::new(path) { match entry { Ok(entry) => { + if options.interactive == InteractiveMode::Always { + for not_descend in ¬_descended { + if entry.path().starts_with(not_descend) { + // We don't need to continue the rest of code in this loop if we are in a directory we don't want to descend into + continue 'outer; + } + } + } let file_type = entry.file_type(); if file_type.is_dir() { - dirs.push_back(entry); + // If we are in Interactive Mode Always and the directory isn't empty we ask if we should descend else we push this directory onto dirs vector + if options.interactive == InteractiveMode::Always + && fs::read_dir(entry.path()).unwrap().count() != 0 + { + // If we don't descend we push this directory onto our not_descended vector else we push this directory onto dirs vector + if prompt_descend(entry.path()) { + dirs.push_back(entry); + } else { + not_descended.push(entry.path().to_path_buf()); + } + } else { + dirs.push_back(entry); + } } else { had_err = remove_file(entry.path(), options).bitor(had_err); } @@ -447,6 +470,10 @@ fn prompt_write_protected(path: &Path, is_dir: bool, options: &Options) -> bool } } +fn prompt_descend(path: &Path) -> bool { + prompt(&(format!("rm: descend into directory {}? ", path.quote()))) +} + fn prompt_file(path: &Path, is_dir: bool) -> bool { if is_dir { prompt(&(format!("rm: remove directory {}? ", path.quote()))) diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index 472d12bed..b600c2090 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -353,6 +353,53 @@ fn test_rm_interactive_never() { assert!(!at.file_exists(file_2)); } +#[test] +fn test_rm_descend_directory() { + // This test descends into each directory and deletes the files and folders inside of them + // This test will have the rm process asks 6 question and us answering Y to them will delete all the files and folders + use std::io::Write; + use std::process::Child; + + // Needed for talking with stdin on platforms where CRLF or LF matters + const END_OF_LINE: &str = if cfg!(windows) { "\r\n" } else { "\n" }; + + let yes = format!("y{}", END_OF_LINE); + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_1 = "a/at.txt"; + let file_2 = "a/b/bt.txt"; + + at.mkdir_all("a/b/"); + at.touch(file_1); + at.touch(file_2); + + let mut child: Child = scene.ucmd().arg("-ri").arg("a").run_no_wait(); + + // Needed so that we can talk to the rm program + let mut child_stdin = child.stdin.take().unwrap(); + child_stdin.write_all(yes.as_bytes()).unwrap(); + child_stdin.flush().unwrap(); + child_stdin.write_all(yes.as_bytes()).unwrap(); + child_stdin.flush().unwrap(); + child_stdin.write_all(yes.as_bytes()).unwrap(); + child_stdin.flush().unwrap(); + child_stdin.write_all(yes.as_bytes()).unwrap(); + child_stdin.flush().unwrap(); + child_stdin.write_all(yes.as_bytes()).unwrap(); + child_stdin.flush().unwrap(); + child_stdin.write_all(yes.as_bytes()).unwrap(); + child_stdin.flush().unwrap(); + + child.wait_with_output().unwrap(); + + assert!(!at.dir_exists("a/b")); + assert!(!at.dir_exists("a")); + assert!(!at.file_exists(file_1)); + assert!(!at.file_exists(file_2)); +} + #[test] #[ignore = "issue #3722"] fn test_rm_directory_rights_rm1() {