Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,68 @@
# Changelog

This project will record user-visible library changes here once tagged releases
begin.
All notable user-visible changes to this project are documented here.

## Unreleased
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Added contributor, support, and release-process documentation.
## [Unreleased]

### Added

- `:requires-any-of` on `make-option`: declares that at least one of a set of
alternative options must also be supplied (unlike `:requires`, which demands
all of them). Signals the new `cli-missing-any-of-options` condition and
renders as `requires one of: ...` in help. `make-app` rejects a
`:requires-any-of` whose every alternative conflicts with the option itself,
since such an option could never be validly supplied.
- Contributor, support, and release-process documentation.

### Changed

- Completion renderers (`render-completion`, `render-bash-completion`,
`render-zsh-completion`, `render-fish-completion`) now return the generated
script as a string when called without a stream, mirroring the `format`
destination contract; passing a stream still writes to it and returns no
values. Previously the no-stream form wrote to `*standard-output*` and
returned no values, so there was no way to obtain the script as a string
through the public API.
- Expanded package metadata for ASDF consumers and package indexes.
- Verified the current test suite on both SBCL and ECL through the Nix flake checks.
- Documented the `cl-prolog` git-dependency install path, since `cl-cli` is not
yet resolvable through a bare Quicklisp `quickload`.
- Option `:requires`/`:conflicts-with` validation now reuses the Prolog
rulebase built once at `make-app` time instead of rebuilding and re-querying
it on every `parse-argv` call.

### Fixed

