CMDkit
CMDkit is a small, implementation-first Rust framework for building command-line tools.
It is designed around three ideas:
- explicit command trees
- instance-owned runtime state
- strategy-based command execution
That makes it a good fit for CLIs that need nested routing, testable dispatch, and predictable parsing without process-global state.
Installation
Highlights
- Register commands with
Command::new(...)or fluentcommand(...).build(). - Attach handlers as structs (
CommandStrategy) or closures (handler_fn/Command::from_fn). - Compose nested command hierarchies with subcommands.
- Parse command input into three channels:
options: Vec<Switch>for switch/flag inputsarguments: Vec<Argument>for value-bearing inputsparams: Vec<String>for remaining positional parameters
- Customize help output via
HelpRenderer. - Configure lock-poison behavior with
CoreConfig.
Core API
Runtime
CliCore::new()creates a runtime with default config.CliCore::create(config)uses customCoreConfig.register,get, andget_allmanage command registration.try_run_from_args(&[String])is ideal for tests and embedding.run_with_commandsandtry_run_with_commandsare convenience wrappers.
Each CliCore instance owns its own registry. Runtime state is not shared across instances.
Command Construction
Command::new(name, description, strategy)Command::from_fn(name, description, closure)command(name, description)fluent builder:.handler(...).handler_fn(...).subcommand(...).with_usage(...).with_long_description(...).with_examples(...).with_options(...).with_arguments(...).with_aliases(...).build()
Metadata Declarations
CMDkit metadata separates value-taking inputs from switch-like inputs:
switch(...)/Switch: declares switch/flag inputsargument(...)/Argument: declares value-bearing inputs
Both support aliases.
Quick Start
use ;
;
Nested Command Trees
Nested trees can be built directly with the fluent builder:
use ;
let core = new;
core.register;
Routing commands forward execution to leaf commands. The selected leaf strategy receives parsed input.
Parser Behavior
For an invocation like:
app create --name demo --language rust --dry-run
the strategy receives:
- an
Argument { name: "name", value: Some("demo") } - an
Argument { name: "language", value: Some("rust") } - an
optionsentry withSwitch { name: "dry-run", ... }
Supported forms include:
--key value--key=value- aliases declared in metadata
Unknown flags are rejected with StrategyErrorKind::InvalidArguments.
Strategy Token Semantics
For try_run_from_args, CMDkit applies deterministic forwarding rules:
argv[1]selects the top-level command only.- The selected command receives and parses
argv[2..]. - Parsing at each command level stops at the first token that matches a declared subcommand name or alias.
- That boundary token and the remaining tail are forwarded to subcommand routing.
- Any non-flag tokens seen before the boundary stay in
paramsat the current command level. - After a subcommand boundary, parsing responsibility shifts to the selected child command.
Practical implication: if you pass tool run --mode fast, the --mode token is parsed by run (the child), not by tool (the parent).
Help Rendering
Default help is plain text via PlainTextHelpRenderer and includes recursively discovered subcommands.
Trigger help with:
<binary> help
Or rely on the generated help from MissingCommand / UnknownCommand errors.
You can provide a custom renderer:
use ;
;
Runtime Configuration
use ;
let config = new
.with_lock_poison_policy;
let core = create;
LockPoisonPolicy values:
FailFast(default): panic when registry lock is poisonedRecover: recover poisoned lock state and continue
Error Model
CliCoreErrorfor dispatch/runtime-level failures:MissingCommandUnknownCommandStrategyExecution
StrategyErrorfor command handler failures withStrategyErrorKind:InvalidArgumentsExecutionInternal
CliCoreError::StrategyExecution preserves the originating StrategyError as source.
Testing and Embedding
Use try_run_from_args to test dispatch deterministically:
use ;
License
This project is licensed under GPL-3.0-or-later.