Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 18 additions & 3 deletions src/runtime/shell/builtin/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ impl ExecState {
#[derive(Clone, Copy)]
pub struct Opts {
/// `-f`, `--force` — ignore nonexistent files and arguments, never prompt.
/// `-f` and the prompting flags cancel each other; the last one given
/// wins, as in GNU rm.
pub(crate) force: bool,
/// Configures how the user should be prompted on removal of files.
pub(crate) prompt_behaviour: PromptBehaviour,
Expand Down Expand Up @@ -134,8 +136,13 @@ impl Rm {
panic!("Invalid");
}
let argc = Builtin::of(interp, cmd).args_slice().len();
// No args / only flags → print usage and exit 1.
// No operands (nothing, or only flags). POSIX: `-f` suppresses
// both the diagnostic and the failure status in that case;
// otherwise print usage and exit 1.
if (idx as usize) >= argc {
if Self::state_mut(interp, cmd).opts.force {
return Builtin::done(interp, cmd, 0);
}
let usage = Kind::Rm.usage_string();
return Self::write_err_literal(interp, cmd, idx, usage);
}
Expand Down Expand Up @@ -512,10 +519,12 @@ impl Rm {
}
b"--interactive=once" => {
opts.prompt_behaviour = PromptBehaviour::Once { removed_count: 0 };
opts.force = false;
RmParseFlag::ContinueParsing
}
b"--interactive=always" => {
opts.prompt_behaviour = PromptBehaviour::Always;
opts.force = false;
RmParseFlag::ContinueParsing
}
_ => RmParseFlag::IllegalOption,
Expand All @@ -530,8 +539,14 @@ impl Rm {
b'r' | b'R' => opts.recursive = true,
b'v' => opts.verbose = true,
b'd' => opts.remove_empty_dirs = true,
b'i' => opts.prompt_behaviour = PromptBehaviour::Once { removed_count: 0 },
b'I' => opts.prompt_behaviour = PromptBehaviour::Always,
b'i' => {
opts.prompt_behaviour = PromptBehaviour::Once { removed_count: 0 };
opts.force = false;
}
b'I' => {
opts.prompt_behaviour = PromptBehaviour::Always;
opts.force = false;
}
_ => return RmParseFlag::IllegalOptionWithFlag,
}
}
Expand Down
34 changes: 34 additions & 0 deletions test/js/bun/shell/commands/rm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,40 @@ describe.concurrent("bunshell rm", () => {
}
});

// POSIX rm: -f shall "not write diagnostic messages or modify the exit
// status in the case of no file operands". GNU and BSD rm exit 0 silently,
// which is what `rm -f ${files}` with an empty list relies on. Without -f,
// no operands is still a usage error.
describe("no operands", () => {
const none: string[] = [];
const usage = "usage: rm [-f | -i] [-dIPRrvWx] file ...\n unlink [--] file\n";

TestBuilder.command`rm -f`.stdout("").stderr("").exitCode(0).runAsTest("rm -f");
TestBuilder.command`rm -f`.quiet().stdout("").stderr("").exitCode(0).runAsTest("rm -f (quiet)");
TestBuilder.command`rm -f ${none}`.stdout("").stderr("").exitCode(0).runAsTest("rm -f with an empty list");
TestBuilder.command`rm -rf`.stdout("").stderr("").exitCode(0).runAsTest("rm -rf");
TestBuilder.command`rm -fv`.stdout("").stderr("").exitCode(0).runAsTest("rm -fv");
TestBuilder.command`rm -f --interactive=never`
.stdout("")
.stderr("")
.exitCode(0)
.runAsTest("rm -f --interactive=never");
TestBuilder.command`rm -i -f`.stdout("").stderr("").exitCode(0).runAsTest("-f given after -i");
TestBuilder.command`rm -f ${none} && echo cleaned`
.stdout("cleaned\n")
.stderr("")
.exitCode(0)
.runAsTest("rm -f succeeds in a && chain");

TestBuilder.command`rm`.stdout("").stderr(usage).exitCode(1).runAsTest("rm");
TestBuilder.command`rm -r`.stdout("").stderr(usage).exitCode(1).runAsTest("rm -r");
TestBuilder.command`rm -rv`.quiet().stdout("").stderr(usage).exitCode(1).runAsTest("rm -rv (quiet)");
// A prompting flag given after -f cancels it, as in GNU rm.
for (const flag of ["-i", "-I", "--interactive=once", "--interactive=always"]) {
TestBuilder.command`rm -f ${flag}`.stdout("").stderr(usage).exitCode(1).runAsTest(`${flag} given after -f`);
}
});

test("recursive", async () => {
const files = {
"existent.txt": "",
Expand Down
Loading