Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/shell/commands/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async fn execute_remove(cwd: &Path, args: &[OsString]) -> Result<()> {
remove_file_or_dir(&path, &flags).await
};
if let Err(err) = result
&& (err.kind() != ErrorKind::NotFound || !flags.force)
&& !(flags.force && should_ignore_error_with_force(&err))
{
bail!(
"cannot remove '{}': {}",
Expand All @@ -76,6 +76,24 @@ async fn execute_remove(cwd: &Path, args: &[OsString]) -> Result<()> {
Ok(())
}

/// Check if an error should be silently ignored when -f (force) flag is used.
/// This includes:
/// - NotFound: file doesn't exist
/// - On Windows: InvalidFilename (os error 123) - happens when literal glob
/// patterns like "*.nonexistent" are passed (since * is invalid in Windows filenames)
fn should_ignore_error_with_force(err: &std::io::Error) -> bool {
if err.kind() == ErrorKind::NotFound {
return true;
}
// On Windows, glob characters like * are invalid in filenames.
// When a non-matching glob is passed literally, Windows returns error 123.
#[cfg(windows)]
if err.raw_os_error() == Some(123) {
return true;
}
false
}

async fn remove_file_or_dir(
path: &Path,
flags: &RmFlags<'_>,
Expand Down
22 changes: 22 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,28 @@ async fn rm() {
.assert_exit_code(1)
.run()
.await;

// rm -f should ignore non-existent files
TestBuilder::new()
.command("rm -f nonexistent.txt")
.assert_exit_code(0)
.run()
.await;

// rm -rf should ignore non-existent directories
TestBuilder::new()
.command("rm -rf nonexistent_dir")
.assert_exit_code(0)
.run()
.await;

// rm -rf with glob pattern that matches nothing should succeed
// (when failglob is disabled, the pattern is passed literally to rm)
TestBuilder::new()
.command("shopt -u failglob && rm -rf *.nonexistent")
.assert_exit_code(0)
.run()
.await;
}

// Basic integration tests as there are unit tests in the commands
Expand Down