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
4 changes: 1 addition & 3 deletions src/runtime/shell/builtin/cat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,7 @@ impl FlagParser for Opts {
b't' => Some(ParseFlagResult::Unsupported(unsupported_flag(b"-t"))),
b'u' => Some(ParseFlagResult::Unsupported(unsupported_flag(b"-u"))),
b'v' => Some(ParseFlagResult::Unsupported(unsupported_flag(b"-v"))),
_ => Some(ParseFlagResult::IllegalOption(
&raw const smallflags[1 + i..],
)),
_ => Some(ParseFlagResult::IllegalOption(&raw const smallflags[i..=i])),
}
}
}
2 changes: 1 addition & 1 deletion src/runtime/shell/builtin/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,8 @@
self.verbose = true;
Some(ParseFlagResult::ContinueParsing)
}
b'n' => Some(ParseFlagResult::ContinueParsing),
_ => Some(ParseFlagResult::IllegalOption(&raw const smallflags[i..])),
_ => Some(ParseFlagResult::IllegalOption(&raw const smallflags[i..=i])),

Check notice on line 812 in src/runtime/shell/builtin/cp.rs

View check run for this annotation

Claude / Claude Code Review

cp's parse_short returns Some(ContinueParsing) for R/v/n, silently dropping the rest of a flag cluster (pre-existing)

