Skip to content

Show section subcommand help when a global flag precedes the section#15

Merged
evanphx merged 1 commit into
mainfrom
mir-1309-section-help-interspersed-flag
Jul 9, 2026
Merged

Show section subcommand help when a global flag precedes the section#15
evanphx merged 1 commit into
mainfrom
mir-1309-section-help-interspersed-flag

Conversation

@evanphx

@evanphx evanphx commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

A value-taking global flag placed before a section/namespace command broke
help rendering. In the runtime (miren):

miren -C prod auth help   → printed TOP-LEVEL help instead of auth's subcommands
miren -C prod auth        → same class of failure

findCommandWithInterspersedFlags correctly guessed that -C consumed
prod, leaving auth help → matched the auth section. But it then
validated the guess by requiring every pre-command flag to exist in that
command's flagset. Section commands have empty flagsets, so -C was unknown,
the match was rejected, the resolver returned nil, and the dispatcher fell
back to showHelp() (top-level help).

This affected every section for every global flag; regular commands were
immune only because they happen to embed those flags.

Fix

Only reject a match when a known flag was wrongly assumed to take a value
(the hasValue && IsBool() case — a genuine bad guess). Drop the
"unknown flag ⇒ reject the whole command path" rule. Truly-bogus flags are
still reported by fs.Parse afterward as unknown flag: X, which is clearer
than the old misleading unknown command: ....

Removed the now-unused isHelpFlag helper.

Tests

  • New TestDispatcherInterspersedValueFlagBeforeSection covering
    -C prod auth [help | --help | -h] and the bare -C prod auth form.
  • Full suite: 467 passing (was 466).

Consumed by mirendev/runtime for MIR-1309 (paired with a runtime change that
sets AllowUnknownFlags(true) on section flagsets for the no-help form).

The interspersed-flag resolver validated its value-consumption guesses by
requiring every flag placed before a command to exist in that command's
flagset. That rejected legitimate matches when a global flag (e.g.
-C/--cluster) preceded a command whose own flagset doesn't declare it —
most visibly section/namespace commands with empty flagsets. The resolver
returned nil and the dispatcher fell back to top-level help.

Only reject a match when a *known* flag was wrongly assumed to take a value
(bool flags don't). Genuinely-unknown flags are still reported by fs.Parse
as "unknown flag: X" after resolution, which is clearer than the previous
misleading "unknown command: ...".

Fixes miren help for `<global-flag> <value> <section>` invocations.
@evanphx
evanphx requested a review from a team as a code owner July 9, 2026 21:15

@miren-code-agent miren-code-agent 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.

🍪 biscuit here — I spotted this PR and popped a review in the oven. Sit tight; I'll be back with a verdict shortly. 👨‍🍳

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The dispatcher’s interspersed-flag validation now allows unknown pre-command flags to proceed to later parsing, while rejecting only guessed values for known boolean flags. The obsolete help-flag helper was removed. A regression test covers value-taking global flags before an auth section and verifies that section help and subcommand output are rendered.


Comment @coderabbitai help to get the list of available commands.

@miren-code-agent miren-code-agent 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.

🍪 biscuit: ✅ ready to merge — auto-review, non-blocking

Review

This PR fixes a real regression in findCommandWithInterspersedFlags: when a global value-taking flag (e.g. -C prod) appeared before a section-style command that has an empty flagset, the old validation loop would see that -C is not in the command's flagset and set valid = false, causing the resolver to fall through and show top-level help instead of the section's sub-commands.

The fix

The old validation code had two independent "invalidity" conditions:

  1. Known bool flag wrongly assumed to take a value — kept.
  2. Pre-command flag not found in the command's flagsetremoved.

Removing condition 2 is the right call. The comment in the new code explains the reasoning well: unknown pre-command flags are exactly the global-flags-before-subcommand idiom, and if a flag is genuinely bogus fs.Parse will report it correctly as "unknown flag: X" rather than silently swallowing it or emitting a misleading "unknown command" error.

The cleanup of isHelpFlag is a nice side-effect — that helper became dead code once the only caller was removed.

What I checked

  • The diff is focused and purposeful: only the validation guard changes; no unrelated movement.
  • I traced the execution path for the regression case ["-C", "prod", "auth"]:
    1. -C and prod are collected as skippedItems with hasValue: true.
    2. auth is the command part; lastCommandIndex lands on the auth token.
    3. The old code looked up -C in auth's empty flagset, found nothing, marked valid = false.
    4. With this PR, the unknown-flag check is gone; only the bool-takes-value check remains, and since auth's flagset has no flags at all, VisitAll simply never fires — valid stays true.
    5. The match succeeds and auth's handler fires, returning ErrShowHelp, which the dispatcher converts to the correct section help.
  • The AllowUnknownFlags(true) on auth's flagset in the test is also load-bearing: without it, fs.Parse would reject -C as an unknown flag when the pre-command args are included in fullArgs. This is consistent with how section commands are constructed in practice.
  • The four test cases cover the interesting variants: auth alone (triggers ErrShowHelp), auth --help, auth -h, and auth help. That's solid coverage of the help-dispatch paths.
  • One small observation: auth help (["-C", "prod", "auth", "help"]) works because "help" in the remaining args is caught by the hasHelp scan loop (line 255) — the i == 0 guard doesn't apply since "help" isn't the first arg. That path is correct.
  • Existing tests aren't broken by the relaxation: commands with known bool flags wrongly assigned a value would still be rejected (condition 1 is intact), and the full suite of prior tests covers many valid/invalid flag combinations.

One mild concern (not a blocker)

The broadened acceptance means that if a user genuinely misplaces a flag meant for a different command before an unrelated subcommand, the dispatcher will now match that subcommand and pass the spurious flag into fullArgs. fs.Parse will then report "unknown flag: X". That's actually better UX than the old "unknown command" error, and the existing allowUnknownFlags path already handles global-flag consumers gracefully. So this is an improvement, not a regression — but worth being aware of if you ever want to add flag-provenance diagnostics.

The change is correct, the test is specific and well-structured, and the comment in the code does a good job explaining the reasoning. Ready to merge.


🍪 full review note

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dispatcher_test.go`:
- Around line 1248-1252: The test fixture for newDispatcher is missing a real
registration for the value-taking -C flag, so it never exercises the intended
known-global-flag-before-section path. Update the fixture to declare -C on the
appropriate global or sibling command while keeping the auth section’s flagset
without it, so Dispatcher candidate validation and Run behavior are tested
against a flag that is known somewhere in the command tree but unknown to auth.
- Around line 1272-1285: The stdout capture in the test helper inside run is
brittle because io.Copy happens only after newDispatcher().Execute returns,
which can deadlock if help output fills the pipe and can also leave os.Stdout
altered on panic. Update this helper to use the project’s existing
output-capture test utility if available, or otherwise read from the pipe
concurrently while Execute runs and restore os.Stdout with defer so the original
stream is always reset even on failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a67d341-ad75-45cd-8749-6aa5b60335b4

📥 Commits

Reviewing files that changed from the base of the PR and between 0ca7adc and f69b73c.

📒 Files selected for processing (2)
  • dispatcher.go
  • dispatcher_test.go

Comment thread dispatcher_test.go
Comment on lines +1248 to +1252
// newDispatcher builds a dispatcher with a section-style "auth" command whose
// flagset declares no flags and whose Run returns ErrShowHelp, plus two
// sub-commands. A real value-taking flag "-C" is only declared on a sibling
// command, so it is unknown to the "auth" section's flagset.
newDispatcher := func() *Dispatcher {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Declare -C as a known value-taking flag in the fixture.

The fixture never registers -C; it is unknown to every flagset. Consequently, these cases do not verify the regression described by the test name and PR objective—handling a known value-taking global flag before a section. Add the real global/sibling flag registration and retain auth without that flag so the candidate-validation path is exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dispatcher_test.go` around lines 1248 - 1252, The test fixture for
newDispatcher is missing a real registration for the value-taking -C flag, so it
never exercises the intended known-global-flag-before-section path. Update the
fixture to declare -C on the appropriate global or sibling command while keeping
the auth section’s flagset without it, so Dispatcher candidate validation and
Run behavior are tested against a flag that is known somewhere in the command
tree but unknown to auth.

Comment thread dispatcher_test.go
Comment on lines +1272 to +1285
run := func(t *testing.T, args []string) string {
t.Helper()
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err := newDispatcher().Execute(args)

w.Close()
os.Stdout = old

var buf bytes.Buffer
io.Copy(&buf, r)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid synchronous pipe capture and restore stdout with defer.

io.Copy runs only after Execute returns, so the test can deadlock if help output fills the pipe buffer. Also, a panic during execution leaves the process-wide os.Stdout modified. Capture output through the project’s existing test helper, or read concurrently and restore stdout with defer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dispatcher_test.go` around lines 1272 - 1285, The stdout capture in the test
helper inside run is brittle because io.Copy happens only after
newDispatcher().Execute returns, which can deadlock if help output fills the
pipe and can also leave os.Stdout altered on panic. Update this helper to use
the project’s existing output-capture test utility if available, or otherwise
read from the pipe concurrently while Execute runs and restore os.Stdout with
defer so the original stream is always reset even on failure.

@evanphx
evanphx merged commit a397dcb into main Jul 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants