Skip to content

fix: accept boolean flag values written with a space - #1037

Merged
JonJagger merged 4 commits into
mainfrom
fix-boolean-flag-space-separator
Jul 28, 2026
Merged

fix: accept boolean flag values written with a space#1037
JonJagger merged 4 commits into
mainfrom
fix-boolean-flag-space-separator

Conversation

@JonJagger

Copy link
Copy Markdown
Contributor

"kosli attest generic Dockerfile --compliant false" failed with
"accepts at most 1 arg(s), received 2". pflag's NoOptDefVal makes a
boolean flag never consume the next token, so "false" was left behind
as a positional argument. The bare "--compliant" form and the space
form are gated by that same single field in opposite directions, so no
custom pflag.Value type can fix it, because the consume-or-not decision
is made before Set() is ever called on the value.

Normalize the argument slice before pflag sees it, rewriting the space
form into the "=" form for boolean flags only. Both places that feed
args to cobra do this: innerMain for the normal path, and getMultiOpts
for the multi-host path, where the stray positional previously emptied
MultiOpts and silently downgraded a multi-host call to a single host.

The golden test harness normalizes too, so tests exercise the same args
path as the real CLI instead of asserting the pre-fix behaviour.

Deliberately left alone, each still failing loudly with the link to the
boolean flags FAQ: grouped shorthands such as "-qC false", literals
pflag accepts via "=" but Kosli does not document such as "TRUE" and
"1", and anything following a "--" terminator.

Refs kosli-dev/server#6235

  "kosli attest generic Dockerfile --compliant false" failed with
  "accepts at most 1 arg(s), received 2". pflag's NoOptDefVal makes a
  boolean flag never consume the next token, so "false" was left behind
  as a positional argument. The bare "--compliant" form and the space
  form are gated by that same single field in opposite directions, so no
  custom pflag.Value type can fix it, because the consume-or-not decision
  is made before Set() is ever called on the value.

  Normalize the argument slice before pflag sees it, rewriting the space
  form into the "=" form for boolean flags only. Both places that feed
  args to cobra do this: innerMain for the normal path, and getMultiOpts
  for the multi-host path, where the stray positional previously emptied
  MultiOpts and silently downgraded a multi-host call to a single host.

  The golden test harness normalizes too, so tests exercise the same args
  path as the real CLI instead of asserting the pre-fix behaviour.

  Deliberately left alone, each still failing loudly with the link to the
  boolean flags FAQ: grouped shorthands such as "-qC false", literals
  pflag accepts via "=" but Kosli does not document such as "TRUE" and
  "1", and anything following a "--" terminator.

  Refs kosli-dev/server#6235
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @JonJagger's task in 5m 4s —— View job


Code Review

  • Static analysis (build, vet)
  • Read changed files
  • Review normalization logic + edge cases
  • Review tests
  • Post review

Static analysis

  • go build ./...
  • go vet ./cmd/kosli/...
  • No go.mod/go.sum changes, so no dependency drift to check.

