Skip to content

fix(builtins): honor --help argv order and rewrite pflag errors to GNU style#238

Merged
julesmcrt merged 20 commits into
mainfrom
df-followups
May 13, 2026
Merged

fix(builtins): honor --help argv order and rewrite pflag errors to GNU style#238
julesmcrt merged 20 commits into
mainfrom
df-followups

Conversation

@julesmcrt

Copy link
Copy Markdown
Collaborator

Summary

Two follow-ups from PR #205 (df builtin) review feedback:

  1. cmd --help --bogus now prints help and exits 0, matching GNU coreutils' left-to-right --help short-circuit. Previously pflag scanned the whole argv and failed on --bogus first. Implemented in builtins/builtins.go via a pre-parse args trim, scoped to commands that register a help flag.

  2. pflag flag-error wording now matches GNU getopt byte-for-byte:

    • unknown flag: --foounrecognized option '--foo'
    • unknown shorthand flag: 'X' in -...invalid option -- 'X'
    • flag does not allow an argumentoption '--foo' doesn't allow an argument
    • flag needs an argument: --foooption '--foo' requires an argument
    • Try 'cmd --help' for more information. footer appended when the command exposes --help.

Several df scenarios that previously carried skip_assert_against_bash solely because of wording divergence now match GNU byte-for-byte and have been de-skipped. Sibling scenarios and Go tests for ping, pwd, ip, strings, uniq, sed, and du were updated to the new wording.

Refs:

Test plan

  • `go test ./...` passes locally
  • `RSHELL_BASH_TEST=1 go test ./tests/ -run TestShellScenariosAgainstBash` passes (all de-skipped `df` scenarios match GNU byte-for-byte)
  • New unit test `TestRewritePflagError` covers each translation pattern + a pass-through case
  • New scenarios lock in the `--help` short-circuit behavior:
    • `tests/scenarios/cmd/df/basic/help_short_circuits_before_unknown_flag.yaml`
    • `tests/scenarios/cmd/df/errors/unknown_flag_before_help.yaml` (verifies left-to-right semantics still error on a bad flag positioned before `--help`)
  • `make fmt` clean

🤖 Generated with Claude Code

…U style

Two follow-ups from PR #205 (df builtin) review:

1. `cmd --help --bogus` now prints help and exits 0, matching GNU
   coreutils' left-to-right `--help` short-circuit. Previously pflag
   scanned the whole argv and failed on `--bogus` first. Implemented
   in `builtins/builtins.go` via a pre-parse args trim, scoped to
   commands that register a `help` flag.

2. pflag's default flag-error wording (`unknown flag: --foo`,
   `unknown shorthand flag: 'X' in -...`, `flag does not allow an
   argument`, `flag needs an argument: --foo`) is now translated to
   GNU's `unrecognized option '--foo'`, `invalid option -- 'X'`,
   `option '--foo' doesn't allow an argument`, and `option '--foo'
   requires an argument`. A `Try 'cmd --help' for more information.`
   footer is appended when the command exposes `--help`.

Several df scenarios that previously carried `skip_assert_against_bash`
solely because of wording divergence now match GNU byte-for-byte and
have been de-skipped. Updated all sibling scenarios and Go tests
(ping, pwd, ip, strings, uniq, sed, du) that asserted on the old
pflag wording.

Refs:
- #205 (comment)
- #205 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25059712c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
Codex review caught that `df --no-such=value` emitted
`df: unrecognized option '--no-such'` while GNU emits the full token
`'--no-such=value'`. pflag strips the `=value` suffix from its error
string, so `rewritePflagError` now takes the parsed argv and scans it
to recover the original token.

Added a unit-test case (--no-such=value, --no-such=, and `--` boundary)
plus a scenario test that asserts byte-for-byte match against GNU.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c85098992

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
Codex caught a second wording divergence: pflag emits two different
payloads for a missing argument depending on flag form:

  long form:  `flag needs an argument: --foo`
  short form: `flag needs an argument: 'X' in -Y`

Previously the rewriter inlined the payload into a single template,
producing `option ''n' in -n' requires an argument` for `head -n`.
GNU uses two separate templates:

  long:  `option '--foo' requires an argument`
  short: `option requires an argument -- 'X'`