Pre-existing (not from this PR): the `b'R'`, `b'v'`, and `b'n'` arms just above return `Some(ParseFlagResult::ContinueParsing)` instead of `None`, so `parse_one_flag` exits the cluster loop early and silently drops the rest of the flags — `cp -Rv` never turns on verbose, and `cp -Rz` doesn't reject `z`. This is why the new test's cp row only covers `["-zR", "z"]` and omits the `["-Rz", "z"]` case that mkdir/ls/rm/mv all cover. Fix: return `None` for these three arms (as mkdir does for `p`/`v`).
Comment thread
robobun marked this conversation as resolved.
Outdated
}
}
}
2 changes: 1 addition & 1 deletion src/runtime/shell/builtin/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ impl Ls {
| b'h' | b'H' | b'i' | b'I' | b'k' | b'L' | b'm' | b'n' | b'N' | b'o' | b'p'
| b'q' | b'Q' | b's' | b'S' | b't' | b'T' | b'u' | b'U' | b'v' | b'w' | b'x'
| b'X' | b'Z' => {}
_ => return ParseFlag::IllegalOption(Box::from(&flag[1..2])),
_ => return ParseFlag::IllegalOption(Box::from([ch])),
}
}
ParseFlag::ContinueParsing
Expand Down
4 changes: 1 addition & 3 deletions src/runtime/shell/builtin/mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,7 @@ impl FlagParser for Opts {
self.verbose = true;
None
}
_ => Some(ParseFlagResult::IllegalOption(
&raw const smallflags[1 + i..],
)),
_ => Some(ParseFlagResult::IllegalOption(&raw const smallflags[i..=i])),
}
}
}
14 changes: 8 additions & 6 deletions src/runtime/shell/builtin/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ pub enum MvState {

/// mv uses its own simpler parser.
enum MvParseError {
IllegalOption(&'static [u8]),
/// The rejected option byte.
IllegalOption(u8),
ShowUsage,
}

Expand Down Expand Up @@ -94,11 +95,11 @@ impl Mv {
Tag::Idle => {
if let Err(e) = Self::parse_opts(interp, cmd) {
let buf: Vec<u8> = match e {
MvParseError::IllegalOption(s) => Builtin::fmt_error_arena(
MvParseError::IllegalOption(ch) => Builtin::fmt_error_arena(
interp,
cmd,
Some(Kind::Mv),
format_args!("illegal option -- {}\n", bstr::BStr::new(s)),
format_args!("illegal option -- {}\n", bstr::BStr::new(&[ch])),
)
.to_vec(),
MvParseError::ShowUsage => Kind::Mv.usage_string().to_vec(),
Expand Down Expand Up @@ -360,7 +361,7 @@ impl Mv {
return Ok(());
}
MvFlag::ContinueParsing => {}
MvFlag::IllegalOption(s) => return Err(MvParseError::IllegalOption(s)),
MvFlag::IllegalOption(ch) => return Err(MvParseError::IllegalOption(ch)),
}
idx += 1;
}
Expand All @@ -374,7 +375,7 @@ impl Mv {
for &ch in &flag[1..] {
match ch {
b'f' | b'h' | b'i' | b'n' | b'v' => {}
_ => return MvFlag::IllegalOption(b"-"),
_ => return MvFlag::IllegalOption(ch),
}
}
MvFlag::ContinueParsing
Expand All @@ -396,7 +397,8 @@ impl Drop for Mv {
enum MvFlag {
ContinueParsing,
Done,
IllegalOption(&'static [u8]),
/// The rejected option byte.
IllegalOption(u8),
}

/// `openat(target, O_RDONLY|O_DIRECTORY)`
Expand Down
15 changes: 7 additions & 8 deletions src/runtime/shell/builtin/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ pub enum PromptBehaviour {
enum RmParseFlag {
ContinueParsing,
Done,
/// Unknown `--long` option.
IllegalOption,
IllegalOptionWithFlag,
/// Unknown short option: the rejected byte.
IllegalOptionWithFlag(u8),
}

impl Rm {
Expand Down Expand Up @@ -248,7 +250,7 @@ impl Rm {
b"rm: illegal option -- -\n",
);
}
RmParseFlag::IllegalOptionWithFlag => {
RmParseFlag::IllegalOptionWithFlag(ch) => {
if let Some(safeguard) = Builtin::of(interp, cmd).stderr.needs_io() {
Self::state_mut(interp, cmd).state = RmState::ParseOpts {
idx,
Expand All @@ -258,18 +260,15 @@ impl Rm {
return Builtin::of_mut(interp, cmd).stderr.enqueue_fmt(
child,
Some(Kind::Rm),
format_args!(
"illegal option -- {}\n",
bstr::BStr::new(&arg[1..])
),
format_args!("illegal option -- {}\n", bstr::BStr::new(&[ch])),
safeguard,
);
}
let buf = Builtin::fmt_error_arena(
interp,
cmd,
Some(Kind::Rm),
format_args!("illegal option -- {}\n", bstr::BStr::new(&arg[1..])),
format_args!("illegal option -- {}\n", bstr::BStr::new(&[ch])),
)
.to_vec();
let _ = Builtin::write_no_io(interp, cmd, IoKind::Stderr, &buf);
Expand Down Expand Up @@ -532,7 +531,7 @@ impl Rm {
b'd' => opts.remove_empty_dirs = true,
b'i' => opts.prompt_behaviour = PromptBehaviour::Once { removed_count: 0 },
b'I' => opts.prompt_behaviour = PromptBehaviour::Always,
_ => return RmParseFlag::IllegalOptionWithFlag,
_ => return RmParseFlag::IllegalOptionWithFlag(ch),
}
}
RmParseFlag::ContinueParsing
Expand Down
4 changes: 1 addition & 3 deletions src/runtime/shell/builtin/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,7 @@ impl FlagParser for Opts {
b'm' => Some(ParseFlagResult::Unsupported(unsupported_flag(b"-m"))),
b'r' => Some(ParseFlagResult::Unsupported(unsupported_flag(b"-r"))),
b't' => Some(ParseFlagResult::Unsupported(unsupported_flag(b"-t"))),
_ => Some(ParseFlagResult::IllegalOption(
&raw const smallflags[1 + i..],
)),
_ => Some(ParseFlagResult::IllegalOption(&raw const smallflags[i..=i])),
}
}
}
5 changes: 5 additions & 0 deletions src/runtime/shell/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2348,6 +2348,11 @@ pub trait FlagParser {
/// Handle a `--long` flag. Return `None` to fall through to short parsing.
fn parse_long(&mut self, flag: &[u8]) -> Option<ParseFlagResult>;
/// Handle one byte of a `-abc` cluster. Return `None` to keep iterating.
///
/// `smallflags` is the argument minus its leading `-` and `ch == smallflags[i]`.
/// An unknown byte is reported as `IllegalOption(&raw const smallflags[i..=i])`:
/// the message names that one byte, as getopt(3) does, and the slice borrows
/// argv, which outlives the error (see [`ParseError::opt`]).
Comment thread
robobun marked this conversation as resolved.
Outdated
fn parse_short(&mut self, ch: u8, smallflags: &[u8], i: usize) -> Option<ParseFlagResult>;
}

Expand Down
10 changes: 2 additions & 8 deletions test/js/bun/shell/commands/ls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,17 +288,11 @@ describe.concurrent("bunshell ls", () => {
});

test("invalid flag", async () => {
await TestBuilder.command`ls -z`
.exitCode(1)
.stderr(s => expect(s).toContain("illegal option"))
.run();
await TestBuilder.command`ls -z`.exitCode(1).stderr("ls: illegal option -- z\n").run();
});

test("invalid combined flags", async () => {
await TestBuilder.command`ls -az`
.exitCode(1)
.stderr(s => expect(s).toContain("illegal option"))
.run();
await TestBuilder.command`ls -az`.exitCode(1).stderr("ls: illegal option -- z\n").run();
});

test.if(isPosix)("permission denied directory", async () => {
Expand Down
31 changes: 29 additions & 2 deletions test/js/bun/shell/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ describe("bun exec", () => {
// prettier-ignore
const programs = [
// ["cat", 1, "", ""],
["touch", 1, "touch: illegal option -- help\n", ""],
["mkdir", 1, "mkdir: illegal option -- help\n", ""],
["touch", 1, "touch: illegal option -- -\n", ""],
["mkdir", 1, "mkdir: illegal option -- -\n", ""],
// ["cd", 1, "cd: no such file or directory: --help\n", ""],
["echo", 0, "", "--help\n"],
["pwd", 1, "pwd: too many arguments\n", ""],
Expand All @@ -71,6 +71,33 @@ describe("bun exec", () => {
}
});

// Like getopt(3), the message names the one byte that was rejected, wherever
// it sits in a cluster of short flags. An unknown --long option is rejected
// at its second `-`, so it is reported as `-` (what BSD getopt prints too).
describe("illegal option names the rejected flag", () => {
// prettier-ignore
const programs: [program: string, cases: [args: string, rejected: string][]][] = [
["cat", [["-z", "z"], ["-zb", "z"], ["--bogus", "-"]]],
["touch", [["-z", "z"], ["-za", "z"], ["--bogus", "-"]]],
["mkdir", [["-z", "z"], ["-pz", "z"], ["-zp", "z"], ["--bogus", "-"]]],
["cp", [["-z", "z"], ["-zR", "z"], ["--bogus", "-"]]],
["ls", [["-z", "z"], ["-az", "z"], ["-za", "z"], ["--bogus", "-"]]],
["rm", [["-z", "z"], ["-rz", "z"], ["-zr", "z"], ["--bogus", "-"]]],
["mv", [["-z", "z"], ["-fz", "z"], ["-zf", "z"], ["--bogus", "-"]]],
];
for (const [program, cases] of programs) {
const script = cases.map(([args]) => `${program} ${args}`).join("; ");
const stderr = cases.map(([, rejected]) => `${program}: illegal option -- ${rejected}\n`).join("");
TestBuilder.command`${BUN} exec ${script}`
// cat and cp are builtins only on Windows unless this flag is set.
.env({ ...bunEnv, BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS: "1" })
.exitCode(1)
.stderr(stderr)
.stdout("")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.runAsTest(program);
}
});

TestBuilder.command`${BUN} exec cd`
.env(bunEnv)
.exitCode(0)
Expand Down
Loading