shell(touch): report --date=, --reference= and --time= as unsupported options - #39227
shell(touch): report --date=, --reference= and --time= as unsupported options#39227robobun wants to merge 2 commits into
Conversation
The --time arm of touch's long option table reported the option as --reference=FILE, copied from the arm above it, so `touch --time` pointed users at an option they had not passed. Report it as --time, like the other arms report the option that was given. Adds a table test pinning the message for every unsupported touch option.
|
Updated 6:52 PM PT - Aug 15th, 2026
✅ @robobun, your commit b10a1b4c34d811d7cb7a65c120c0721852cbc3d5 passed in 🧪 To try this PR locally: bunx bun-pr 39227That installs a local version of the PR into your bun-39227 --bun |
|
Warning Review limit reached
Next review available in: 22 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 |
|
Status: reproduced and fixed.
|
There was a problem hiding this comment.
LGTM — small, well-scoped error-message fix with a table test covering every arm.
What was reviewed:
parse_longnow splits on=before matching value-taking options;split_once_charreturnsNonewhen no=is present so bare--datestill matches.--no-createis checked before the split, so--no-create=1still falls through toillegal option— pinned by the new test.- Test follows sibling
test/js/bun/shell/commands/*.test.tsconventions (TestBuilder,describe.concurrent,ensureTempDir, exit-code + stderr assertions).
Extended reasoning...
Overview
This PR touches src/runtime/shell/builtin/touch.rs (the parse_long implementation of the FlagParser trait) and adds test/js/bun/shell/commands/touch.test.ts. The change makes touch --date=x, --reference=x and --time=x report as unsupported options (matching the existing behavior for the space-separated --date x spelling) instead of falling through to short-flag parsing and reporting illegal option -- date=x. It also carries forward the stacked #39219 fix where --time previously reported --reference=FILE due to a copy-paste. Only stderr text changes; exit code was already 1.
Security risks
None. This is pure error-message classification in a shell builtin's flag parser. No user input reaches a syscall differently than before — every path through parse_long still returns Unsupported or None, both of which cause the command to exit 1 before any filesystem operation. split_once_char is a bounds-safe in-tree helper over &[u8].
Level of scrutiny
Low. The diff is ~10 lines of Rust changing which literal string an already-failing command prints, plus a new test file. No control-flow, memory, or lifetime changes. The --no-create exact-match is deliberately kept before the split (it takes no value in GNU touch), and the test pins that --no-create=1 remains an illegal option. I confirmed bun_core::strings::split_once_char exists and returns Option<(&[u8], &[u8])>, so .map_or(flag, ...) correctly preserves the no-= case. I also confirmed parse_one_flag in interpreter.rs only calls parse_long for args with the -- prefix, so the split is always operating on a well-formed long option.
Other factors
The test file mirrors the structure of sibling files in test/js/bun/shell/commands/ (TestBuilder, .ensureTempDir(), .exitCode(), .doesNotExist(), describe.concurrent). The table covers every long and short option arm plus the =VALUE variants and the empty-value --date= edge, and a negative case for --no-create=1. The PR description explains the stacking on #39219 and verifies the test fails on released Bun and passes on the branch. No outstanding reviewer comments.
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate of #39219: this PR is stacked on it. The first commit here is #39219's commit (the |
|
Heads up: the |
|
Heads up: #39223 has been reworked to handle |
d3fd1ea to
9fa2108
Compare
…pported options they spell
touch's long option table matched --date, --reference and --time only as
bare words. The --option=VALUE spelling of the same options fell through
to short option parsing and was reported as an illegal option
("illegal option -- date=x"), as if the option did not exist. Split the
argument at its first '=' and match the name, so both spellings are
reported as the same unsupported option. --no-create takes no value, so
it only matches without one and --no-create=x stays an illegal option.
9fa2108 to
b10a1b4
Compare
There was a problem hiding this comment.
LGTM — small, correct error-message fix with a table test that pins each case.
What was reviewed:
parse_longnow splits on the first=viabun_core::strings::split_once_charand matches the name;--no-create'svalue.is_none()guard keeps--no-create=1falling through to illegal — matches GNU touch.- The stacked
--timearm fix (was reporting--reference=FILE) is included. - Test uses the shell
TestBuilderharness withensureTempDir/doesNotExist/exit-code checks;.stderr(callback)is a supported overload; the--no-create=1case pins only theillegal option --prefix so it stays green regardless of #39230's ordering.
Extended reasoning...
Overview
Two files: src/runtime/shell/builtin/touch.rs (the parse_long body only) and a new test/js/bun/shell/commands/touch.test.ts. The Rust change splits the incoming long-option argument at its first = into (name, value) and matches on name, so --date=x, --reference=x and --time=x are recognized as the same unsupported options as their bare spellings instead of falling through to the short-flag path and being reported as illegal. The --time arm's copy-paste literal (--reference=FILE) is fixed to --time (the stacked #39219 commit). --no-create gains a value.is_none() match guard so --no-create=VALUE continues to fall through as illegal, matching GNU touch.
Security risks
None. This affects only which of two error messages is printed on an already-failing path (exit code was already 1). No new syscalls, no memory management, no user-controlled data reaching allocation or filesystem paths.
Level of scrutiny
Low. The change is a ~15-line rewrite of a static match table in an error-message path of a shell builtin. I verified bun_core::strings::split_once_char returns (before, after) at the first delimiter or None, so --date (no =) still matches, --date= yields ("--date", "") and matches, and --no-create=1 yields Some so the guard rejects it and it falls through to _ => None → short-flag illegal. No other callers of parse_long are affected (this is touch's own FlagParser impl).
Other factors
The new test file follows the existing test/js/bun/shell/commands/*.test.ts conventions (createTestBuilder, ensureTempDir, .quiet(), .exitCode(1), .doesNotExist("file")), covers all 15 known-but-unsupported spellings plus the negative --no-create=1 case, and asserts exact stderr for each. The .stderr(callback) overload used for the illegal-option case exists in test_builder.ts. The comment-cop bot's feedback about a paragraph-long comment was addressed in b10a1b4 — the current code has a one-line comment and expresses the takes-no-value rule as a match guard. The remaining timeline notes are cross-PR merge-order coordination (#39223, #39230), not correctness issues with this diff, and the test was already loosened to survive #39230. No CODEOWNERS entry for shell paths.
Problem
touch --date=x f,touch --reference=f gandtouch --time=atime ffail withtouch: illegal option -- date=x(reference=f,time=atime), as if the option did not exist. The bare spellings of the same options (touch --date x f) already fail withtouch: unsupported option, please open a GitHub issue -- --date.parse_longinsrc/runtime/shell/builtin/touch.rscompares the whole argument against--date,--referenceand--time, so--date=xmatches nothing,parse_longreturnsNone, and the shared parser (parse_one_flaginsrc/runtime/shell/interpreter.rs) falls through to short option parsing of-date=x, whose first byte-is reported as an illegal option.Fix
parse_longsplits the argument at its first=into a name and an optional value and matches the name, so--date xand--date=xare both reported as the unsupported--date(likewise--reference=FILEand--time). The reported names are unchanged.--no-createtakes no value, so its arm only matches when there is none:--no-create=xis not a spelling of it and stays an illegal option (GNU touch rejects it too).--option VALUEand--option=VALUEare the two spellings getopt_long accepts for an option that takes a value, and these three take one in GNU touch, so both spellings name the same option and should get the same answer. The two messages mean different things to the user: "unsupported" says the option is known and not implemented (and asks for an issue), "illegal" says it does not exist.=VALUEsplit is done in touch's own table, the same way shell: report --name=value spellings of unsupported builtin options as unsupported #39223 does it formkdir --mode=; these are the only twoFlagParserimplementations with long options (catandcphave none). Doing the split once inparse_one_flaginstead would be the tidier end state, butparse_one_flag, theFlagParsertrait and mkdir'sparse_longare each being edited by open PRs right now (shell: treat a lone-as an operand in the builtin option parsers #39214, shell: accept--end-of-options delimiter in builtins #33995, shell: name the rejected flag in the builtins' illegal option errors #39230, shell(mkdir): accept --verbose instead of the misspelling --vebose #39221, shell: report --name=value spellings of unsupported builtin options as unsupported #39223), so this PR stays inside touch.rs; a later consolidation deletes this hunk and keeps the test rows.--timearm reports and adds the table test this PR extends. If shell(touch): name --time in its unsupported option message #39219 lands first this rebases to just the second commit.test/js/bun/shell/commands/touch.test.ts: the table gains--date=@0,--date=,--reference=otherand--time=atime, plus a case checking--no-create=1is still classified as an illegal option (only the classification is pinned: shell: name the rejected flag in the builtins' illegal option errors #39230 changes which bytes that message quotes, and this case passes either way). On the releasedbun(1.4.0) the four=VALUEcases fail (and shell(touch): name --time in its unsupported option message #39219's--timecase); on this branch all 16 cases pass.test/js/bun/shell/exec.test.ts(pinstouch --helpas an illegal option, unaffected because--helpis not in the table) and theexit codesblock oftest/js/bun/shell/bunshell.test.tsagainst the debug build; both pass.Background
touch,mkdir,catandcpparse their flags through theFlagParsertrait insrc/runtime/shell/interpreter.rs. For an argument starting with--,parse_one_flagfirst offers the whole argument to the builtin'sparse_long; if that returnsNoneit treats the bytes after the first-as a cluster of short flags, which is where theillegal option -- date=xtext comes from.ParseFlagResult::Unsupported(name)for an option it recognizes but does not implement;Builtin::fail_parseformatsname(a literal chosen by the builtin, not the argv entry) into theunsupported option, please open a GitHub issue -- <name>line and exits 1.IllegalOptionproduces theillegal option -- <bytes>line, also exit 1.touchis a shell builtin on every platform (onlycatandcpdefer to the system binary on POSIX), so the test needs no platform gating.