`rewritePflagError` now branches on the payload shape via a small
`shortMissingArg` helper. Added unit-test cases for the single
shorthand and the combined-shorthand form (`-an`) plus scenario tests
under `tests/scenarios/cmd/head/errors/missing_arg_{short,long}.yaml`
that assert against GNU.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb77a039a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
julesmcrt and others added 2 commits May 7, 2026 16:48
Codex flagged that the previous trim "args at first --help" was too
aggressive: `head -n nope --help --bogus` would discard `--bogus`,
let pflag parse `-n nope --help` cleanly, then short-circuit on
--help and exit 0 — silently swallowing both the unknown later flag
and the invalid earlier value. GNU instead surfaces the earlier
validation error.

Restricted the trim to the case where `--help` is the very first
argument. This still covers the cases the original reviewer cited
(`df --help --sync`, `df --help --bad`) and every existing test, but
keeps full pflag validation when `--help` appears after another
flag — preserving GNU's "fail on the leftmost bad option" semantics.

Added a regression scenario at
`tests/scenarios/cmd/head/errors/help_after_value_does_not_swallow_unknown_flag.yaml`
that locks in: `head -n nope --help --bogus` exits 1 (does not print
help). skip_assert_against_bash is set because GNU's failure reason
(invalid -n value) and rshell's (unknown --bogus) differ — both exit
1 and neither prints help, which is the property we care about. The
underlying value-validation gap is a pre-existing issue out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
GNU getopt parses `-h=true` as the no-arg shorthand `-h` followed by
`=` interpreted as another shorthand char (which isn't registered),
emitting `df: invalid option -- '='`. pflag instead treats `=` as the
value separator and routes the value through Set, which our no-arg
guards reject.

Detect the `-X=value` argv shape in the rewriter and substitute GNU's
wording when the descriptor's shorthand char matches X. Closes the
last wording-only divergence flagged in PR #205's follow-up audit
(`tests/scenarios/cmd/df/flags/rejected_h_equals_true.yaml`); skip
removed and stderr now matches GNU byte-for-byte.

Updated unit tests:
- new case: -h=true with descriptor `-h, --human-readable` → `'='`
- existing -k=true case (short-only descriptor) updated — GNU also
  emits `invalid option -- '='` for that, not `option '-k' doesn't
  allow an argument`

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f55983930b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
julesmcrt and others added 2 commits May 7, 2026 17:24
Codex flagged that GNU honors --help even after earlier valid options
— `head -q --help --bogus` and `df -h --help --bogus` print help and
exit 0. The previous "first-only" trim was too narrow.

The new `safeHelpTrimIndex` scans tokens before --help and trims only
when every preceding token is a registered no-argument flag (long form
or shorthand cluster). Tokens that could fail validation (value-takers,
=value forms, positionals, unknown flags, end-of-flags `--`) all
preclude the trim — keeping the earlier-failure semantics from the
prior round (`head -n nope --help --bogus` still errors instead of
silently swallowing the invalid value).

Added unit tests for `safeHelpTrimIndex` covering each token class and
a regression scenario at
`tests/scenarios/cmd/df/basic/help_short_circuits_after_no_arg_flag.yaml`
(`df -h --help --bogus` exits 0 with usage).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
CI's `Fuzz (pwd)` and `Fuzz (du)` jobs caught a panic in the new
`isNoArgFlagToken`: for a token like `-˞`, the loop calls
`fs.ShorthandLookup(string(a[i]))`. When `a[i]` is a UTF-8
continuation byte (≥ 0x80), `string(byte)` produces the 2-byte UTF-8
encoding of that byte interpreted as a rune (e.g. 0xCB → "Ë"), and
pflag panics with `can not look up shorthand which is more than one
ASCII character`. The interpreter recovers and reports `internal
error` from api.go's panic guard.

Guard the loop: any byte outside printable ASCII (≤ ' ' or > 0x7E)
disqualifies the token as a no-arg cluster — non-ASCII bytes can't
be valid shorthand chars anyway. Captured the failing input as a
permanent corpus seed (`builtins/pwd/testdata/fuzz/FuzzPwdArgs/9386e59311458487`)
and added a unit-test case to TestSafeHelpTrimIndex.

Verified locally: 30s fuzz on both pwd and du now passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18e763ab39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go Outdated
@julesmcrt
julesmcrt marked this pull request as ready for review May 7, 2026 15:59
Comment thread builtins/builtins.go Outdated
julesmcrt and others added 3 commits May 11, 2026 13:42
Closes the trade-off codex flagged on the prior round
([3202837999](#238 (comment))).
GNU coreutils process options left-to-right, so `head -n 5 --help
--bogus` prints help (valid -n value, --help short-circuits) while
`head -n nope --help --bogus` errors (invalid -n value surfaces
before --help is reached). The prior no-arg-only trim only matched
GNU for the no-arg-before-help case.

Three changes work together:

1. `builtins/builtins.go` — replaced the syntactic no-arg-only
   `safeHelpTrimIndex` with `trialHelpTrimIndex`, which trial-parses
   the prefix on a fresh FlagSet. If pflag accepts every preceding
   option, the suffix is safely discardable. This now permits trims
   after value-takers, but defers the safety guarantee to per-builtin
   value validation.

2. `builtins/head/head.go` — moved the -n/-c value validation to
   BEFORE the `--help` short-circuit. Switched the error wording
   from Go-style `%q` (double quotes) to single-quoted `'%s'` so
   the message matches GNU byte-for-byte
   (`head: invalid number of lines: 'nope'`).

3. `builtins/tail/tail.go` — removed the no-op validation from
   `modeFlag.Set` (it returned `errors.New("invalid count")` and
   pflag wrapped it as `invalid argument "X" for "-n, --lines"
   flag: invalid count`). Validation now lives upfront in the
   handler, before `--help`, with the same GNU-matching format as
   head. Both explicit -n and -c values are validated even when one
   later overrides the other, matching GNU.

Tests:
- `TestSafeHelpTrimIndex` replaced with `TestTrialHelpTrimIndex`,
  covering no-arg flags, value-takers (long, short, `=value`),
  positionals, `--`, unknown flags, and the non-ASCII fuzz case.
- Updated `tests/scenarios/cmd/head/errors/help_after_value_does_not_swallow_unknown_flag.yaml`
  to assert GNU's "invalid number of lines: 'nope'" wording and
  dropped its `skip_assert_against_bash` (now byte-for-byte with bash).
- Updated `tests/scenarios/cmd/tail/errors/minint64_suffix.yaml`
  substring from "invalid count" to "invalid number of lines".
- Added four scenarios locking in the four target cases:
  `head/basic/help_short_circuits_after_value_taker.yaml`,
  `head/errors/invalid_lines_value.yaml`,
  `tail/basic/help_short_circuits_after_value_taker.yaml`,
  `tail/errors/invalid_lines_value.yaml`.

Verified end-to-end against bash with `RSHELL_BASH_TEST=1` plus
15s fuzz on pwd and du.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pure refactor — no behaviour change. Moves the error-rewriting and
trial-parse helpers out of builtins.go into a dedicated package so
the file is back to focusing on builtin registration.

What moved:
  - rewritePflagError  → flagparser.RewriteError
  - trialHelpTrimIndex → flagparser.TrialHelpTrimIndex
  - extractFlagDescriptor, longFlagName, recoverLongFlagToken,
    shortFlagEqualsValueIn, shortFlagFromDescriptor, shortMissingArg
    (all unexported helpers)

builtins.go now imports flagparser and delegates. The trial-parse
helper's signature changed from `func(*FlagSet) HandlerFunc` to
`func(*FlagSet)` so the package doesn't need to know about
builtins.HandlerFunc — the caller wraps factory in a one-line
closure that discards the returned handler.

Tests moved with the code (builtins/internal/flagparser/flagparser_test.go).
analysis/symbols_internal.go gained a flagparser entry plus
io.Discard, strings.CutPrefix, strings.HasSuffix, and the three
pflag symbols used by the trial parser.

Addresses [3204000991](#238 (comment))
option #2. Keeps option #1 (own parser) as a future possibility:
if we ever swap pflag for a hand-written GNU-getopt implementation,
the swap is local to this package.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Alignment-only diff — gofmt rebalanced the comment columns after the
previous commit added new entries for the flagparser package. CI's
gofmt check picked this up locally; running gofmt -w resolved it.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt julesmcrt added the verified/analysis Human-reviewed static analysis changes label May 11, 2026
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8873b509aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/internal/flagparser/flagparser.go
Codex caught two bugs in TrialHelpTrimIndex's success condition. The
trial parse could succeed even when pflag consumed `--help` in a way
that did NOT set the help flag:

1. As the value of a preceding value-taker. `grep -e --help --bogus`
   makes `--help` the pattern for -e; the trial parsed fine but help
   was never requested. The old code trimmed `--bogus` away and grep
   then ran with pattern `--help` instead of reporting the bad flag.

2. As a positional, for FlagSets that called `SetInterspersed(false)`
   (xargs, tr, read). After the first positional, pflag stops parsing
   flags. `xargs echo --help --bogus` left both `--help` and `--bogus`
   as positional args for the child command, but our trim discarded
   `--bogus`.

Fix: after a successful trial parse, also require
`trial.Lookup("help").Changed == true`. If pflag didn't actually call
Set on the help flag, we leave args alone and let the real parse
report any later error.

Tests:
- New cases in TestTrialHelpTrimIndex for both shapes (`-n --help`,
  `-e --help`, plus a dedicated SetInterspersed(false) sub-test for
  the xargs/tr/read style).
- Verified end-to-end: `grep -e --help --bogus` now emits
  `grep: unrecognized option '--bogus'` (matching GNU);
  `xargs echo --help --bogus` correctly passes `--help --bogus` to
  echo.
- All previously-fixed cases (head -q/-n/-h/--help-first variants,
  df invocations) still match GNU.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e6601a666

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go
Codex flagged that `sort --check=bogus --help` printed help and exited
0 instead of erroring on the invalid value. Same class of bug as
head/tail had: sort defers `--check` value validation, `-cC` conflict
detection, `-t`/`-k` parsing, and `-d`/`-n`/`-h` global-flag conflicts
to its handler — but the handler checked `*help` first, so any
`--help` later in argv silently masked these earlier validations.
GNU sort processes options left-to-right; an invalid earlier option
exits non-zero even when `--help` follows.

Moved all GNU-style validations to BEFORE the `--help` short-circuit
in builtins/sort/sort.go. The rshell-specific rejections (`--output`,
`--temporary-directory`, `--compress-program`) stay AFTER `--help`
because GNU accepts those flags and prints help when `--help` is also
set — we mirror that ordering and only reject when no help short-
circuit fires.

Verified end-to-end:
- `sort --check=bogus --help` → exit 2, "invalid argument" (was: help, 0)
- `sort -c -C --help` → exit 2, "'-cC' are incompatible" (was: help, 0)
- `sort -t '' --help` → exit 2, "empty tab" (was: help, 0)
- `sort -t XX --help` → exit 2, "multi-character tab" (was: help, 0)
- `sort -k bad --help` → exit 2, "invalid key option" (was: help, 0)
- `sort --output=/tmp/x --help` → help, 0 (still, since --output is
  rshell-rejected after --help — matches GNU which accepts --output)
- `sort --help` → help, 0 (unchanged)

Added two regression scenarios:
- `tests/scenarios/cmd/sort/errors/invalid_check_with_help.yaml`
- `tests/scenarios/cmd/sort/errors/check_conflict_with_help.yaml`

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 918125a0d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/sort/sort.go Outdated
Codex caught that GNU sort treats the global ordering-flag conflicts
as deferred validation: `sort -d -n --help`, `sort -d -h --help`, and
`sort -h -n --help` all print help and exit 0. My previous reorder
was too aggressive — it moved EVERY validation before --help,
including these.

Moved the -dn/-dh/-hn global-conflict checks back to AFTER --help.
The genuinely-parse-time validations (--check value, -c+-C, -t length,
-k parsing) stay BEFORE --help, matching GNU's left-to-right rule.

Verified GNU/rshell match on all cases:

| invocation                          | GNU/rshell |
|-------------------------------------|------------|
| sort --check=bogus --help          | error, 2   |
| sort -c -C --help                  | error, 2   |
| sort -t XX --help                  | error, 2   |
| sort -k bad --help                 | error, 2   |
| sort -d -n --help                  | help, 0    |
| sort -d -h --help                  | help, 0    |
| sort -h -n --help                  | help, 0    |
| sort -d --help -n                  | help, 0    |
| sort --help                        | help, 0    |

Added a regression scenario at
`tests/scenarios/cmd/sort/basic/help_short_circuits_over_global_conflicts.yaml`
to lock in the deferred-validation behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 642d0c6697

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/internal/flagparser/flagparser.go Outdated
Codex caught that the previous code scanned the entire argv for any
`-X=value` token whose shorthand matched the failing flag, even when
the actual failing token was the long form earlier in argv. Example
regression: `df --human-readable=true -h=x` should emit
`option '--human-readable' doesn't allow an argument` (GNU), but
rshell emitted `invalid option -- '='` because the later `-h=x`
matched the shorthand scan.

pflag stops at the FIRST bad option in argv, so the rewriter now
walks args left-to-right and uses whichever form (long `--LONG=val`
or short `-X=val`) appears first. Removed the now-unused
`shortFlagEqualsValueIn` helper; the lookup is inlined into the
no-arg branch of `RewriteError` so it can return immediately on
either match.

Two new unit-test cases lock in the left-to-right rule:
- long --foo=val before -X=val → long-form error wins
- short -X=val before --LONG=val → short-form error wins (the
  reverse, even though no real builtin has flags ordered that way).

Verified end-to-end:
- df --human-readable=true -h=x → `option '--human-readable' doesn't allow an argument` (matches GNU; was: `invalid option -- '='`)
- df -h=x → `invalid option -- '='` (unchanged)
- df --human-readable=true → `option '--human-readable' doesn't allow an argument` (unchanged)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

func (f *modeFlag) Set(s string) error {
f.val = s
*f.seq++

P2 Badge Reject earlier invalid repeated head counts

When the same mode flag is repeated, this setter overwrites the previous value and the handler only validates the final one. As a result, head -n nope -n 1 now succeeds and reads stdin, while GNU head rejects the first invalid -n value before considering later options; tracking the first invalid value (as sort does for --check) would preserve the intended left-to-right validation without losing the --help short-circuit.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/tail/tail.go
Codex caught that `tail -n nope -n 1` (and the same for head) exited 0
and printed the last line, masking the earlier invalid value. The
deferred-validation approach lost track of the FIRST bad input because
modeFlag.Set always overwrote val and the handler only validated the
final value.

Added `invalid` / `hasInvalid` fields to both head's and tail's
modeFlag. Set captures the first value rejected by parseCount but
keeps accepting later writes (so val/offsetSeen/pos still track the
last call). The bound handlers in both builtins now check
`hasInvalid` and report the captured `invalid` value, matching GNU
which validates each option as it sees it.

Verified end-to-end:
- `tail -n nope -n 1` → `tail: invalid number of lines: 'nope'` (was: prints last line, exit 0)
- `head -n nope -n 1` → `head: invalid number of lines: 'nope'` (was: prints first line, exit 0)
- `head -c bad -n 5`  → `head: invalid number of bytes: 'bad'`
- `head -n 1` / `tail -n 1` → still work (no regression)

Added two regression scenarios:
- `tests/scenarios/cmd/head/errors/invalid_lines_overridden.yaml`
- `tests/scenarios/cmd/tail/errors/invalid_lines_overridden.yaml`

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ab6703b14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/builtins.go
julesmcrt and others added 4 commits May 13, 2026 13:37
Codex P1 flagged that the shared trial-parse trim in builtins/builtins.go
silently masks handler-time validation errors for builtins that check
`*help` before their value-taker checks. Three builtins were
specifically called out:

- `cut -d xy --help --bogus` printed help instead of erroring on the
  multi-byte delimiter.
- `uniq -f nope --help --bogus` printed help instead of erroring on
  the invalid skip-fields count.
- `strings -n 0 --help --bogus` printed help instead of erroring on
  the out-of-range minimum length.

Same fix pattern as head/tail/sort: move the value validation BEFORE
the `if *help` short-circuit. For cut, the delimiter check is now
guarded by `fs.Changed("delimiter")` so `cut --help` (with the default
"\t") still short-circuits cleanly. For uniq and strings, default
flag values pass the existing checks unchanged.

Verified end-to-end:
- `cut -d xy --help --bogus` → `cut: the delimiter must be a single character`, exit 1
- `uniq -f nope --help --bogus` → `uniq: nope: invalid number of fields to skip`, exit 1
- `strings -n 0 --help --bogus` → `strings: invalid minimum string length 0`, exit 1
- `cut --help` / `uniq --help` / `strings --help` → help, 0 (no regression)
- `cut -d , --help --bogus` / `uniq -f 1 --help --bogus` / `strings -n 4 --help --bogus` → help, 0 (value valid)

Added regression scenarios at
`tests/scenarios/cmd/{cut,uniq,strings}/errors/invalid_*_with_help.yaml`.

(Docker is currently unavailable so the bash-comparison test was not
re-run for these specific scenarios; the Go-level scenarios all pass.
Prior bash-comparison runs were green on the rest of the suite.)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PR #237 (cd builtin) merged to main after this PR branched and
brought tests that assert pflag's pre-rewrite wording: `unknown flag`
/ `unknown shorthand flag`. After merging main into this branch the
shared rewriter in builtins/internal/flagparser/ translates those to
GNU's `unrecognized option` / `invalid option -- 'X'`, plus the
`Try 'cmd --help' for more information.` footer.

Updated:
- builtins/cd/cd_test.go: TestCdUnknownLongFlag substring assertion
  switched to "cd: unrecognized option".
- tests/scenarios/cmd/cd/errors/unknown_flag_short.yaml: stderr now
  matches "cd: invalid option -- 'x'\nTry 'cd --help' for more
  information.".
- tests/scenarios/cmd/cd/errors/unknown_flag_long.yaml: stderr now
  matches "cd: unrecognized option '--no-such-flag'\nTry 'cd --help'
  for more information.".

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
CI's bash-comparison flagged that GNU cut appends `Try 'cut --help'
for more information.` after the "delimiter must be a single
character" error; rshell only emitted the error line. Updated cut's
handler to emit the footer too and updated the regression scenario
`tests/scenarios/cmd/cut/errors/invalid_delimiter_with_help.yaml` to
match. Other cut error scenarios use `stderr_contains` substring
match and are unaffected by the extra line.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e91315756

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread builtins/head/head.go Outdated
Comment thread builtins/tail/tail.go Outdated
Codex caught that head/tail's mode-flag validation always reported
linesFlag's error before bytesFlag's, regardless of argv order. For
`head -c bad -n nope` (and tail), GNU reports the byte error since
-c came first; rshell reported the line error because the handler
checked linesFlag first.

Added an `invalidPos` field to each builtin's modeFlag that captures
the shared seq counter value WHEN hasInvalid is first set. Added a
small `reportLinesInvalidFirst` helper in each handler that picks the
mode flag with the smaller invalidPos when both are invalid, so the
emitted error matches whichever bad value GNU's leftmost-bad-option
rule would surface.

Verified end-to-end:
- `head -c bad -n nope` → `head: invalid number of bytes: 'bad'` (was: lines)
- `head -n nope -c bad` → `head: invalid number of lines: 'nope'` (unchanged)
- `tail -c bad -n nope` → `tail: invalid number of bytes: 'bad'` (was: lines)
- `tail -n nope -c bad` → `tail: invalid number of lines: 'nope'` (unchanged)
- single-flag cases (`head -n nope`, `head -c bad`, `head -n 5 -c bad`,
  prior `-n nope -n 1` regression) → all match GNU.

Added regression scenarios at
`tests/scenarios/cmd/{head,tail}/errors/invalid_bytes_before_lines.yaml`.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@julesmcrt
julesmcrt added this pull request to the merge queue May 13, 2026
Merged via the queue into main with commit 77af742 May 13, 2026
40 checks passed
@julesmcrt
julesmcrt deleted the df-followups branch May 13, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

verified/analysis Human-reviewed static analysis changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants