Skip to content

Getting Started

Installing

cl-cli itself depends on uiop (ships with every modern ASDF) on every implementation but SBCL, and additionally on cl-host-kit on SBCL. Cloning it where ASDF can find both (for example under ~/common-lisp/) is enough to load it:

git clone https://github.com/nerima-lisp/cl-cli  ~/common-lisp/cl-cli
(asdf:load-system "cl-cli")

The flake wires up cl-prolog-kit, cl-weave, cl-process-kit (plus its own cl-boundary-kit / cl-log-kit / cl-codec-kit dependencies), cl-json-kit, cl-concurrent-kit, and cl-host-kit for you. The test dependencies are only needed to run the test suite; cl-host-kit is also a runtime dependency when cl-cli is loaded by SBCL. The optional cl-cli/concurrent system uses cl-concurrent-kit on SBCL:

nix develop        # drop into a shell with all dependencies available
nix flake check    # run the sbcl and ecl suites and build the docs

The flake declares x86_64-linux (the CI platform) and aarch64-darwin (a development output). On another host, run them inside a builder for one of those systems; there are no flake outputs for other systems. See Compatibility. The flake is generated by cl-nix-forge's mkPackageFlake, and also brings in paredit-cli for nix develop — both are development-only inputs and do not affect a consumer's own flake.

Clone cl-prolog-kit, cl-weave, and cl-json-kit where ASDF can find them, the same way as cl-cli above, then run run-tests.lisp at the repository root:

sbcl --script run-tests.lisp

Adding cl-process-kit, cl-boundary-kit, cl-date-kit, cl-concurrent-kit, cl-log-kit, and cl-codec-kit alongside them enables the extra suite that runs generated scripts through the real shells.

Both the sbcl and ecl checks must be green. They do not run the same thing: the shell-verification half of the suite needs cl-process-kit, whose own cl-log-kit dependency is SBCL-only (nerima-lisp/cl-log-kit#1), so ECL runs the portable core. The runner prints which half it ran. See Compatibility for the full matrix.

Your first app spec

(asdf:load-system "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* :argv '("demo" "compile" "-o" "out.bin" "input.lisp"))

make-app builds an immutable spec — name, version, global options, and a list of commands. run-app parses an argv list against that spec and dispatches the matched command's :handler, returning a process exit code. Call parse-argv directly instead when you want the parsed invocation without running a handler (useful for tests); note that it takes argv as a required positional argument — (cl-cli:parse-argv *app* '("demo" "compile" "input.lisp")) — rather than as :argv.

Concurrent parsing on SBCL

The optional cl-cli/concurrent system keeps the portable parser unchanged while parsing independent argv batches with a bounded worker pool:

(asdf:load-system "cl-cli/concurrent")

(cl-cli/concurrent:parse-argv-batch
 *app*
 '(("demo" "compile" "one.lisp")
   ("demo" "compile" "two.lisp"))
 :parallelism 2
 :max-in-flight 2)

Each element is a complete argv list. Results preserve input order, while :parallelism controls workers and :max-in-flight bounds admitted requests. Parser errors are propagated after submitted requests settle.

Reusable specs with the DSL

For named reusable building blocks, the DSL macros expand to the same make-* constructors shown above. Use define-app, define-command, define-option, and define-positional when the shape of a spec is known at read or compile time:

(cl-cli:define-app *dsl-app*
    (:name "dsl-demo" :version "0.1.0")
  (:option "verbose" :short #\v :kind :flag)
  (:command "compile" (:description "Compile a source file.")
    (:positional :input :required-p t)))

(cl-cli:run-app *dsl-app*
               :argv '("dsl-demo" "--verbose" "compile" "input.lisp"))

The functional make-* API remains the direct choice for dynamically assembled specs; both forms produce the same immutable model objects.

Root positional example

Not every CLI needs subcommands. Attach :positionals and a :handler directly to the app for script-style tools that dispatch on positional arguments alone:

(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)))))

Built-in help and version

Every app gets --help / -h for free, and --version / -V once the app declares a :version string:

$ sbcl --script demo.lisp -- --help
Usage: demo [global-options] <command> [args]
...

If you want help and version as real subcommands instead of (or in addition to) the built-in flags, splice in cl-cli:make-standard-commands. It returns help and version by default; completion and docs are opt-in via :include-completion-p t / :include-docs-p t — see Shell Completion and Documentation Generation.

Where to go next