v1.0.0
The API is now covered by Semantic Versioning. What that promise does and does
not cover is written down in
docs/src/compatibility.md,
along with the supported-implementation matrix and the deprecation policy.
This release is mostly about earning that promise rather than adding features.
The breaking changes below are deliberately concentrated here, because 1.0 is
the last point at which they are free: an accessor family that could not be
read, a condition taxonomy that made a programmer's bug look like a user's,
and a machine-readable format with no version marker would all otherwise have
been permanent. Verification was rebuilt to match: nix flake check was
previously guaranteed to fail, the checks that run generated scripts through
real shells were almost entirely skipping, and no non-SBCL implementation could
compile the suite at all.
Added
- The positional accessor family is exported.
app-positionals,
command-positionals, andmake-positionalwere all public, so callers
legitimately receivedpositional-specstructs -- but not one of its slot
readers was exported, leaving them opaque unless you reached through
cl-cli::. All sixteen are public now, renamed from thepositional-spec-
conc-name topositional-to match theoption-/command-/app-
families they sit beside. Also newly exported:command-subcommandsand
command-default-command(nested subcommands are a documented feature and
app-default-commandwas already public),option-groupplus
option-group-members/-mode/-required-p(so a custom help renderer
can render "exactly one of ..." instead of degrading to pairwise
conflicts), andbuilt-in-option-specs(so it can see the synthesized
--help/--version). Between them, a third-party renderer can now
reproduce everything the built-in one prints. cli-missing-required-option, a subtype ofcli-missing-option-value
for the single most common CLI mistake there is: a required option never
supplied. It was previously indistinguishable from "--outputtyped with
nothing after it". Being a subtype, a handler that does not need the
distinction is unaffected.cli-response-file-error(with acli-response-file-error-path
reader), for the three@fileexpansion failures that previously signaled
a barecli-usage-error-- "your@args.txtis missing" deserves a
different message from "you mistyped a flag".- Docstrings on every exported condition class and slot reader. The
condition hierarchy is the primary integration point for a consumer CLI and
had no prose at all;cli-invalid-option-value-causein particular was
unguessable. tests/cases-public-api.lisp, which pins the exported symbol set in both
directions.:exportfails silently in the direction that matters -- a
misspelled name interns a dead symbol rather than signaling -- and no other
test would notice, since they all call these symbols from a package that
:usescl-cli. It also asserts that every exported condition is reachable
fromcli-errorand thatcli-invalid-specificationstays out of the
cli-usage-errorbranch.render-jsonnow emits aschemaVersionmember first, and exports
+json-schema-version+(currently1). A machine-readable format with no
version marker gives a consumer nothing to branch on; a tool written against
today's shape would silently misread a future one. Adding it now, at the
1.0 boundary, is the last point where it costs nothing. New members may
still appear in a minor release -- readers should ignore unknown members --
but a change that would break a reader of the previous shape bumps the
number. This changesrender-jsonoutput; a consumer that parses the
0.3.0 document and rejects unknown keys needs updating.docs/src/compatibility.md, stating what the version number promises
now that the project is on Semantic Versioning for real: the exported
symbols of theCL-CLIpackage are covered,cl-cli::internals and the
byte-for-byte text of rendered help/completion/man output are not, plus a
supported-implementation matrix and a deprecation policy.- Dedicated unit tests for the PowerShell, Nushell, and Elvish completion
renderers (tests/cases-completion-powershell.lisp,-nushell.lisp,
-elvish.lisp). Three of the six shipped renderers previously had no
assertions on what they emit at all -- only a performance benchmark and a
"does it parse" smoke check. - An Elvish leg in
tests/cases-shell-verification.lisp: the generated script
is compiled by realelvish -compileonly. Becauseelvishonly exposes its
edit:module interactively, the check bindsedit:to a stub namespace so
the generated code itself is what gets compiled; a negative-control case
asserts that a deliberately broken completer still fails, so the check
cannot pass vacuously.
Changed
cli-invalid-specificationis no longer acli-usage-error. A new
cli-errorroot splits the hierarchy in two:cli-usage-errorfor what the
user typed,cli-invalid-specificationfor what the programmer
declared. The idiomatic
(handler-case (run-app ...) (cli-usage-error (e) (print-usage) (exit 2)))
previously swallowed the developer's own malformed spec and answered it with
a usage message shown to the end user. If such an error does reach
run-app, it now exits with:error-exit-code(70,EX_SOFTWARE)
instead of:usage-exit-code(64,EX_USAGE) -- which is what a bug in
the program actually is. In practice the condition is raised while the spec
is being built, before parsing.
cli-error-app/cli-error-commandare the new names for the context
readers;cli-usage-error-app/-commandstill read the same slots.- The test suite is split into two ASDF systems.
cl-cli/testsis the
portable core;cl-cli/tests/shell-verificationadds the checks that shell
out to real tools and is the only half that needscl-process-kit. The
transitivecl-log-kitdependency hard-codessb-thread:*
(upstream #1), which
previously made the entire suite fail to compile on any non-SBCL
implementation. ECL now runs 637 tests green instead of not building.
tests/run-tests.lispprints which half it loaded, so a run covering less
than expected cannot be mistaken for a pass. - The fuzz suite and the benchmark budgets are now gated on the capability
each actually needs, rather than failing where it is absent: the fuzz suite
needscl-weave's:timeoutplatform capability, and the benchmark
thresholds are absolute milliseconds calibrated against SBCL. Both report as
skips with a reason. No threshold was widened and no assertion was removed. - The benchmark workloads are resized so contention on a shared CI runner
cannot fail them. One case measured 520ms idle and 2534ms against a 2000ms
budget inside a concurrentnix flake check-- a flaky gate, and an
intermittently red check gets read as noise, which is how a real regression
gets waved through. Each case now targets an idle median around a tenth of
its budget; the 2000ms convention itself is unchanged, since raising it is
the one response that keeps the test green while destroying what it
measures. nix flake checkis green again, and now means something it did not before.
Theeclcheck was previously guaranteed to fail, so CI onmainwas
permanently red. Alongside the split above:- the flake defines outputs for
aarch64-linux,x86_64-darwin, and
aarch64-darwinas well asx86_64-linux, sonix developand
nix flake checkwork on a macOS machine instead of silently checking
nothing; - the checks put
bash,zsh,fish,nushell,pwsh,elvish, and
mandoconPATH. The shell-verification cases skip themselves when a
tool is missing, so in CI they had been skipping almost entirely -- the
generated scripts were never actually run by the shells that consume
them. The SBCL suite is now 664 passed, 0 skipped; - the documentation build is a check too, so a broken docs link fails CI
rather than the Pages deploy.
- the flake defines outputs for
- CI runs the flake check on
aarch64-darwinas well asx86_64-linux, so
the macOS support the project claims is verified rather than assumed. consume-value-option(src/parser-option-consumption.lisp) had a
provably unreachable:flag-kind guard clause -- its sole call site is
the(:value :optional-value :key-value)case arm of
consume-long-option-token, so(option-kind spec)can never be:flag
inside its body. Removed as dead code; no observable behavior changed.- Bumped
cl-weave,cl-process-kit,cl-boundary-kit,cl-log-kit, and
cl-json-kitto their latest upstream revisions (cl-prologwas already
current). Verifiedcl-boundary-kit's new default 60-second process
timeout (previously unbounded when:timeoutwas omitted) doesn't affect
cl-cli: everyprocess-kit:runcall site in the test suite already
passes an explicit:timeout.
Security
expand-response-files(:expand-response-files t) had no bound on the
total bytes read across a response-file expansion -- only
+response-file-max-depth+limited nesting depth, not fan-out (many
files referenced at one depth) or a single huge file. A caller piping a
response-file path derived from untrusted input into an app that
opted into response-file expansion could exhaust memory. Added
+response-file-max-total-bytes+(8 MiB, generous for any legitimate
argument list) as a total-size budget checked after every file read;
exceeding it signalscli-usage-errorthe same way exceeding the depth
limit already did.