Skip to content

v0.3.0

Choose a tag to compare

@takeokunn takeokunn released this 25 Jul 09:37
v0.3.0
3968d3c

Added

  • A full MkDocs (Material) documentation site under docs/, published to
    GitHub Pages via nix build .#docs and .github/workflows/docs.yml.
    Covers installation, a quick start, a per-topic guide (option values,
    option relations, commands, validation, CLI behavior, shell completion,
    documentation generation), an API reference, the migration guide, scope
    and non-goals, and the project's governance documents.
  • define-app / define-command (src/model-dsl.lisp): a declarative,
    clause-based DSL over make-app / make-command / make-option /
    make-positional. :option, :positional, and :command clauses replace
    the nested :global-options (list ...) / :positionals (list ...) /
    :commands (list ...) keyword arguments; :command clauses nest the same
    vocabulary for a command's own options, positionals, and subcommands, and
    :commands-from splices in an already-built command list (e.g.
    make-standard-commands, or another spec bound via define-command).
    Purely additive -- the functional API is unchanged and remains
    independently usable. See README's "Declarative DSL" section.
  • Adopted cl-json-kit as a
    test-only dependency (mirroring cl-process-kit's test-only role): the
    json renderer test suite now parses render-json's own output back
    through cl-json-kit's independent reader as an additional correctness
    oracle, catching structural/escaping defects that substring-matching the
    writer's output can't. Not needed to use cl-cli itself.

Fixed

  • Two README code examples called run-app with argv as a positional
    argument instead of :argv, which signals odd number of &KEY arguments
    if copy-pasted as-is.

Changed

  • The Nushell and Elvish completion renderers' quoting functions
    (%COMPLETION-NUSHELL-QUOTE, %COMPLETION-ELVISH-QUOTE) and the
    Markdown documentation renderer's prose-escaping functions
    (%MD-ESCAPE-PROSE, %MD-SINGLE-LINE, %MD-MAX-BACKTICK-RUN, all in
    src/doc-renderers-markdown.lisp) had the same double-nested
    WITH-OUTPUT-TO-STRING pattern found and fixed in bash/zsh/PowerShell's
    quoting functions earlier in this changelog -- each ran its own escaping
    pass over the already-built result of a separate control-character-
    stripping pass, rather than folding both into one. %MD-SINGLE-LINE's
    second pass turned out to be provably dead code once traced through
    (its input can never contain the newline/return characters it was
    checking for, since the upstream control-stripping pass already maps
    them to a space), so it now simply delegates. %MD-MAX-BACKTICK-RUN
    needed care: a character control-stripping drops entirely doesn't
    break a backtick run in the final output, while one it maps to a
    space
    does, so its merged single pass reproduces that distinction
    exactly rather than just concatenating both transformations (see the
    new regression test in tests/cases-doc-markdown.lisp, which checks a
    control character between two backticks counts as one run of 2 while a
    newline between them counts as two runs of 1). Nushell and Elvish
    completion rendering were separately profiled and found already fast
    enough (tens of microseconds per call on a 330-option benchmark app)
    that this fix's effect is not separately measurable there; it is applied
    for consistency with the other renderers, not for a measured win. No
    public API or observable output changed.
  • The PowerShell completion renderer (RENDER-POWERSHELL-COMPLETION):
    %COMPLETION-POWERSHELL-QUOTE folds control-character-stripping and
    single-quote-doubling into one pass instead of two nested
    WITH-OUTPUT-TO-STRING calls (the same fix already applied to
    %COMPLETION-SHELL-QUOTE and %COMPLETION-ZSH-ARGUMENTS-FIELD), and
    %COMPLETION-POWERSHELL-COMMAND-OPTION-MAP now builds each command's
    option-array literal once and reuses it across every alias instead of
    rebuilding it (re-quoting every token again) per alias. (Profiled all
    four remaining completion renderers this round: nushell and elvish are
    already tens of microseconds per call -- optimizing them further would
    be immeasurable -- so only PowerShell needed work.) Measured on a
    330-option/33-command benchmark app: PowerShell rendering is roughly 1.3x
    faster. No public API or observable script output changed; see
    tests/cases-parser-benchmark.lisp.
  • The fish completion renderer (RENDER-FISH-COMPLETION) no longer
    re-shell-quotes the same loop-invariant value on every iteration of a
    loop it's constant across: the app name, each level's "already seen a
    sibling command" condition, and each command's description are now
    quoted once and reused, instead of being re-quoted once per command
    alias, once per option candidate, or once per complete line as
    before. (Profiling on a 330-option/33-command benchmark app after fixing
    the bash and zsh renderers, above, found fish was the next-most-expensive
    renderer, dominated almost entirely by this redundant re-quoting rather
    than the tree-copy pattern those two had; nushell and elvish were also
    profiled and are already fast enough -- tens of microseconds per call --
    that optimizing them further would be immeasurable.) Measured: fish
    rendering on the same benchmark app is roughly 1.5x faster. No public API
    or observable script output changed; see
    tests/cases-parser-benchmark.lisp.
  • The bash and zsh completion renderers (RENDER-BASH-COMPLETION,
    RENDER-ZSH-COMPLETION) write directly into one shared output stream
    instead of building a separate string per recursive subcommand node (each
    of which a parent then copied again via WRITE-STRING) and per
    shell-quoted or array/case-label value (each of which was quoted into its
    own string, joined into a second string, then copied a third time into a
    parent's buffer). Profiling a 330-option/33-command benchmark app showed
    this quote-then-join-then-copy pattern accounting for the large majority
    of sampled render time in both renderers, dominated by repeated
    WITH-OUTPUT-TO-STRING setup/copy overhead rather than the actual
    shell-quoting logic. %COMPLETION-SHELL-QUOTE's core loop is now also
    exposed as %COMPLETION-WRITE-SHELL-QUOTED, which writes straight to a
    caller-supplied stream, with shared %COMPLETION-WRITE-CASE-LABELS /
    %COMPLETION-WRITE-SPACE-JOINED-QUOTED writers built on top of it used by
    both renderers (%COMPLETION-BASH-ARRAY-LITERAL and
    %COMPLETION-CASE-LABELS, now dead, were removed). Zsh's
    %COMPLETION-ZSH-ARGUMENTS-FIELD (used to build every _arguments
    bracket/placeholder field) also folded its control-character-stripping and
    bracket-blanking into one pass instead of two nested
    WITH-OUTPUT-TO-STRING calls, the same fix already applied to
    %COMPLETION-SHELL-QUOTE. Measured on the 330-option benchmark app: bash
    rendering is roughly 1.5x faster and zsh rendering roughly 1.5x faster
    than before this round of changes. No public API or observable script
    output changed; see tests/cases-parser-benchmark.lisp.
    Measured on the same 330-option benchmark app: bash completion rendering
    is roughly twice as fast as before this round of changes (and faster
    still relative to the pre-caching baseline). No public API or observable
    script output changed; see tests/cases-parser-benchmark.lisp.
  • PARSE-ARGV is dramatically faster, especially across repeated calls
    against the same APP (a REPL, a long-running server, or simply a hot
    loop). MAKE-APP now precomputes and caches, once per app/command scope,
    the built-in-augmented option list, its name/alias lookup table, and the
    command-name/alias lookup table -- previously all three were rebuilt from
    scratch (including re-running MAKE-OPTION for the built-in --help/
    --version specs) on every single PARSE-ARGV call. Three option-
    relationship validators (VALIDATE-CONDITIONAL-REQUIREMENTS,
    VALIDATE-REQUIRED-OPTION-GROUPS, VALIDATE-INCLUSIVE-GROUPS) now skip
    building their lookup tables entirely when the app declares none of the
    corresponding feature (conditional requirements / option groups), mirroring
    VALIDATE-OPTION-RELATIONSHIPS' existing early exit. The per-parse
    accumulating-option-value hash table is now allocated lazily, on the first
    value that actually needs it, instead of unconditionally on every call.
    Measured on a representative subcommand-dispatch benchmark: ~4.5x faster
    for repeated parses against one app, up to ~15x for a flag-only app with
    no subcommands. BUILT-IN-OPTION-SPECS -- also called directly by every
    help and completion renderer, not just the parser -- is cached the same
    way, so generating help text or a shell-completion script no longer
    reruns MAKE-OPTION for the built-ins either. No public API or
    observable behavior changed; see tests/cases-parser-benchmark.lisp.
  • CANONICAL-OPTION-NAME (the hot path for every long-option token during
    parsing) now strips a leading -/-- and downcases the remainder in one
    pass instead of two separate allocations (SUBSEQ then
    STRING-DOWNCASE). Shell-completion rendering is also faster on apps
    with many options: %COMPLETION-SHELL-QUOTE (used by the bash/zsh/fish
    renderers) now strips control characters and quote-escapes in a single
    pass rather than two nested string streams, and three
    REMOVE-DUPLICATES call sites switched from :TEST #'STRING= to
    :TEST #'EQUAL -- semantically identical for strings, but lets SBCL use
    its hash-table-based dedup instead of an O(n^2) pairwise scan. Measured
    on a 330-option benchmark app: shell-completion rendering is noticeably
    faster; no observable behavior changed.
  • cl-cli no longer depends on cl-prolog at runtime. Option-relation
    validation (:requires, :requires-any-of, :conflicts-with) is now a
    plain in-memory adjacency graph (src/option-relations.lisp) with the same
    public behavior and error messages. cl-cli's only dependency is now
    uiop; cl-prolog remains a test-only dependency (used as an independent
    validation oracle in the test suite).
  • The test suite's real-shell verification (tests/cases-shell-verification.lisp)
    now launches every subprocess (bash -n, zsh -n, fish --no-execute,
    mandoc -T lint, ...) through cl-process-kit with an explicit timeout
    instead of an unbounded uiop:run-program call. A hung or misbehaving
    tool is now terminated (SIGTERM escalating to SIGKILL, in its own process
    group) rather than blocking the test run indefinitely; a regression test
    proves the timeout actually fires. Test-only; does not affect cl-cli
    itself, which never spawns subprocesses.
  • The man page and Markdown doc renderers (%MANPAGE-OPTION-ENTRY,
    %MD-OPTION-TABLE) no longer rebuild a full %OPTION-TARGET-TABLE from
    scratch for every single option in their loop -- an O(n^2) hash-table-
    construction cost that dominated both renderers' profiles on a large app.
    Each loop now hoists one table build to the top and threads it through
    explicitly, mirroring the plain-text help renderer's existing pattern.
    Measured on a 330-option/33-command benchmark app: render-manpage ~40%
    faster (682us -> 406us), render-markdown ~50% faster (1073us -> 534us).
    No public API or observable output changed; see
    tests/cases-parser-benchmark.lisp.
  • render-json writes directly into a shared stream instead of each layer
    (%option->json, %command->json, %app->json, ...) building and
    returning its own string for its caller to copy again one level up --
    the same allocation-cascade bug already fixed in the bash/zsh/PowerShell
    completion renderers, one level removed. render-json's public contract
    (a string when called without a stream, no return value when passed one)
    is unchanged. Measured on a 330-option/33-command benchmark app: ~3.7x
    faster (963us -> 263us per call), verified against cl-json-kit's
    independent reader to confirm the output is still valid and semantically
    identical, not just byte-similar. See tests/cases-parser-benchmark.lisp.
  • MAKE-APP-time option-relation validation (:requires,
    :requires-any-of, :required-if, :required-unless, :conflicts-with)
    no longer rebuilds a full option target-table from scratch once per
    declared relation target, plus again independently in cycle detection and
    relation-graph construction -- the table is now built once per MAKE-APP
    call and threaded through. Measured on a relation-heavy 100-option
    benchmark app: MAKE-APP ~36% faster (4.822ms -> 3.109ms). Apps declaring
    no option relations at all also skip relation-graph construction entirely
    now, rather than building an empty one: ~27% faster MAKE-APP on a
    330-option/33-command app with no relations declared (1.755ms -> 1.288ms).
    No public API or observable behavior changed; see
    tests/cases-parser-benchmark.lisp.
  • PARSE-ARGV's four option-relation validators (VALIDATE-OPTION- RELATIONSHIPS, VALIDATE-REQUIRED-OPTION-GROUPS, VALIDATE-INCLUSIVE- GROUPS, VALIDATE-CONDITIONAL-REQUIREMENTS) no longer each independently
    rebuild their own key/target lookup tables from the validated-specs list
    on every parse; the tables are now cached once per app/command scope
    (alongside the existing relation-graph cache) and threaded through.
    Measured on an app combining multiple relation kinds on overlapping
    options: PARSE-ARGV ~37% faster (2.862us -> 1.801us per call). No public
    API or observable behavior changed; see tests/cases-parser-benchmark.lisp.