feat(cli)!: refuse an argument no command declares - #402
Merged
Conversation
A mistyped flag was accepted, ignored and reported as a success: `fce config show --verbosee` exited 0 having done nothing about verbosity. The parser collects an argument it does not recognise into the remaining arguments, and this tool never reads them — grep finds no use of context.Remaining anywhere in the CLI — so the leftovers were dropped in silence. A pipeline asking for something the tool does not do was told it had it. Rejecting an unknown option is what every comparable tool does: git exits 128, ls and grep exit 2, dotnet build exits 1. Tolerating one is the outlier, and it is the parser's default rather than a choice made here. Refused arguments now report as the usage error 64 that ADR-0067's set already carries, naming the offending argument. The parser's own strict mode says the same thing and is NOT used. In Spectre.Console.Cli 0.55 — the newest published version, so this is not an upgrade away — UseStrictParsing makes an option declared without a value swallow the internal "__default_command" token as that value: `fce generate --solution` then looks for a file by that name and exits 1, instead of reporting a usage error. It trades a silent wrong for a visible one that leaks a parser internal into a user-facing message. Refusing the leftovers through an interceptor keeps that diagnosis intact, which a test now pins. BREAKING CHANGE: a command line carrying an argument no command declares now exits 64 instead of running and exiting 0. Nothing can depend on the previous behaviour deliberately — the arguments were never read — so what breaks is a caller whose invocation is already wrong and silently ignored, which is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuSBsGG7wMPMnSeTHKZAom
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A mistyped flag was accepted, ignored, and reported as a success:
The parser collects an argument it does not recognise into the remaining arguments, and this tool never reads them —
grepfinds no use ofcontext.Remaininganywhere inFirstClassErrors.Cli. So the leftovers were dropped in silence, and a pipeline asking for something the tool does not do was told it had it.Rejecting an unknown option is what every comparable tool does. Measured here rather than remembered:
git status --nope128ls --nope2grep --nope2dotnet build --nope1fce config show --nope(before)0Type of change
Changes
Refused arguments now report as
ExitCodes.UsageError(64) — the code ADR-0067's set already carries — naming the offending argument.The parser's own strict mode says the same thing and is deliberately NOT used. This is the part worth reviewing. In Spectre.Console.Cli 0.55 — the newest published version, so this is not an upgrade away —
UseStrictParsing()makes an option declared without a value swallow the internal__default_commandtoken as that value:It trades a silent wrong for a visible one that leaks a parser internal into a user-facing message, and it regresses the missing-value case from a clean
64to a misleading1. Refusing the leftovers ourselves — anICommandInterceptorthat rejects anything landing inRemaining— keeps the parser's own diagnosis intact. A test pins that:fce generate --solutionstill reports "Option 'solution' is defined but no value has been provided" and exits64.Behaviour after this change:
fce config show0fce config show --nope64Unknown argument '--nope'.fce config show extra64Unknown command 'extra'.fce generate --solution64Option 'solution' is defined but no value has been provided.fce frobnicate64Unknown command 'frobnicate'.fce --help0Docs — both exit-code tables in the catalog-versioning reference gain "unknown option", English and French.
One suppression.
UndeclaredArgumentExceptionisinternal, which raises S3871 ("exception types should be public"). Suppressed at the type with a written justification: the rule exists so a caller outside the assembly can catch it, and this assembly is an executable — nothing references it, and the only code that catches it is the exit-code handler a few lines above.Testing
dotnet build FirstClassErrors.sln— succeeded, 0 warnings, 0 errorsdotnet test FirstClassErrors.sln— 2206 passed, 0 failed, 0 skipped across the 13 suites (FirstClassErrors.Cli.UnitTests70 → 72)FirstClassErrors.Analyzers.UnitTests) — 132 passed, run as part of the solution test aboveEvery before/after behaviour quoted above was observed on the built binary, including the
UseStrictParsing()regression — that path was implemented, measured, and then backed out in favour of the interceptor.Documentation
doc/updatedCatalogVersioningReferencemoves in both languages. The CI guide still needs no change: it propagates the code without enumerating the set.Architecture decisions
Proposed: ADR-____This lands inside ADR-0067's set rather than extending it —
64already means "the command line was refused", and this widens what counts as refused without adding a code. Say the word if you read the widening itself as decision-shaped, @Reefact; I did not, because the set and its meanings are unchanged.A breaking change, flagged as one, and accepted.
feat(cli)!with aBREAKING CHANGE:footer, so theclirelease train sees a MAJOR. @Reefact accepted the break on the grounds that nothing has shipped yet — which the repository bears out: the only release tag islib-v0.1.0-preview.1, and theclitrain carries no tag at all, sofcehas never been published and no consumer exists to break. Independently of that, nothing could have depended on the previous behaviour deliberately, since the arguments were never read.Related issues
None. Follows #395, #396, #397 and #399.