Skip to content

Raise from generate-help on an undeclared subcommand - #24

Merged
baijum merged 2 commits into
mainfrom
fix/help-unknown-subcommand
Sep 12, 2026
Merged

baijum merged 2 commits into
mainfrom
fix/help-unknown-subcommand

Conversation

@baijum

@baijum baijum commented Sep 12, 2026

Copy link
Copy Markdown
Member

Closes #15.

generate-help app "nope" fell back to the app's description and specs when the name matched no declared command, but kept the unknown name in the header and usage line, so it printed a confident page for a command that does not exist:

t nope — T

Usage: t nope [options] <command>
...

run-cli only ever passes real command names, but generate-help is exported, and an app author calling it with a typo deserves a fault, not a fabricated page.

Change

The command is looked up once, and a miss raises generate-help: no such command with the name as irritant, before anything is printed. That matches the existing treatment of caller bugs in run-cli (a declared command with no handler, no default handler), which also raise rather than print. The two duplicated find-command lookups in the let* collapse into one binding as a side effect.

Tests

Three checks: the error message, that nothing is printed before raising, and that a declared name still renders. Suite: 207 passed, 0 failed on Kaappi v0.27.1.

Docs

README: one sentence under Help. CHANGELOG: Fixed entry.

🤖 Generated with Claude Code

When the name did not match a declared command, generate-help fell back
to the app's own description and specs but still printed the unknown
name in the header and usage line, so the page looked like a real
subcommand page for a command that does not exist. run-cli never passes
an undeclared name, but generate-help is exported and an app author
calling it with a typo got a confident wrong page instead of a fault.

The command is now looked up once and a miss raises, before anything is
printed, in line with the other caller-bug paths in run-cli.

Closes #15

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Baiju Muthukadan <baiju.m.mail@gmail.com>

@baijum baijum left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed locally on the branch (Kaappi v0.27.1, same as the PR): suite passes 207/0, git diff --check is clean, DCO sign-off present, subject matches house style, and the greeter example still runs.

The main thing I verified beyond the tests: the new raise is unreachable through run-cli. parsed-command is only ever set from a found-cmd produced by find-command during parsing; an unknown bare word takes the unknown command error branch instead; and myapp nope --help takes the help-wins arm, which builds the result with no command attached, so it prints the app page. The raise therefore fires only when the exported generate-help is called directly with an undeclared name — exactly the caller-bug case targeted here. A non-string name also fails cleanly: find-command compares with equal?, so it misses and raises with the irritant intact.

Framing as Fixed rather than Changed looks right: the old output (the app's own specs rendered under a fabricated name) had no legitimate use, so no working code can have depended on it.

Two inline nitpicks, both optional — otherwise this looks good to merge from my side.

Comment thread lib/kaappi/cli.sld Outdated
sub-name)))
(if c (cmd-desc c) (cli-desc app)))
(cli-desc app)))
(sub (and sub-name

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Verified the integration concern this change hinges on: run-cli can never reach this raise. parsed-command is only set for a command found via find-command in the parse loop (the found-cmd arm), unknown bare words are reported as usage errors, and --help after an unknown word resets to the app page. So the raise is reachable only by calling the exported generate-help directly with a bad name — the caller bug this targets. Collapsing the two duplicated lookups into one binding is a nice side effect.

Nitpick, feel free to skip: the (and sub-name (or (find-command …) (error …))) chain is correct (error never returns) but the raise is easy to miss as the second arm of or nested inside and inside let*. An if puts the two outcomes at the same level:

(sub (if sub-name
         (or (find-command
               (filter (lambda (s) (eq? (spec-type s) 'command))
                       (cli-specs app))
               sub-name)
             (error "generate-help: no such command" sub-name))
         #f))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Restructured in 532f67a as suggested: if with the or lookup-or-raise in the consequent and #f in the alternative, so both outcomes sit at the same level.

Comment thread tests/test-cli.scm Outdated
"generate-help: no such command"
(guard (e ((error-object? e) (error-object-message e)))
(capture-output (lambda () (generate-help app "nope")))
'returned))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nice trick: if generate-help ever stops raising, the guard body evaluates to 'returned and the check fails instead of passing vacuously. Worth a one-line comment so a future reader doesn't take 'returned for dead code:

    'returned))  ; reached only if generate-help returns normally — must fail

Related nitpick on line 779: (#t #f) swallows any escape, not just error objects — a hypothetical future exit inside generate-help would pass silently. ((error-object? e) #f) keeps the assertion honest. Both optional; the coverage is correct as written.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both taken in 532f67a: the 'returned sentinel carries a comment saying it is reached only if generate-help returns normally and must fail, and the nothing-printed check now guards with (error-object? e) instead of #t.

Review nits. The raise was the second arm of an or nested inside an and
inside a let*, easy to miss; an if puts the found and not-found outcomes
side by side. In the tests, the sentinel that makes the raise check fail
rather than pass vacuously now says so, and the nothing-printed check
catches only error objects so a future non-error escape cannot slip
through it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Baiju Muthukadan <baiju.m.mail@gmail.com>
@baijum

baijum commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Review addressed in 532f67a:

  • Raise placement: if instead of and/or, so the found and not-found outcomes are side by side.
  • Test sentinel: commented; the nothing-printed check guards only error objects.

Suite: 207 passed, 0 failed.

@baijum
baijum merged commit 7c63814 into main Sep 12, 2026
2 checks passed
@baijum
baijum deleted the fix/help-unknown-subcommand branch September 12, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

generate-help with a nonexistent subcommand fabricates a plausible help page

1 participant