Guide for AI agents working in the carapace-spec-clap repository.
A Rust library that generates carapace-spec YAML from clap Command definitions. It implements clap_complete::Generator so consumers call clap_complete::generate(Spec, &mut cmd, name, &mut stdout) to emit a YAML spec consumable by carapace shell completion.
cargo build # build the library
cargo test # run snapshot tests (primary test suite)
cargo run --example carapace_spec # print example spec to stdout
cargo run --example git # print a git-like spec to stdoutCI (.github/workflows/rust.yml) runs cargo build --verbose and cargo test --verbose. There is no separate lint job.
Cargo.toml ships with version = "0.1.0-PLACEHOLDER". The real version is substituted at release time by the CI workflow using sed on tag pushes (refs/tags/v*), which also runs cargo publish --allow-dirty. Do not commit a real version number into Cargo.toml — keep the 0.1.0-PLACEHOLDER token. GoReleaser is configured but builds are skipped (.goreleaser.yml has builds: - skip: true); release artifacts come from the crate publish only.
The crate is intentionally small — a single implementation module plus a thin lib.rs re-export:
src/lib.rs— declaresmod carapace_specand re-exportsSpec.src/carapace_spec.rs— all logic. Public API surface is theSpecunit struct (implementsclap_complete::Generator) plus serializable model structs (Command,Completion,Documentation).
Spec::generate(&self, cmd: &clap::Command, buf: &mut dyn Write)is the entry point invoked byclap_complete::generate.command_for(cmd)recursively walks the clapCommandtree into the crate's ownCommandmodel. Subcommands that are hidden (is_hide_set) are dropped at this step.filter_inherited_flags(&mut command, &mut inherited)runs after the tree is built and stripspersistentflagsfrom children when an ancestor already declared them, so each level only lists the persistent flags it newly introduces. It uses a borrow-then-restore pattern: flags added at this level are inserted into theinheritedmap before recursing into children andshift_remove-ed afterward, so siblings don't inherit each other's persistent flags.- Output is serialized with
yaml_serde(notserde_yaml) and prefixed with a fixed schema-header comment pointing athttps://carapace.sh/schemas/command.json.
The YAML shape is governed by serde attributes on the model structs and is what the carapace spec schema expects. Several conventions are easy to break:
- Flag signatures are built by
flag_signature:-s, --longwhen both exist,--longonly, or-sonly. Positional args are never flags. - Modifiers appended to the signature string (
modifier_for):=— flag takes values, norequire_equals.?— flag takes values andrequire_equals(true)is set.!— flag is required (is_required_set).&— flag is hidden and takes values. For hidden flags the code ensures a&is present even on aliases (hidden_modifier).*— action isAppendorCount(repeatable).- Order of these matters and is not intuitive:
&is pushed first, then!, then=/?, then*— so a fully-decorated hidden required repeatable flag withrequire_equalsyields&!?*. Seemodifier_forfor exact sequencing.
- Persistent vs. local flags:
flags_for(cmd, persistent)splits ona.is_global_set() == persistent. Global flags becomepersistentflags; non-global becomeflags. - Flag ordering:
sorted_argssorts by(get_long(), get_short()). The YAML map preserves insertion order becauseflags/persistentflagsuseindexmap::IndexMap, notBTreeMap/HashMap. If you change the map type you will break snapshot tests and the expected spec ordering. - Flag values — plain vs. extended notation (carapace-spec v1.8.0): each flag value is a
FlagValueenum serialized with#[serde(untagged)]. The generator emits:FlagValue::Plain(String)— the common case: just the description string. Used when the flag has nonargsand no default.FlagValue::Extended(ExtendedFlag)— a map withdescription,nargs, anddefaultfields. Triggered whennargs != 0ordefaultis non-empty (matching carapace-spec'sExtendedstruct and its marshal triggerf.Nargs != 0 || f.Default != "").- Selection happens in
flag_value_for.default_forguards onarg.get_action().takes_values()so only value-taking actions (Set/Append) can produce adefault; boolean/count/help/version actions don't expose defaults through clap'sget_default_values()anyway, but the guard keeps the intent explicit. nargs_formaps clap'snum_argsrange to the spec's integer: fixedn != 1→n, unbounded (max_values() == usize::MAX, e.g.num_args(1..)) →-1, otherwise0.default_forjoins multiple default values with,, matching how pflag'sstringSliceValue.Setsplits on comma for repeatable flags in carapace-spec. Only emitted for value-taking flags. Note: a non-repeatableSetflag with multiple defaults is an uncommon edge case — carapace-spec'sDefaultis a single string and itsStringflag does not split on comma, so the joined value is kept verbatim there.
- Completion sources, in priority order within each entry:
action_for(value_hint)first, thenvalues_for(arg)(possible values from the value parser). Possible values are rendered asname\thelpwhen help text exists (tab-separated). positionalvspositionalany: a positional whosenum_args().max_values() == usize::MAX(variadic, e.g.num_args(1..)) goes topositionalany; all others go topositional. Empty positional entries are filtered out.- ValueHint → carapace macro mapping lives in
action_for. Only a subset is mapped:AnyPath/FilePath/ExecutablePath→$files,DirPath→$directories,CommandName/CommandString→[$executables, $files],Username→$carapace.os.Users,Hostname→$carapace.net.Hosts.Url,EmailAddress,Other,Unknown, andCommandWithArgumentscurrently map to no action (empty vec) — adding support for them is a known extension point. - Aliases: visible aliases become regular flag entries with the same help; non-visible (hidden) aliases get the
&hidden modifier. Visible short aliases are inserted with their own signature; non-visible short aliases are only added if not already present under the visible form.flag_completions_formirrors this for completions keyed byarg_key(long, else short) and all aliases.
Every field on Command/Completion/Documentation uses #[serde(skip_serializing_if = …)] against is_empty/is_default/Vec::is_empty/Map::is_empty. When adding a field, follow this pattern or empty values will leak into the YAML and break snapshots. Command also has custom is_empty impls on Completion and Documentation (not derived). The ExtendedFlag struct follows the same convention (String::is_empty for description/default, a custom is_zero for the nargs i64).
Tests are snapshot tests using snapbox with the diff feature.
tests/carapace_spec.rs— one#[test]per fixture, each callingcommon::assert_matchesagainst a file intests/snapshots/*.yaml.tests/common.rs— builds clapCommandfixtures (basic_command,feature_sample_command,special_commands_command,quoting_command,aliases_command,sub_subcommands_command,value_hint_command,extended_notation_command) and definesassert_matches. Marked#![allow(dead_code)]because helpers are shared across test modules.- Snapshots live in
tests/snapshots/and are the source of truth. Each snapshot begins with the schema-header comment line.
snapbox is configured with action_env(snapbox::assert::DEFAULT_ACTION_ENV), which resolves to the SNAPSHOTS environment variable (not SNAPBOX_ACTION). To regenerate snapshots after an intentional output change:
SNAPSHOTS=overwrite cargo testWithout that env var, mismatches fail the test with a diff. Do not hand-edit snapshots — regenerate them so the YAML exactly matches what the generator emits (including ordering and quoting).
- Add a
pub fn <name>_command(name: &'static str) -> clap::Commandintests/common.rs. - Add a
#[test] fn <name>()intests/carapace_spec.rspointing atsnapshots/<name>.yaml. - Run with
SNAPSHOTS=overwrite cargo testto generate the initial snapshot, then review the diff before committing.
- Rust edition 2021.
clapis imported withdefault-features = false, features = ["std"]— keep it minimal; don't introduce dependencies on clap features that aren't enabled. - The crate is a library only (
[lib]with no explicit name, no[[bin]]). Executables live underexamples/and are illustrative, not shipped. indexmapis used specifically for insertion-order-preserving maps — do not swap it forstd::collections::HashMaporBTreeMap.yaml_serde(theyaml-serdecrate) is the serializer, notserde_yaml. Output formatting depends on its emitter behavior.- No comments are used in source beyond the single
#![allow(dead_code)]intests/common.rs; match that style.