shell(rm): reject -i/-I/--interactive by name through the shared unsupported-option path - #39233
shell(rm): reject -i/-I/--interactive by name through the shared unsupported-option path#39233robobun wants to merge 2 commits into
Conversation
|
Warning Review limit reached
Next review available in: 29 minutes Limit details: You’ve used all 5 included reviews currently available under your plan. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 6:52 PM PT - Aug 15th, 2026
✅ @robobun, your commit 758ffabfba483c3714bfe09457f3d9ad9dd60abd passed in 🧪 To try this PR locally: bunx bun-pr 39233That installs a local version of the PR into your bun-39233 --bun |
|
Status: fix is up in this PR (current revision: 758ffab). Reproduced on bun 1.4.0 (and a debug build of main at bcba472): With this branch each of those prints Tests: CI (build 98754, first revision): |
There was a problem hiding this comment.
LGTM — routes rm's unsupported-prompting rejection through the shared Builtin::fail_parse path and fixes the swapped -i/-I → PromptBehaviour mapping.
What was reviewed:
fail_parse+ParseError::Unsupported(unsupported_flag(...))matches the existing pattern incat/cp/mkdir/touch; theset_wait_errclosure setsRmState::ParseOpts { wait_write_err: true }exactly aswrite_err_literaldid, andon_io_writer_chunk's non-Execarm already resolves that to exit 1.PromptBehaviour::flag()and the swappedb'i'/b'I'arms now agree with the enum's doc comments and the--interactive=once/alwayslong-form arms.- Test matrix covers each spelling, clustered short flags, last-wins ordering, both stderr paths (quiet/non-quiet), the
-f/--interactive=nevercancellation cases, and the no-operand usage path.
Extended reasoning...
Overview
Two files: src/runtime/shell/builtin/rm.rs replaces the bespoke b"rm: \"-i\" is not supported yet" literal (no trailing newline, always said -i) with a call to the shared Builtin::fail_parse → write_failing_error path, adds a PromptBehaviour::flag() helper that maps Once→-I / Always→-i, and swaps the b'i'/b'I' short-flag arms so they match the enum doc comments, the long-form --interactive=... arms, and GNU rm. test/js/bun/shell/commands/rm.test.ts adds a 35-case describe("interactive flags") block.
Security risks
None. This is error-message formatting in the shell builtin's option parser; no user input reaches new syscalls or allocations, and the unsupported_flag payload is a 'static byte literal.
Level of scrutiny
Low. The change is small and mechanical: it replaces a one-off error path with the exact idiom already used by four sibling builtins (cat.rs:58, touch.rs:56, mkdir.rs:62, cp.rs). I verified fail_parse calls set_wait_err() before write_failing_error, which branches on needs_io() the same way rm's own write_err_literal does, and that rm's on_io_writer_chunk non-Exec arm already returns Builtin::done(.., 1) for the ParseOpts { wait_write_err: true } state — so the async-stderr completion path is unchanged. The -i↔Always / -I↔Once swap only affects which flag name appears in the error (the sole consumer was previously an is not Never check), and now matches GNU semantics.
Other factors
Test coverage is thorough: 13 rejection combos × 2 stderr modes assert the exact message (with newline), exit 1, and that the operand file is left untouched; 8 cancellation combos assert the file is removed silently; the no-operand case pins usage-first ordering. Array interpolation in TestBuilder.command expands to separate argv words (per bunshell.test.ts:117), so multi-flag cases like ["-I", "-i"] are passed as distinct arguments. The PR description confirms USE_SYSTEM_BUN=1 fails the 26 rejection cases and the debug build passes the full rm.test.ts and bunshell.test.ts suites. The noted overlap with #39225 is a trivial two-line merge.
|
Related: #39225 makes |
…orted-option path The rm builtin's prompting stub rejected -i, -I and --interactive=once/always with the literal `rm: "-i" is not supported yet`: no trailing newline, and always "-i" whichever option was given. Record the option as it was spelled and reject it through Builtin::fail_parse, which prints the newline-terminated "unsupported option" message the other builtins use for options they do not implement. PromptBehaviour's Once/Always variants and removed_count had no reader (the only check was "not Never"), and the short-flag arms set them the opposite way round from their doc comments and the long forms (-i selected Once, -I selected Always). Collapse them into one variant carrying the option's spelling; the mode distinction goes back in when prompting is implemented.
8fd1a85 to
842f950
Compare
There was a problem hiding this comment.
LGTM — routes rm's "-i not supported" through the shared fail_parse/ParseError::Unsupported path and drops the dead Once/Always variants.
What was reviewed:
- Verified
Builtin::fail_parse+write_failing_errorproducerm: unsupported option, please open a GitHub issue -- <flag>\nand thaton_io_writer_chunkin theParseOptsstate still exits 1 after the async write, matching the oldwrite_err_literalpath. - Checked
unsupported_flag(&'static [u8])→ParseError::Unsupported(*const [u8])is sound: the storedflagis a'staticliteral, soopt()'s deref is always valid. - Confirmed the
-f/--interactive=nevercancellation still works because the rejection stays atRmParseFlag::Done; grepped for other users ofPromptBehaviour::Once|Always— none. - Tests cover every spelling, clustered short flags, last-wins ordering, both stderr modes (fd +
.quiet()), the cancelled cases, and the no-operand usage path.
Extended reasoning...
Overview
Two files: src/runtime/shell/builtin/rm.rs collapses PromptBehaviour::{Once{removed_count}, Always} into a single Prompt { flag: &'static [u8] } recording the option as spelled, and replaces the hand-written b"rm: \"-i\" is not supported yet" (no newline, always says -i) with a call to Builtin::fail_parse(…, ParseError::Unsupported(unsupported_flag(flag)), …). test/js/bun/shell/commands/rm.test.ts adds a 35-case describe("interactive flags") block.
Security risks
None. This is error-message formatting for an already-rejected option in the shell builtin; no new parsing of untrusted input, no filesystem operations added, and the raw *const [u8] handed to ParseError::Unsupported points at a 'static byte literal so the unsafe deref in ParseError::opt() stays within its documented contract.
Level of scrutiny
Low-to-medium. The change is mechanical: it deletes a bespoke error path in favour of the shared one already used by cat/cp/mkdir/touch, and removes enum variants that had no reader. I traced the async-stderr completion path (fail_parse → write_failing_error → .stderr.enqueue → on_io_writer_chunk non-Exec arm → Builtin::done(…, 1)) to confirm it behaves identically to the removed write_err_literal call, and confirmed the no-io / .quiet() branch goes straight to Builtin::done(…, 1) as the other builtins do. The rejection stays at RmParseFlag::Done so -i -f / -if still cancel, which the tests pin.
Other factors
Test coverage is thorough (each spelling × both stderr modes, clustered flags, last-prompting-option-wins, 8 cancellation orderings, no-operand usage) and the PR notes 26 of the 35 new cases fail on the release binary and pass on the debug build, with an earlier CI run green on rm.test.ts across all lanes. The two comment-cop threads were addressed in 758ffab (comments cut to one line each) and are resolved. The robobun note about #39225 / clearing opts.force in the prompting arms is an optional GNU-compat follow-up already tracked by that PR, not a defect here. No other references to the removed Once/Always variants exist in the tree.
Problem
rm -i filein the Bun shell fails withrm: "-i" is not supported yetand no trailing newline, so whatever is written to stderr next is glued onto the same line (rm: "-i" is not supported yetnext line). Every otherrmdiagnostic (usage,illegal option -- x,No such file or directory,may not be removed) ends in a newline.-iwhichever option was given (-I,--interactive=onceand--interactive=alwaysall print it), and it is a one-off string: the other builtins report options they do not implement throughBuiltin::fail_parse, as<cmd>: unsupported option, please open a GitHub issue -- <option as given>\n.RmParseFlag::Donearm ofRm::next(src/runtime/shell/builtin/rm.rs) writes that literal wheneveropts.prompt_behaviouris notNever. ThePromptBehaviourenum it checks hadOnce { removed_count }/Alwaysvariants that nothing else read, and the short-flag arms ofparse_flagset them the opposite way round from their doc comments and from the--interactive=once/--interactive=alwaysarms (-iselectedOnce,-IselectedAlways; GNU rm's-iprompts before every removal and-Iprompts once). Both the literal and the inverted arms date from the original implementation of the builtin.Fix
PromptBehaviouris nowNeverorPrompt { flag }, whereflagis the option as the user spelled it (-i,-I,--interactive=once,--interactive=always);-fand--interactive=neverstill setNever.Donearm rejects a remainingPromptthroughBuiltin::fail_parsewithParseError::Unsupported, sorm --interactive=once fileprintsrm: unsupported option, please open a GitHub issue -- --interactive=onceplus a newline, exits 1 and leaves the operand alone. The message text and the newline come from the shared formatter; rm's private literal is deleted.touch --datereports--date, an unknown short flag reports the letter), so the user sees the option they typed. Tracking the spelling is all the builtin needs today; the once/always distinction andremoved_counthad no consumer, which is how the inverted arms went unnoticed, so they are removed rather than flipped. Whoever implements prompting adds the modes back together with the code that reads them. The check stays after flag parsing (not insideparse_flag) so that a later-for--interactive=neverstill cancels an earlier prompting option, as in GNU rm;rm -if fileandrm -i -f filekeep removing the file.test/js/bun/shell/commands/rm.test.ts("interactive flags"): 13 rejected combinations (each spelling, clustered-ri/-rfI/-fi/-iI, and pairs where the option given last is the one named), each run with stderr as a real fd and with.quiet()(the two branches ofwrite_failing_error); 8 cancelled combinations that must remove the file silently;rm -ialone still prints usage. The 26 rejection cases fail on the current release (USE_SYSTEM_BUN=1: old text, no newline); the whole file andtest/js/bun/shell/bunshell.test.tspass on the debug build.opts.force = false;to each) and shell: name the rejected flag in the builtins' illegal option errors #39230 edits the_ =>arm directly below them, so whichever of these lands later gets a conflict confined to those lines; the resolution is to keep both sides (this PR'sPrompt { flag }assignments plus their added statements). shell(rm): accept --force as the long form of -f #39224's new--forcearm assignsPromptBehaviour::Never, which still exists, and merges cleanly.Background
rmis a builtin of the Bun shell on every platform (src/runtime/shell/builtin/rm.rs), so this message is Bun's own, not the systemrm's.ParseOptsstate, callingparse_flagon each; the first non-flag word returnsRmParseFlag::Done, which is where the collected options are validated before any operand is removed. "Last flag wins" for-fvs-ifalls out of each arm overwritingprompt_behaviour, which is why the rejection has to happen atDonerather than when-iis seen.Builtin::fail_parse(src/runtime/shell/Builtin.rs) is the shared failure path for option parsing: given aParseErrorit formats theillegal option/ usage /unsupported optionmessage, lets the builtin mark its state as waiting for the stderr write, then writes the message and exits 1.cat,cp,mkdirandtouchreport their unimplemented options through it;rmnow does too.$uses) or a capture buffer (written synchronously;.quiet()). The tests run every rejection through both.Earlier revision
The first push kept the
Once/Alwaysvariants, swapped the-i/-Iarms, and named the mode's short flag in the message (--interactive=oncewas reported as-I). Self-review pointed out that every other builtin names the option as typed and that the once/always model had no reader, so this revision records the spelling instead and drops the unused variants.no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/shell/commands/rm.test.ts