- Bash completion never offered attached-value candidates (`--option=value`)
for command-scoped options.
- A short-option cluster silently dropped characters that followed a
mid-cluster `:stop-parsing-p` flag or boolean instead of surfacing them as
positional input.
- An `:optional-value` option configured with `:consume-optional-value-p t`
could not accept a separated bare `-` (the stdin/stdout idiom) as its value.
- `make-app` no longer accepts a positional sequence with a required positional
after an optional one, since such a spec could never assign the later
positional correctly.
- `make-app` now rejects two options whose keys collide (e.g. case-differing
single-character names like `-a`/`-A`) instead of letting them silently share
one storage slot.
- `make-app` now rejects a user-declared option that reuses the reserved
`:help`/`:version` key, which previously could hijack CLI dispatch into the
help/version action.
- `extract-application-argv`/`application-argv` no longer strip a literal
application argument that happens to match a runtime marker when it appears
after the `--` separator.
- `make-option`/`make-positional` now validate identifier safety before
interning the option/positional key, instead of after.
- A relation rulebase cached on a shared, reused `command-spec` (per the
library's own "reusable command spec" pattern) could be silently overwritten
by a second, unrelated `make-app` call that spliced in the same command
object, corrupting validation for the first app. The cache now lives per-app,
keyed by command object, instead of being stored on the shared command
struct.
- Corrected broken absolute-path documentation links and gave `SECURITY.md` a
concrete private vulnerability-reporting path.

[Unreleased]: https://github.com/takeokunn/cl-cli/commits/main
125 changes: 109 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,38 @@ minimal Common Lisp and Nix environments.

## Installation

With Quicklisp / ASDF:
`cl-cli` depends on [`cl-prolog`](https://github.com/takeokunn/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.

```lisp
(ql:quickload :cl-cli)
### With Nix (recommended)

The flake wires up `cl-prolog` and `cl-weave` for you:

```bash
nix develop # drop into a shell with all dependencies available
nix flake check # run the test suite across sbcl and ecl
```

With Nix:
### With ASDF / Quicklisp

Clone `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:

```bash
nix develop
git clone https://github.com/takeokunn/cl-prolog ~/common-lisp/cl-prolog
git clone https://github.com/takeokunn/cl-cli ~/common-lisp/cl-cli
```

```lisp
(ql:quickload :cl-cli)
```

Running the test suite additionally requires
[`cl-weave`](https://github.com/takeokunn/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.
Expand Down Expand Up @@ -123,6 +143,10 @@ Help output is context-sensitive:
- 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`/`:version` key
- unknown commands and options suggest the nearest known spelling when the typo is close enough
- `--version` is only shown in help when the app has a version string

Expand Down Expand Up @@ -194,6 +218,20 @@ 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:

```lisp
(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`.

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
Expand Down Expand Up @@ -334,7 +372,7 @@ Use these `cl-cli` features:

Existing coverage:

- script tail preservation and separator normalization are exercised in `stop-parsing-option-preserves-remaining-arguments`, `stop-parsing-short-attached-value`, and `stop-parsing-script-mode-can-normalize-opaque-tail` in [tests/run-tests.lisp](/Users/take/ghq/github.com/takeokunn/cl-cli/tests/run-tests.lisp)
- script tail preservation and separator normalization are exercised in `stop-parsing-option-preserves-remaining-arguments`, `stop-parsing-short-attached-value`, and `stop-parsing-script-mode-can-normalize-opaque-tail` in [tests/run-tests.lisp](tests/run-tests.lisp)

### `cl-tmux`

Expand All @@ -356,7 +394,7 @@ Use these `cl-cli` features:
Existing coverage:

- attached short value parsing is shown in the `-S/tmp/tmux.sock` example above
- root/default dispatch is exercised in `default-command-dispatches-without-command-token` in [tests/run-tests.lisp](/Users/take/ghq/github.com/takeokunn/cl-cli/tests/run-tests.lisp)
- root/default dispatch is exercised in `default-command-dispatches-without-command-token` in [tests/run-tests.lisp](tests/run-tests.lisp)

### `private-trade-fx`

Expand All @@ -379,7 +417,7 @@ Use these `cl-cli` features:
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](/Users/take/ghq/github.com/takeokunn/cl-cli/tests/run-tests.lisp)
- separator-preserving and separator-normalizing flows are covered in [tests/run-tests.lisp](tests/run-tests.lisp)

### `nshell`

Expand All @@ -400,16 +438,16 @@ Use these `cl-cli` features:
Existing coverage:

- root positional parsing is shown in the `script-runner` example above
- root/default dispatch behavior is covered in [tests/run-tests.lisp](/Users/take/ghq/github.com/takeokunn/cl-cli/tests/run-tests.lisp)
- root/default dispatch behavior is covered in [tests/run-tests.lisp](tests/run-tests.lisp)

### Verification path

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](/Users/take/ghq/github.com/takeokunn/cl-cli/examples/consumer-migrations.lisp)
- representative specs for all four migration targets live in [examples/consumer-migrations.lisp](examples/consumer-migrations.lisp)
- load the system with ASDF
- run [tests/run-tests.lisp](/Users/take/ghq/github.com/takeokunn/cl-cli/tests/run-tests.lisp)
- run [tests/run-tests.lisp](tests/run-tests.lisp)
- if you launch through SBCL, Nix, or a wrapper script, normalize argv first with `application-argv`

## Validation and exit codes
Expand Down Expand Up @@ -458,6 +496,15 @@ Shell-specific renderers are also available:
(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:

```lisp
(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.
Expand Down Expand Up @@ -500,6 +547,52 @@ 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...]`.

## Public API reference

All public symbols live in the `cl-cli` package.

**Spec constructors** — `make-app`, `make-command`, `make-option`,
`make-positional`, `exclusive-group`, `required-exclusive-group`.

**Built-in commands** — `make-standard-commands` (the aggregate), or the
individual `make-help-command`, `make-version-command`, and
`make-completion-command`.

**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`, and
`render-fish-completion`.

**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; `invocation-app`, `invocation-command`, `invocation-action`,
`invocation-argv0`, `invocation-raw-argv`, `invocation-global-options`,
`invocation-command-options`, `invocation-positionals`, `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.

## Test

```bash
Expand All @@ -511,27 +604,27 @@ nix flake check
## Contributing

Development workflow, change expectations, and verification requirements are
documented in [CONTRIBUTING.md](/Users/take/ghq/github.com/takeokunn/cl-cli/CONTRIBUTING.md).
documented in [CONTRIBUTING.md](CONTRIBUTING.md).

## Support

Usage questions, bug-report expectations, and maintainer support boundaries are
documented in [SUPPORT.md](/Users/take/ghq/github.com/takeokunn/cl-cli/SUPPORT.md).
documented in [SUPPORT.md](SUPPORT.md).

## Changelog

User-visible release notes are tracked in
[CHANGELOG.md](/Users/take/ghq/github.com/takeokunn/cl-cli/CHANGELOG.md).
[CHANGELOG.md](CHANGELOG.md).

## Releasing

The maintainer release checklist is documented in
[RELEASING.md](/Users/take/ghq/github.com/takeokunn/cl-cli/RELEASING.md).
[RELEASING.md](RELEASING.md).

## Security

Private vulnerability handling expectations are documented in
[SECURITY.md](/Users/take/ghq/github.com/takeokunn/cl-cli/SECURITY.md).
[SECURITY.md](SECURITY.md).

## License

Expand Down
23 changes: 13 additions & 10 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ against the latest commit on the default branch.

Do not open a public issue for a suspected security problem.

Report it privately to the maintainers with:

- a description of the issue
- impact and expected attack conditions
- a minimal reproduction if available
- any proposed mitigation or patch

If you do not have a private contact path yet, establish one before publishing a
release from this repository. Until then, treat the repository as not ready for
private vulnerability intake.
Report it privately through GitHub's private vulnerability reporting:

1. Go to <https://github.com/takeokunn/cl-cli/security/advisories/new>
(also reachable from the repository's **Security** tab → **Report a
vulnerability**).
2. Include:
- a description of the issue
- impact and expected attack conditions
- a minimal reproduction if available
- any proposed mitigation or patch

GitHub keeps the report private between you and the maintainers until an
advisory is published.

## Response expectations

Expand Down
2 changes: 1 addition & 1 deletion SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ includes:
## Security issues

Do not use public issues for suspected vulnerabilities. Follow
[SECURITY.md](/Users/take/ghq/github.com/takeokunn/cl-cli/SECURITY.md).
[SECURITY.md](SECURITY.md).

## Maintenance expectations

Expand Down
14 changes: 10 additions & 4 deletions src/completion-renderers-bash.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
(format out " case \"${words[1]}\" in~%")
(format out " ~A)~%"
(%completion-case-labels (%completion-command-names command)))
(format out " case \"$cur\" in~%")
(format out " case \"~A:$cur\" in~%" command-name)
(write-string (%completion-bash-value-case-body options
:command-name command-name)
out)
Expand All @@ -75,9 +75,15 @@
(format out " esac~%"))))

(defun render-bash-completion (app &optional stream)
"Render a bash completion script."
(let ((stream (or stream *standard-output*))
(function-name (%completion-function-name app))
"Render a bash completion script.

With no STREAM, return the completion script as a string. With a STREAM,
write the script to it and return no values."
(unless stream
(return-from render-bash-completion
(with-output-to-string (string-stream)
(render-bash-completion app string-stream))))
(let ((function-name (%completion-function-name app))
(app-name (app-name app)))
(format stream "#!/usr/bin/env bash~%")
(format stream "# bash completion for ~A~%" app-name)
Expand Down
12 changes: 9 additions & 3 deletions src/completion-renderers-fish.lisp
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
(in-package :cl-cli)

(defun render-fish-completion (app &optional stream)
"Render a fish completion script."
(let ((stream (or stream *standard-output*))
(app-name (app-name app)))
"Render a fish completion script.

With no STREAM, return the completion script as a string. With a STREAM,
write the script to it and return no values."
(unless stream
(return-from render-fish-completion
(with-output-to-string (string-stream)
(render-fish-completion app string-stream))))
(let ((app-name (app-name app)))
(format stream "complete -c ~A -f~%" (%completion-shell-quote app-name))
(dolist (command (%completion-visible-commands app))
(format stream "complete -c ~A -n ~A -a ~A -d ~A~%"
Expand Down
Loading
Loading