(I couldn't run the suite here — the command tests need the local Kosli server from make test_setup, and the standalone TestNormalizeBoolFlagArgs* tests were blocked by the sandbox. I traced them by hand instead and they line up with the implementation.)

Assessment — this is a strong PR

The core insight in the description is correct and well-explained: pflag's NoOptDefVal decides before Set() whether to consume the next token, so no custom pflag.Value can rescue the space form — the fix has to happen on the arg slice before pflag sees it. normalizeBoolFlagArgs is a clean way to do that.

Things I specifically checked and am satisfied with:

  • Two-pass design is necessary and correct. A global bool flag in space form before the subcommand (--debug false list flows) would otherwise leave false as a stray positional and stop root.Find resolving the command. Pass 1 over root.LocalFlags() clears that so pass 2 can resolve the leaf and reach its flags. Nicely pinned by TestNormalizeBoolFlagArgsJoinsGlobalBoolFlagBeforeSubcommand.
  • Idempotency holds across the double call. getMultiOpts normalizes, then innerMain normalizes again on the multi-host path — a joined --flag=value token no longer matches any flag token, so the second pass is a no-op. Correct.
  • The multi-host bug fix is the subtle, valuable part. Previously a stray positional emptied MultiOpts and silently downgraded a multi-host call to single-host — a quiet correctness bug, now covered by TestRunDoubledHostAcceptsSpaceSeparatedBoolFlag.
  • Shorthand rewrite is safe. -C false-C=false is accepted by pflag's short-arg parser (-C=value), and the end-to-end golden case confirms it. Grouped shorthands (-qC) are deliberately left alone.
  • -- terminator and non-bool flags are both handled: --name false passes through untouched, nothing past -- is rewritten.

The doc comments are excellent — the two accepted ambiguities (positional literally named true/false, and a bool-flag token used as another flag's value) are pinned in both prose and tests, so the boundary of the rewrite is fully documented rather than implicit. Restricting the space form to true/false only (while = still accepts 1/0/TRUE) is a good call, and the reasoning (positionals named 0/1 are plausible artifact names) is convincing.

Minor / optional notes (nothing blocking)

  1. Stale dead test helper. HelpStatusLines() (cmd/kosli/multiHost_test.go:212) is only referenced from a commented-out block (:295), so it isn't exercised — but its --debug line still carries A boolean flag https://docs.kosli.com/faq/#boolean-flags, which no longer matches debugFlag in root.go:125. Since this PR is already tidying the FAQ references, it'd be a natural moment to delete the dead helper (or refresh its text). Pre-existing, so entirely optional.
  2. Coverage gap (small). normalizeBoolFlagArgs_test.go has no unit case for a lone shorthand in space form (-C false-C=false); it's only covered end-to-end by the attestGeneric golden. A one-line unit case would pin the shorthand rewrite at the same level as the long form. Not required.
  3. Consistent with the earlier review thread, the --name --compliant false ambiguity is now documented and tested (TestNormalizeBoolFlagArgsCapturesBoolFlagTokenUsedAsFlagValue) — good follow-through.

Nice work — the reasoning, the docs, and the test coverage are all above the bar.
fix-boolean-flag-space-separator

Comment thread cmd/kosli/normalizeBoolFlagArgs.go
Comment thread cmd/kosli/normalizeBoolFlagArgs.go Outdated
  Review of #1037 spotted an input the rewrite reads differently from
  pflag and that nothing recorded: "--name --compliant false". pflag
  gives --name the value "--compliant" and leaves "false" positional,
  whereas the rewrite sees a space-form boolean and joins it.

  Left as is. It sits in the same family as the already-accepted
  positional named "true", and telling the two apart would mean tracking
  which tokens pflag consumes as values, turning a token pre-pass into a
  second parser. What was missing was the boundary being written down,
  so a later reader does not rediscover it as a bug.

  The doc comment now names the rewrite as positional and lists both
  inputs, and a test pins the behaviour the way the sibling ambiguity
  already is.

  Comment and test only, no behaviour change.
@mbevc1
mbevc1 self-requested a review July 28, 2026 09:01
@mbevc1

mbevc1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Looks okay, just a note - this fix works for and correctly handles global flags positioned after the subcommand, but it does not address global bool flags written in space form before the subcommand, and that case fails with a confusing error + hard exit. So it does not address all bool args.

Perhaps we can leave it like this to avoid potentially swallowing even more true / false commands.

  Marko pointed out on #1037 that the space-form fix only covered flags
  placed after the subcommand. Written before it, "kosli --debug false
  list flows" printed the root help and exited 0: the command silently
  never ran, yet the caller was told it had succeeded.

  The flags in scope depend on where the token sits, so a single pass
  cannot cover both positions. root.Find cannot resolve the command while
  a leading bool flag is still in the space form, because its value is
  left as a stray positional that cobra reads as an unknown subcommand.
  Normalizing from root's own flags first is what lets Find succeed, so
  the second pass can still reach the flags declared on the resolved
  command.

  Refs #1037
@JonJagger

Copy link
Copy Markdown
Contributor Author

Good spot. New commit pushed to fix this.

@mbevc1

mbevc1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Awesome, thanks @JonJagger ! We should also probably clean up flag messages and docs, e.g.

./cmd/kosli/root.go:	attestationCompliantFlag        = "[defaulted] Whether the attestation is compliant or not. A boolean flag https://docs.kosli.com/faq/#boolean-flags"

  The CLI now accepts boolean flag values written with a space, so the
  --compliant and --new-compliance-status help text no longer needs to
  send users to the FAQ to learn the --flag=value form. Pointing at the
  FAQ from these two flags alone was also inconsistent: none of the other
  ~23 boolean flags carried the link.
@JonJagger

Copy link
Copy Markdown
Contributor Author

Agreed. New commit on its way

@JonJagger
JonJagger merged commit cb806d6 into main Jul 28, 2026
20 checks passed
@JonJagger
JonJagger deleted the fix-boolean-flag-space-separator branch July 28, 2026 12:01
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