Skip to content

Reserve -h/--help and list the help row on every page - #22

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

baijum merged 2 commits into
mainfrom
fix/help-handling

Conversation

@baijum

@baijum baijum commented Sep 12, 2026

Copy link
Copy Markdown
Member

Closes #9 and #10, both about the hardwired -h/--help.

#10: --help as a user option disabled dispatch

flag and option now raise when given -h or --help:

$ kaappi --lib-path lib app.scm
app.scm:2: error[KP3000]: option: -h and --help are reserved for the built-in help "-H" "--help"

Reserving the names, rather than dispatching on a private key, is the honest choice: the parser honours -h/--help before any spec lookup on every level, and every help page lists the row, so a user option with either name could never have worked as declared. Catching it when the spec is built means the app fails at definition time with a message naming the offending spec, instead of at the first invocation with a help page. A name that merely starts with help (--helper) is fine, and the check runs inside command specs too since they use the same builders.

This also removes the edge noted on PR #21: a clustered -xh and a bare -h can no longer diverge, because -h cannot be declared.

#9: help row missing on option-less pages

The -h, --help row was nested inside the Options section, which only rendered when the spec declared options, so myapp init --help printed a page that never mentioned the -h it had just accepted. The section and the row now render on every page, and the usage line always says [options]:

greeter farewell — Say goodbye

Usage: greeter farewell [options] <name>

Options:
  -h, --help                Show this help

Arguments:
  <name>                    Name

Pages that already had options are unchanged. I diffed both README help samples (quick start and build --help) against the actual output: identical.

Tests

12 new checks: the help row, Options section and [options] on an option-less subcommand and on an app with no options at all, -h still honoured there, and rejection of --help/-h on flag, option, and inside a command spec, with the exact message, plus --helper accepted. Suite: 170 passed, 0 failed on Kaappi v0.27.1.

Docs

README Help section states the always-present row and the reserved names. CHANGELOG: two Changed entries, since raising on a previously accepted spec and adding a section to some help pages are both visible changes.

Closes #9
Closes #10

🤖 Generated with Claude Code

Help is hardwired: the parser honours -h and --help before any spec
lookup, on every level. Two things contradicted that. A user option
named --help stored its value under the "help" key that dispatch tests,
so every invocation printed the help page and no handler ever ran, and
the page listed two --help rows. And the built-in help row lived inside
the Options section, which only rendered when the spec declared options
of its own, so a page for an option-less subcommand said nothing about
the -h it accepted.

flag and option now raise when given -h or --help, so the collision is
caught when the spec is built rather than discovered at the first
invocation. This also removes the divergence between a clustered -xh
and a bare -h that PR #21's review noted, since -h can no longer be
declared. Every help page renders the Options section with the help row
and says [options] in its usage line; pages that already had options are
byte-for-byte unchanged.

Closes #9
Closes #10

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 PR head (9190d44), Kaappi v0.27.1.

Verified

  • Suite: 170 passed, 0 failed.
  • Both README help samples (quick start and build --help) are byte-identical to the actual output.
  • -h on option-less specs still yields help at both levels (napp -h, myapp init -h), and a cluster like -vh expands to help too, so the always-on Options row and [options] in the usage line are honest.
  • The CHANGELOG's "every invocation printed help" is accurate for the canonical repro from #10: an option with a truthy default stored under "help" makes run-cli's truthiness check fire on every parse. (With a flag, default #f, the old collision only showed via shadowing and the duplicate help row.)

Both #9 and #10 look resolved. Two non-blocking notes:

  1. The crossed-slot gap in check-not-help (inline comment) — (flag "-H" "-h" ...) and (flag "--help" "--helper" ...) still build and print the reserved name on the help page.
  2. Nit: with -h no longer declarable, the o disjunct in expand-cluster's (or o (string=? short "-h")) (lib/kaappi/cli.sld:355) is now dead — find-opt-short can never match -h against specs that all passed check-not-help. Harmless to keep as belt-and-braces, but it simplifies to (string=? short "-h") if you'd rather.

Comment thread lib/kaappi/cli.sld Outdated
;; -h and --help are handled by the parser and listed on every help
;; page, so a spec cannot claim either name.
(define (check-not-help who short long)
(when (or (equal? short "-h") (equal? long "--help"))

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.

This checks each slot against only its own spelling, so the crossed forms still build. (flag "-H" "-h" "H") is accepted and renders:

Options:
  -H, -h                    H
  -h, --help                Show this help

The page now claims -h for the flag in the long column, while typing -h still gives the built-in help — the same divergence this PR removes, via the other slot. (flag "--help" "--helper" "H") is symmetric (prints a --help, --helper row). The -h-as-long case is worse: opt-long-name computes (substring "-h" 2 1), past the end of the string — R7RS leaves that an error, and Kaappi clamps to "", so the value is stored under an empty key that parsed-ref can never reach.

Checking both slots against both names closes it:

(when (or (member short '("-h" "--help"))
          (member long '("-h" "--help")))

Non-blocking — these are pathological specs — but it's two lines and makes the "reserved" promise literal.

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.

Done in a64c1e1: both slots are checked against both names with member, so (flag "-H" "-h" ...) and (flag "--help" "--helper" ...) now raise with the same message. Good catch on the empty-key consequence of -h in the long slot.

Comment thread tests/test-cli.scm
"option: -h and --help are reserved for the built-in help"
(spec-error (lambda () (option "-h" "--host" "Host" "localhost"))))

(check "a name that merely starts with help is fine" #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.

If the reservation is widened to both slots (see the comment on check-not-help), the crossed cases belong next to this one: (flag "-H" "-h" ...) and (flag "--help" "--helper" ...) both rejected, --helper-style names still accepted.

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.

Added in a64c1e1 next to the existing reserved-name checks: both crossed forms rejected with the exact message, and --helper still accepted.

check-not-help tested each slot only against its own spelling, so the
crossed forms (flag "-H" "-h" ...) and (flag "--help" "--helper" ...)
still built and put the reserved name on the help page next to the
built-in row. The -h-as-long case was worse: opt-long-name substrings
past the end of "-h", so the value landed under an empty key that
parsed-ref could never reach. Both slots are now checked against both
names, which makes the reservation literal.

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 a64c1e1:

  • Crossed slots: check-not-help now tests both the short and long slot against both reserved names. Two tests pin (flag "-H" "-h" ...) and (flag "--help" "--helper" ...) as rejected.
  • expand-cluster disjunct: left as is. In (or o (string=? short "-h")) the o is what admits a declared flag such as -v; the clause above it only handles value-taking options. Dropping o would make -vv an unknown option. What is now unreachable is o being non-false when short is -h, which is why the two disjuncts are exclusive rather than one being dead. I reworded the comment on that clause to say "a declared flag, or the built-in help".

Suite: 172 passed, 0 failed.

@baijum
baijum merged commit 13dd70c into main Sep 12, 2026
2 checks passed
@baijum
baijum deleted the fix/help-handling branch September 12, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant