cl-cli is a small, dependency-light Common Lisp CLI toolkit for building:
- strict flag and option parsers
- subcommand-based tools
- root/default command entry points
- positional and rest-argument parsers
- help/version handling
- reusable app, command, option, and positional specs
It is intentionally conservative about dependencies so it works well in minimal Common Lisp and Nix environments.
cl-cli depends on cl-prolog, which
is not in Quicklisp, so a bare (ql:quickload :cl-cli) will not resolve on
its own. Choose one of the paths below.
The flake wires up cl-prolog and cl-weave for you:
nix develop # drop into a shell with all dependencies available
nix flake check # run the test suite across sbcl and eclClone cl-cli and its cl-prolog dependency where ASDF can find them (for
example under ~/common-lisp/ or ~/quicklisp/local-projects/), then load it.
Once the dependency is registered, Quicklisp resolves the remaining uiop
dependency automatically:
git clone https://github.com/takeokunn/cl-prolog ~/common-lisp/cl-prolog
git clone https://github.com/takeokunn/cl-cli ~/common-lisp/cl-cli(ql:quickload :cl-cli)Running the test suite additionally requires
cl-weave checked out the same way.
The repository includes Nix checks for both sbcl and ecl, so
nix flake check verifies the current test suite across multiple Common Lisp
implementations.
(ql:quickload :cl-cli)
(defparameter *app*
(cl-cli:make-app
:name "demo"
:version "0.1.0"
:global-options (list (cl-cli:make-option :name "verbose" :short #\v :kind :flag))
:commands (list
(cl-cli:make-command
:name "compile"
:options (list (cl-cli:make-option :name "output" :short #\o :kind :value))
:positionals (list (cl-cli:make-positional :key :input :required-p t))
:handler (lambda (invocation)
(format t "compile ~A -> ~A~%"
(cl-cli:positional-value invocation :input)
(cl-cli:option-value invocation :output)))))))
(cl-cli:run-app *app* '("demo" "compile" "-o" "out.bin" "input.lisp"))(defparameter *script-app*
(cl-cli:make-app
:name "script-runner"
:positionals (list (cl-cli:make-positional :key :script :required-p nil)
(cl-cli:make-positional :key :script-args :rest-p t))
:handler (lambda (invocation)
(format t "script=~S args=~S~%"
(cl-cli:positional-value invocation :script)
(cl-cli:positional-value invocation :script-args)))))Supported option kinds are :flag, :boolean, :value, :optional-value,
:count, and :key-value. A :key-value option parses each occurrence as
key=value (a bare key records value t) and accumulates the pairs into an
alist, matching the compiler-define / container-env shape:
(cl-cli:make-option :name "define" :short #\D :kind :key-value)
;; -D a=1 -D b=2 => (("a" . "1") ("b" . "2"))A :count option is a repeatable counter: each occurrence increments
an integer and it defaults to 0, so -vvv (or a repeated --verbose) yields
3. This is the conventional shape for verbosity flags:
(cl-cli:make-option :name "verbose" :short #\v :kind :count
:description "Increase verbosity.")The built-in help/version flags are --help, -h, --version, and -V.
--version / -V are available only when the app declares a version string.
Optional values accept bare and attached forms by default, such as
--coverage and --coverage=mcdc. Use :consume-optional-value-p t when a
CLI must also accept a separated non-option value such as --coverage true.
Boolean options accept positive and auto-generated negated long forms. For
example, :name "threads" supports both --threads and --no-threads.
Command and global options may also appear after command positionals when a
consumer CLI expects interspersed arguments, such as
demo compile input.lisp --output out.fasl --verbose.
Options may also source their default from environment variables with
:env-var or :env-vars, and callers may layer in values loaded from a config
file by passing a :config plist to parse-argv / run-app. The full
precedence is CLI argument > environment variable > :config > literal :default:
(cl-cli:run-app *app*
:argv (cl-cli:current-process-argv)
:config '(:profile "prod" :threads 8))The config plist is keyed by option key, and its values are coerced exactly like
a literal :default (a string is parsed through the option's :type/:parser,
a list is spread across a repeatable option, and a delimited option splits a
string value).
Help output can surface that metadata directly, including :required-p,
:default, :env-var / :env-vars, enumerated :choices, and option
relationships via :requires / :conflicts-with.
Value-bearing options can also be made repeatable with :multiple-p t; repeated
occurrences accumulate in input order and option-value returns a list.
(cl-cli:make-option :name "threads"
:short #\t
:kind :boolean
:env-var "APP_THREADS")Repeatable value options are useful for include paths, module filters, and similar consumer CLI shapes:
(cl-cli:make-option :name "include"
:short #\I
:kind :value
:multiple-p t
:default '("src"))A :value option can also split a single occurrence into a list with
:value-delimiter (a single character), so --tags a,b,c yields
("a" "b" "c"). Each piece is parsed (honoring :type) and repeated
occurrences keep accumulating; a string or list :default and an environment
value are split the same way:
(cl-cli:make-option :name "tags"
:kind :value
:value-delimiter #\,
:default '("core"))A :value option can consume a fixed number of separate tokens with
:value-count N, returning a parsed list (--point 1 2 => (1 2)); with
:multiple-p each occurrence contributes its own N-element list:
(cl-cli:make-option :name "point" :kind :value :type :integer :value-count 2):value-count may also be :+ (one or more) or :* (zero or more), which
greedily consume following tokens up to the next option-like token
(--files a b c); help shows the value as <NAME>...:
(cl-cli:make-option :name "files" :kind :value :value-count :+)Help output is context-sensitive:
- command help includes an
[options]usage token when the command or app has options - subcommand apps render dispatch-oriented usage such as
Usage: demo [global-options] <command> [args] - app and command specs can expose curated
:exampleslines directly in help - command aliases are shown in the command list
- options and commands can be marked
:deprecated(with an optional reason string); they stay usable and visible but are annotated as deprecated in help and generated docs, and a deprecated command prints a stderr warning when run - command lists can be grouped with
:groupfor large subcommand surfaces - option relationships can enforce
:requiresand:conflicts-with - mutually-exclusive option sets can be declared with
exclusive-group, orrequired-exclusive-groupfor an "exactly one of" obligation - invalid app specs fail fast during
make-app, not only during parsing - spec constructors reject empty strings for user-facing identifiers such as names, aliases, value names, env vars, choices, completion candidates, groups, and examples
- app, command, and option names are restricted to safe identifier characters
(letters, digits,
-,_,.), so author- or config-supplied names cannot inject shell syntax into generated completion scripts - a required positional may not follow an optional one, and two options may not resolve to the same key (e.g. via case-differing single-character names), since either would silently misassign or overwrite parsed values
- an option may not reuse the reserved
:help/:versionkey - unknown commands and options — and invalid
:choicesvalues — suggest the nearest known spelling when the typo is close enough --versionis only shown in help when the app has a version string
Use :stop-parsing-p t for options such as shell -c COMMAND [ARGS...]
where the option consumes one value and every following token, including
flag-like tokens, must remain positional arguments:
(cl-cli:make-option :name "command"
:short #\c
:kind :value
:stop-parsing-p t)Environment-backed defaults are useful for consumer CLIs that already carry runtime configuration in the process environment:
(cl-cli:make-option :name "profile"
:kind :value
:env-var "APP_PROFILE"
:default "dev")When an option accepts a closed set of CLI values, declare them once and let both parsing and help output share the same source of truth:
(cl-cli:make-option :name "mode"
:kind :value
:choices '("dev" "prod")
:description "Execution mode.")Positionals accept :choices too, validated the same way (mismatches signal
cli-invalid-positional-value) and surfaced in help and JSON output:
(cl-cli:make-positional :key :env
:required-p t
:choices '("dev" "prod"))For file-path arguments, attach :value-hint :file or :value-hint :dir to an
option or positional; the generated bash, zsh, and fish completions then offer
file or directory completion at that slot (and it is shown in help and JSON):
(cl-cli:make-option :name "config" :kind :value :value-hint :file)
(cl-cli:make-positional :key :dir :value-hint :dir)A rest positional (:rest-p t) can also bound how many values it collects with
:min-count / :max-count (too few signals cli-missing-positional, too many
cli-unexpected-argument):
(cl-cli:make-positional :key :files :rest-p t :min-count 1 :max-count 8)When a CLI accepts free-form values but shell completion should still suggest a
curated set, keep validation open and attach :completion-candidates
separately. Candidates may be plain values or (value . description) pairs:
(cl-cli:make-option :name "profile"
:kind :value
:completion-candidates '(("dev" . "Local development")
("prod" . "Production release"))
:description "Runtime profile.")For modern CLIs that need cross-option validation, declare option relationships directly in the option spec so parsing and help stay aligned:
(list
(cl-cli:make-option :name "profile"
:kind :value)
(cl-cli:make-option :name "config"
:kind :value
:requires '(:profile)
:description "Config file.")
(cl-cli:make-option :name "token"
:kind :value)
(cl-cli:make-option :name "password"
:kind :value
:conflicts-with '("token")))Relation targets may be written either as option keys such as :profile or as
names such as "profile" / "--profile". They are evaluated after CLI values,
environment defaults, and literal defaults are resolved.
When a relation target points at a hidden option, parsing still honors the
relation but generated help omits that hidden target from public metadata, and
runtime relationship errors avoid printing the hidden option name.
:requires demands every listed target; when only one of several alternatives
must be present (such as authenticating with a token or a username and
password), declare :requires-any-of instead:
(cl-cli:make-option :name "login"
:kind :flag
:requires-any-of '(:token :username))Parsing signals cli-missing-any-of-options when none of the declared
alternatives are supplied, and help lists them as requires one of: --token, --username.
For requirements that depend on other options, use :required-if (the option
becomes mandatory when any listed option is present) or :required-unless (it
is mandatory unless any listed option is present). Both signal
cli-missing-option-value and render in help as required if: ... /
required unless: ...:
(list
(cl-cli:make-option :name "profile" :kind :value)
(cl-cli:make-option :name "config" :kind :value :required-if '(:profile))
(cl-cli:make-option :name "token" :kind :value)
(cl-cli:make-option :name "user" :kind :value :required-unless '(:token)))To make a set of options mutually exclusive (at most one may be supplied),
splice them through cl-cli:exclusive-group instead of hand-writing every
pairwise :conflicts-with. Each member gains the others as conflicts, so
exclusivity reuses the same validation and hidden-target-safe error messages:
:global-options (cl-cli:exclusive-group
(cl-cli:make-option :name "json" :kind :flag)
(cl-cli:make-option :name "yaml" :kind :flag)
(cl-cli:make-option :name "table" :kind :flag))Conflicts an option already declares are preserved, so a group member can still conflict with options outside the group.
When the choice is mandatory, use cl-cli:required-exclusive-group instead:
exclusivity is enforced as above, and parsing additionally fails with
Exactly one of ... when none of the members is supplied — expressing an
"exactly one of" obligation that pairwise :conflicts-with cannot.
For the opposite relationship — options meant to be used together, such as a
paired --host and --port — splice them through cl-cli:inclusive-group. If
any member is supplied, all must be (supplying none is fine); a partial set
signals cli-missing-dependent-option and help renders the members as
"all or none of":
:global-options (cl-cli:inclusive-group
(cl-cli:make-option :name "host" :kind :value)
(cl-cli:make-option :name "port" :kind :value))Options accept a :group label too, which sections them under their own heading
in help (and appears in JSON), mirroring command grouping:
(list
(cl-cli:make-option :name "output" :kind :value :group "Output")
(cl-cli:make-option :name "format" :kind :value :group "Output")
(cl-cli:make-option :name "token" :kind :value :group "Auth"))For larger CLIs, group related commands in app help without changing parsing:
(list
(cl-cli:make-command :name "compile"
:group "Build"
:description "Compile sources.")
(cl-cli:make-command :name "test"
:group "Build"
:description "Run tests.")
(cl-cli:make-command :name "doctor"
:group "Diagnostics"
:description "Inspect the environment."))When help text should stay aligned with README snippets or a manpage, attach examples to the app or command spec and let the generated help reuse them:
(cl-cli:make-app
:name "demo"
:examples '("demo compile src/main.lisp"
"demo test --filter smoke")
:commands
(list
(cl-cli:make-command
:name "compile"
:examples '("demo compile -o out.fasl src/main.lisp"))))cl-cli:command-by-name resolves both primary command names and aliases
case-insensitively, which is useful when custom commands need to reuse the
same lookup semantics as the built-in help command.
For script-style options such as --script FILE [ARGS...] [-- ...], keep the
opaque tail in a rest positional and normalize away a literal separator token
only when the downstream script runner should not see it:
(let* ((app (cl-cli:make-app
:name "cl-cc"
:global-options
(list (cl-cli:make-option :name "script"
:kind :value
:stop-parsing-p t))
:positionals
(list (cl-cli:make-positional :key :script-argv :rest-p t))))
(invocation
(cl-cli:parse-argv app
'("cl-cc" "--script" "ci/build.lisp"
"--" "--target" "release"))))
(values
(cl-cli:option-value invocation :script)
(cl-cli:strip-argv-separators
(cl-cli:positional-value invocation :script-argv))))Value-bearing short options also accept attached forms. This covers CLI shapes
such as -Lmain or -S/tmp/tmux.sock without special-case parsing:
(let* ((app (cl-cli:make-app
:name "cl-tmux"
:global-options
(list (cl-cli:make-option :name "socket"
:short #\S
:kind :value))))
(invocation
(cl-cli:parse-argv app '("cl-tmux" "-S/tmp/tmux.sock"))))
(cl-cli:option-value invocation :socket))
;; => "/tmp/tmux.sock"Use application-argv when a launcher such as SBCL or Nix inserts runtime
tokens before the real CLI arguments. It applies the library's default SBCL/Nix
runtime markers:
(cl-cli:application-argv
:argv (cl-cli:current-process-argv))
(cl-cli:application-argv
:argv (cl-cli:current-process-argv)
:separator "--")Use extract-application-argv directly when a consumer needs fully custom
runtime markers:
(cl-cli:extract-application-argv
:argv (cl-cli:current-process-argv)
:runtime-markers '("--no-userinit" "--end-toplevel-options"))cl-cli already covers the shared parser shapes used by the current in-house
CLIs it is intended to replace. The practical question is not "can it parse
options?" but "which API reproduces each existing command surface without local
parser forks?".
cl-cc needs a mix of ordinary subcommands and script-style execution:
compile-style commands with--flag,--key value,--key=value, and-o value- command dispatch on the first non-option token
--script FILE [ARGS...] [-- ...]where everything after the script path stays opaque- value kinds matching
:boolean,:value, and:optional-value
Use these cl-cli features:
make-commandfor first-token subcommand dispatchmake-optionwith:kind :flag,:kind :boolean,:kind :value, and:kind :optional-value:stop-parsing-p ton--scriptto preserve the opaque tailstrip-argv-separatorswhen the downstream script runner should not receive a literal--
Existing coverage:
- script tail preservation and separator normalization are exercised in
stop-parsing-option-preserves-remaining-arguments,stop-parsing-short-attached-value, andstop-parsing-script-mode-can-normalize-opaque-tailin tests/run-tests.lisp
cl-tmux needs launcher-aware argv handling and tmux-style attached short
values:
- runtime token stripping before application parsing
- attached short payloads such as
-Lmainand-S/tmp/tmux.sock - modes that forward the remaining argv tail verbatim
- root execution when no explicit subcommand token is present
Use these cl-cli features:
application-argvorextract-application-argvbeforerun-app- short value options with
:shortplus:kind :value :stop-parsing-p twhen a mode must forward the remaining tail unchanged- root
:handleror:default-commandfor no-subcommand entry points
Existing coverage:
- attached short value parsing is shown in the
-S/tmp/tmux.sockexample above - root/default dispatch is exercised in
default-command-dispatches-without-command-tokenin tests/run-tests.lisp
private-trade-fx needs a stricter "single binary" interface:
- flat value options without subcommand routing
- hard failures for unknown options and missing required values
- built-in
--help - preservation of arguments after
-- - option-local parsers for domain validation such as positive integers
Use these cl-cli features:
- app-level
:global-optionswithout defining commands - the default strict parser behavior for unknown and malformed input
- built-in help/version handling
strip-argv-separatorsonly when downstream logic should hide the separator:parseron options and positionals for domain-specific validation
Existing coverage:
- parser-hook validation is demonstrated in the positive-integer example below
- separator-preserving and separator-normalizing flows are covered in tests/run-tests.lisp
nshell needs a shell-like root entry point with a few special launch modes:
- root execution with no args
- built-in
--help,-h,--version, and-V -c COMMAND [ARGS...]- a leading script file followed by rest arguments
Use these cl-cli features:
- app-level
:handlerfor the zero-arg root path - built-in help/version flags
:stop-parsing-p ton-c- root positionals with a trailing
:rest-p tpositional for script argv
Existing coverage:
- root positional parsing is shown in the
script-runnerexample above - root/default dispatch behavior is covered in tests/run-tests.lisp
For the four target CLIs above, the remaining migration work is consumer-side
spec translation, not new parser primitives in cl-cli.
- representative specs for all four migration targets live in examples/consumer-migrations.lisp
- load the system with ASDF
- run tests/run-tests.lisp
- if you launch through SBCL, Nix, or a wrapper script, normalize argv first with
application-argv
For the common case of numeric or boolean domain validation, declare a :type
instead of writing a :parser lambda. Supported types are :integer,
:number, :float, :boolean, and the default :string; :min and :max
add inclusive bounds for numeric types. Both make-option and make-positional
accept them:
(cl-cli:make-option :name "jobs"
:kind :value
:type :integer
:min 1
:max 64
:description "Parallel job count.")
(cl-cli:make-positional :key :port
:type :integer
:min 1
:max 65535)A :type and an explicit :parser are mutually exclusive, :min/:max
require a numeric :type, and :min may not exceed :max; all three are
checked at make-* time. The resolved type and range appear in help metadata
(for example type: integer; range: 1..64). Type failures and out-of-range
values are reported as cl-cli:cli-invalid-option-value /
cl-cli:cli-invalid-positional-value, exactly like a failing :parser. Numeric
parsing binds *read-eval* off, so a crafted value can never execute code.
When a domain rule does not fit a built-in :type, option and positional
parsers remain the right place for custom validation:
(cl-cli:make-option
:name "count"
:kind :value
:parser (lambda (value)
(let ((number (parse-integer value)))
(unless (plusp number)
(error "Expected a positive integer."))
number)))Parser failures are reported as cl-cli:cli-invalid-option-value or
cl-cli:cli-invalid-positional-value, so run-app treats them as usage errors
instead of internal failures. Command handlers may return an integer to choose
the process exit code.
run-app returns 0 on success, 64 (EX_USAGE) for a cli-usage-error, and
70 (EX_SOFTWARE) for any other unhandled error — the BSD sysexits
conventions. Pass :usage-exit-code and/or :error-exit-code to match a
different policy (for example :usage-exit-code 2 to mirror argparse):
(cl-cli:run-app *app* :argv argv :usage-exit-code 2 :error-exit-code 1)option-value-source reports the provenance of an option value — one of
:command-line, :env, :config, or :default (or nil when the option was
never set). This is the analogue of clap's ArgMatches::value_source, and lets
a handler tell an explicit user choice apart from a fallback — the key to
"only override when the user actually set it" and to layered-config merges:
(lambda (invocation)
(when (eq (cl-cli:option-value-source invocation :output) :command-line)
(override-output (cl-cli:option-value invocation :output)))
0)Handlers can write through the invocation streams when run-app is used:
(lambda (invocation)
(format (cl-cli:invocation-stdout invocation) "ok~%")
0)Generate a static completion script directly from the app definition:
(write-string
(cl-cli:render-completion *app* "bash")
*standard-output*)Shell-specific renderers are also available:
(cl-cli:render-bash-completion *app*)
(cl-cli:render-zsh-completion *app*)
(cl-cli:render-fish-completion *app*)Like format, these renderers take an optional stream: called with no stream
they return the script as a string, and called with a stream they write to it
and return no values. Pass an explicit stream to avoid building an intermediate
string:
(cl-cli:render-completion *app* "bash" *standard-output*)If you want standard built-in subcommands, use
cl-cli:make-standard-commands. It returns help and version by default;
when the app omits a version string, version prints just the app name.
Set :include-completion-p t to add completion too:
(setf *app*
(cl-cli:make-app
:name "demo"
:commands (append
(cl-cli:make-standard-commands
:include-completion-p t)
(list ...))))Then users can install completion with:
eval "$($(command -v demo) completion bash)"For Zsh and Fish:
autoload -U compinit && compinit
source <($(command -v demo) completion zsh)
source <($(command -v demo) completion fish)Current built-in support is bash, zsh, fish, powershell (alias pwsh),
nushell (alias nu), and elvish. Generated completion offers command names,
their options, and positional values from a positional's :choices or
:completion-candidates. The bash, zsh, and fish completers descend the
full nested-subcommand tree (app remote add, with each level's accumulated
option scope); the remaining shells complete the top command level. Hidden
commands and hidden options are omitted from the generated script. Aliases are included in generated completion candidates.
Options declared with :choices also feed shell value completion candidates.
Use :completion-candidates when shell suggestions should be broader or
differently documented than parser validation. The PowerShell renderer emits a
Register-ArgumentCompleter -Native script block that suggests subcommands and
option tokens, switching to a subcommand's own options once that subcommand
appears on the line; the Nushell renderer emits an export extern module
covering subcommands and global option flags:
(cl-cli:render-powershell-completion *app*)
(cl-cli:render-nushell-completion *app*)For candidates that are only known at runtime (branch names, hostnames, records
in a database), attach a :complete function to an option or positional and add
the hidden callback command with make-standard-commands :include-dynamic-p t
(or make-complete-command):
(cl-cli:make-app
:name "demo"
:global-options (list (cl-cli:make-option
:name "branch"
:kind :value
:complete (lambda (partial)
(remove-if-not
(lambda (b) (eql 0 (search partial b)))
(list-git-branches)))))
:commands (cl-cli:make-standard-commands :include-dynamic-p t))All six generated completions — bash, zsh, fish, PowerShell, nushell, and
elvish — then call demo __complete branch <partial> at completion time and
offer whatever the function prints. (bash/zsh/fish and elvish shell out from the
completion function; PowerShell inspects the token before the cursor; nushell
attaches a per-flag custom completer.) Because the shell already knows which
slot it is completing, the callback only receives the option/positional key and
the partial word — the full command line is never re-parsed. A candidate may be
a plain string or a (value . description) cons; descriptions are emitted
tab-separated and shown by shells that support them.
cl-cli:render-complete-reply performs the same lookup directly if you wire the
callback yourself.
Beyond interactive --help, cl-cli can render offline reference documentation
directly from an app spec, reusing the same option/command metadata so the docs
never drift from the parser. Both renderers follow the completion renderers'
stream contract (no stream returns a string; a stream is written to and returns
no values):
(cl-cli:render-manpage *app*) ; a section-1 man page (roff)
(cl-cli:render-markdown *app*) ; GitHub-flavored Markdown reference
(cl-cli:render-json *app*) ; machine-readable spec for toolingThe man page covers NAME, SYNOPSIS, DESCRIPTION, OPTIONS, ARGUMENTS, COMMANDS,
EXIT STATUS, EXAMPLES, and an ENVIRONMENT section for env-backed options, plus
SEE ALSO / AUTHORS sections and a .TH date when the app declares :see-also /
:authors / :manual-date (it passes mandoc -T lint cleanly); the Markdown
output produces a title, a usage block,
option/argument tables, per-command sections, and examples. render-json emits
the declared spec — options, positionals, commands, and their :type, range,
delimiter, choices, and default metadata — as a minified JSON object that
external tooling can consume. Hidden options and commands are omitted from all
three.
To let users generate these themselves, add the built-in docs [FORMAT] command
(the documentation counterpart to completion [SHELL]). It defaults to man
and also accepts markdown and json:
(cl-cli:make-app
:name "demo"
:commands (append
(cl-cli:make-standard-commands :include-docs-p t)
(list ...)))demo docs man > demo.1
demo docs markdown > docs/demo.md
demo docs json > demo.schema.jsoncl-cli:render-docs dispatches to the two renderers by format name if you need
the same routing without the command wrapper.
Use :default-command when a subcommand app should dispatch a command even
without an explicit command token. Use root :positionals plus a root
:handler for script-style CLIs such as SCRIPT [ARGS...]. Conversely, pass
:require-command t to make-app when a subcommand is mandatory: parsing then
fails with cli-unknown-command (listing the available commands) if the root is
invoked without one.
A command may declare its own :subcommands, so it dispatches like a mini-app
(git remote add). The next non-option token after the command selects a
subcommand, and the parent command's options remain available to the whole
subtree; global options and counters accumulate across the entire path:
(cl-cli:make-app
:name "git"
:global-options (list (cl-cli:make-option :name "verbose" :short #\v :kind :count))
:commands
(list (cl-cli:make-command
:name "remote"
:options (list (cl-cli:make-option :name "porcelain" :kind :flag))
:subcommands
(list (cl-cli:make-command
:name "add"
:positionals (list (cl-cli:make-positional :key :name :required-p t)
(cl-cli:make-positional :key :url :required-p t))
:handler (lambda (invocation)
(format t "add ~A ~A~%"
(cl-cli:positional-value invocation :name)
(cl-cli:positional-value invocation :url))))))))git -v remote --porcelain add origin URL dispatches the add handler with the
global --verbose count and the parent --porcelain flag both visible. Nesting
works to arbitrary depth. invocation-command is the dispatched leaf command and
invocation-command-path is the full root-to-leaf chain. Command help renders a
path-qualified usage line (Usage: git remote add ...) and lists a command's
subcommands, and the man/Markdown/JSON renderers recurse through the tree. When a
parent command has subcommands but no handler, running it with no subcommand
token prints that command's help listing its subcommands. A mistyped subcommand
of a command that takes no positionals signals cli-unknown-command with a
suggestion. A command may also declare a :default-command naming one of its
subcommands to dispatch when no subcommand token is present, mirroring the
app-level :default-command.
Pass :allow-abbreviated-options t to make-app for GNU-style prefix matching,
where a unique unambiguous prefix of a long option stands in for the full name
(--verb for --verbose). An ambiguous prefix signals cli-unknown-option
listing the candidates, and an exact match always wins over a longer option that
merely shares the prefix. It is off by default to preserve strict exact-match
parsing.
Pass :expand-response-files t to make-app to expand a @path argument into
the whitespace-separated arguments read from that file before parsing (useful
when a command line grows past a shell's length limit). Expansion is recursive,
@@ yields a literal leading @, and a missing file signals a usage error:
(cl-cli:run-app (cl-cli:make-app :name "tool" :expand-response-files t ...)
:argv '("tool" "@args.txt"))print-app-help, print-command-help, and run-app accept :color t to wrap
headings and option/command names in ANSI styling, and :width N to word-wrap
descriptions to column N (continuation lines align under the description gutter).
Both accept :auto for terminal-aware detection so the common case needs no
manual probing:
:color :autodisables color whenNO_COLORis set, forces it on whenCLICOLOR_FORCEis set to anything but0, and otherwise enables it only when the target stream is a terminal (isatty; SBCL probes the real descriptor, other implementations fall back to the environment rules).:width :autoreads$COLUMNS, wrapping to that width when it is a positive integer and leaving output unwrapped otherwise.
Explicit t / nil / integer values still force the decision, and the defaults
stay nil (no styling, no wrapping) so nothing changes unless you opt in:
;; Follow the terminal and the NO_COLOR / CLICOLOR_FORCE / COLUMNS conventions:
(cl-cli:run-app *app* :argv (cl-cli:current-process-argv) :color :auto :width :auto)
;; Or force it, as before:
(cl-cli:run-app *app* :argv (cl-cli:current-process-argv) :color t :width 80)To suppress the built-in -h / --help flag entirely — for a CLI that manages
its own help or forwards --help to a wrapped tool — pass :auto-help nil to
make-app (a help command from make-standard-commands still works).
By default a token like -5 is parsed as a short-option cluster (and rejected
if 5 is not an option). Pass :allow-negative-numbers t to make-app so a
token that looks like a negative number (-5, -1.5) is instead kept as a
positional or option value — useful for numeric CLIs:
(cl-cli:make-app :name "calc" :allow-negative-numbers t
:positionals (list (cl-cli:make-positional :key :n :type :number)))All public symbols live in the cl-cli package.
Spec constructors — make-app, make-command, make-option,
make-positional, exclusive-group, required-exclusive-group,
inclusive-group.
Built-in commands — make-standard-commands (the aggregate), or the
individual make-help-command, make-version-command,
make-completion-command, make-docs-command, and make-complete-command (the
hidden dynamic-completion callback; render-complete-reply is its logic).
Parsing and dispatch — parse-argv returns an invocation object without
running handlers; run-app parses and dispatches, returning a process exit
code.
Help — print-app-help and print-command-help render help text directly
to a stream, independent of the built-in help command.
Shell completion — render-completion (shell name as a string) plus the
shell-specific render-bash-completion, render-zsh-completion,
render-fish-completion, render-powershell-completion,
render-nushell-completion, and render-elvish-completion.
Documentation — render-manpage, render-markdown, and render-json
render offline reference docs and machine-readable schema; render-docs
dispatches to them by format name ("man" / "markdown" / "json").
Runtime argv — current-process-argv, application-argv,
extract-application-argv, default-runtime-markers, and
strip-argv-separators normalize launcher-inserted tokens and -- separators.
Invocation accessors — option-value and positional-value read resolved
values; option-value-source reports where an option value came from
(:command-line / :env / :config / :default). invocation-app,
invocation-command, invocation-action, invocation-argv0,
invocation-raw-argv, invocation-global-options, invocation-command-options,
invocation-positionals, invocation-command-path, invocation-option-sources,
invocation-stdout, and invocation-stderr expose the rest of the parsed
invocation. command-by-name resolves a command spec by name or alias.
Spec accessors — every make-app / make-command / make-option /
make-positional keyword has a matching reader in the app-*, command-*,
and option-* families (for example app-commands, command-options,
option-required-p).
Conditions — usage errors subclass cli-usage-error; specification errors
signal cli-invalid-specification. Concrete conditions include
cli-unknown-option, cli-unknown-command, cli-missing-option-value,
cli-missing-dependent-option, cli-missing-any-of-options,
cli-conflicting-options, cli-missing-positional, cli-invalid-option-value,
cli-invalid-positional-value, and cli-unexpected-argument, each with
readers such as cli-error-message for structured handling.
cl-cli aims for feature parity with the mainstream CLI frameworks (clap, cobra, click, argparse) on everything that is fundamentally a parsing, dispatch, help, or completion concern. A few capabilities those frameworks offer are deliberately left to the application, because they are runtime-I/O or policy concerns rather than argument handling:
- Interactive prompting for missing values, hidden/password input, and
confirmation prompts (click's
prompt=/hide_input=). Reading a terminal with echo disabled is an I/O concern; clap and cobra defer it too. A handler can prompt frominvocation-stdout/invocation-stderras needed. - Filesystem-touching validators (existence/readability checks). Declare
intent with
:value-hint :file/:dirfor completion, and enforce with a custom:parser; whether a path must already exist is per-app policy. - Config-file parsing (TOML/YAML). The
:configlayer accepts an already-parsed plist and slots it between env vars and literal defaults (seeoption-value-source); choosing and reading the file format stays with the caller. - Stdin values, progress bars, and completion-script installation are pure
runtime behavior; the library gives you a
--token you can special-case, the streams to draw on, andrender-*-completionoutput to install where you like.
Everything else the reference frameworks expose — including terminal-aware color/width, value provenance, "did you mean" suggestions, and configurable exit codes — is built in and covered above.
sbcl --non-interactive --load tests/run-tests.lisp --eval '(cl-cli/tests:run-tests)' --quit
ecl --norc --load tests/run-tests.lisp --eval '(cl-cli/tests:run-tests)'
nix flake checkDevelopment workflow, change expectations, and verification requirements are documented in CONTRIBUTING.md.
Usage questions, bug-report expectations, and maintainer support boundaries are documented in SUPPORT.md.
User-visible release notes are tracked in CHANGELOG.md.
The maintainer release checklist is documented in RELEASING.md.
Private vulnerability handling expectations are documented in SECURITY.md.
MIT