- Rust 98.9%
- Just 1.1%
| .forgejo/workflows | ||
| .opencode | ||
| benches | ||
| docs | ||
| examples | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| clippy.toml | ||
| justfile | ||
| LICENSE | ||
| PERFORMANCE.md | ||
| README.md | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
Flag-rs - A Cobra-inspired CLI Framework for Rust
Flag-rs is a command-line interface (CLI) framework for Rust inspired by Go's Cobra library. It provides dynamic command completion, self-registering commands, and a clean, modular architecture for building sophisticated CLI applications.
Key Features
- Zero Dependencies - Pure Rust implementation with no external crates
- Multi-Level Nested Commands - Full support for commands like
myapp deploy statuswith unlimited nesting - Dynamic Runtime Completions - Generate completions based on runtime state (like kubectl)
- Working Zsh Subcommand Completion - Tab completion that actually works for nested commands in zsh, bash, and fish
- Self-Registering Commands - Commands can register themselves with parent commands
- Modular Architecture - Organize commands in separate files/modules
- Hierarchical Flag Inheritance - Global flags are available to all subcommands
- Idiomatic Error Handling - Uses standard Rust
Resulttypes - Colored Output - Beautiful help messages with ANSI color support (respects NO_COLOR and terminal detection)
Why Flag-rs?
Flag-rs solves specific pain points that other CLI frameworks struggle with:
- Dynamic completions that actually work - Query APIs, databases, or any runtime state at tab-press time, just like
kubectldoes when completing pod names - Reliable zsh subcommand completion - Multi-level commands like
myapp deploy statusget proper tab completion in various shells.
Why Not Flag-rs?
The Flag-rs implementation may be naive - the leading crate, Clap, is very well regarded and meets the needs of a huge knowledgeable user base. Choose clap if you need maximum stability,
extensive documentation, and don't require dynamic completions.
Installation
Add this to your Cargo.toml:
[dependencies]
flag-rs = "0.8"
Quick Start
use flag_rs::{Command, CommandBuilder, Context, Flag, FlagType, FlagValue, CompletionResult};
fn main() {
let app = CommandBuilder::new("myapp")
.short("A simple CLI application")
.long("This is a longer description of my application")
.flag(
Flag::new("verbose")
.short('v')
.usage("Enable verbose output")
.value_type(FlagType::Bool)
.default(FlagValue::Bool(false))
)
.subcommand(build_serve_command())
.build();
let args: Vec<String> = std::env::args().skip(1).collect();
if let Err(e) = app.execute(args) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
fn build_serve_command() -> Command {
CommandBuilder::new("serve")
.short("Start the server")
.flag(
Flag::new("port")
.short('p')
.usage("Port to listen on")
.value_type(FlagType::Int)
.default(FlagValue::Int(8080))
)
.run(|ctx| {
let port = ctx.flag("port")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(8080);
println!("Starting server on port {}", port);
Ok(())
})
.build()
}
Nested Commands with Tab Completion
Flag-rs fully supports multi-level command hierarchies with working tab completion at every level. This is especially valuable for zsh users who have experienced issues with other frameworks.
Building Nested Commands
Create commands like myapp deploy status and myapp deploy publish:
use flag_rs::{CommandBuilder, Context, CompletionResult};
fn main() {
let app = CommandBuilder::new("myapp")
.short("My application")
.subcommand(build_deploy_command())
.build();
let args: Vec<String> = std::env::args().skip(1).collect();
app.execute(args).unwrap();
}
fn build_deploy_command() -> Command {
CommandBuilder::new("deploy")
.short("Deployment commands")
.subcommand(
CommandBuilder::new("status")
.short("Check deployment status")
.arg_completion(|ctx, prefix| {
// Tab completion for deployment names
let deployments = vec!["web-api", "worker", "database"];
Ok(CompletionResult::new()
.extend(deployments.into_iter()
.filter(|d| d.starts_with(prefix))
.map(String::from)
.collect()))
})
.run(|ctx| {
let deployment = ctx.args().first()
.ok_or("Deployment name required")?;
println!("Status of {}: Running", deployment);
Ok(())
})
.build()
)
.subcommand(
CommandBuilder::new("publish")
.short("Publish a deployment")
.run(|ctx| {
println!("Publishing deployment...");
Ok(())
})
.build()
)
.build()
}
Tab Completion Behavior
With the above setup:
myapp <TAB>→ showsdeploy(and any other root commands)myapp deploy <TAB>→ showsstatus,publishmyapp deploy status <TAB>→ showsweb-api,worker,database
This works correctly in bash, zsh, and fish shells.
Arbitrary Nesting Depth
The architecture supports unlimited nesting levels:
CommandBuilder::new("myapp") // Level 1
.subcommand(
CommandBuilder::new("cloud") // Level 2
.subcommand(
CommandBuilder::new("compute") // Level 3
.subcommand(
CommandBuilder::new("instances") // Level 4
.subcommand(
CommandBuilder::new("list") // Level 5
.run(|ctx| { /* ... */ })
.build()
)
.build()
)
.build()
)
.build()
)
.build()
Results in: myapp cloud compute instances list with tab completion at each level.
Dynamic Completions
The killer feature of Flag-rs is dynamic completions. Unlike static completions, these run at completion time and can return different values based on current state:
CommandBuilder::new("get")
.arg_completion(|ctx, prefix| {
// This runs when the user presses TAB!
let namespace = ctx.flag("namespace").unwrap_or("default");
// In a real app, you'd query an API here
let items = fetch_items_from_api(namespace);
Ok(CompletionResult::new()
.extend(items.into_iter()
.filter(|item| item.starts_with(prefix))
.collect::<Vec<_>>()))
})
.build()
Completions with Descriptions
Flag-rs supports rich completions with descriptions, similar to Cobra. When descriptions are provided, they are handled appropriately by each shell:
.flag_completion("environment", |_ctx, prefix| {
Ok(CompletionResult::new()
.add_with_description("dev", "Development environment - safe for testing")
.add_with_description("staging", "Staging environment - production mirror")
.add_with_description("prod", "Production environment - BE CAREFUL!"))
})
Shell-specific behavior:
- Bash: Shows only the completion values (descriptions not supported natively)
- Zsh: Shows descriptions using native format:
value:description - Fish: Shows descriptions using tab-separated format:
value[TAB]description
For Zsh and Fish, descriptions appear alongside completions in the shell's native format, providing helpful context when selecting options.
Completion Caching
For expensive completion operations (API calls, file system scans), use the built-in caching mechanism:
use flag_rs::completion_cache::CompletionCache;
use std::time::Duration;
use std::sync::Arc;
let cache = Arc::new(CompletionCache::new(Duration::from_secs(5)));
CommandBuilder::new("get")
.arg_completion({
let cache = Arc::clone(&cache);
move |ctx, prefix| {
let key = CompletionCache::make_key(
&["get".to_string()],
prefix,
ctx.flags()
);
// Check cache first
if let Some(cached) = cache.get(&key) {
return Ok(cached);
}
// Expensive operation
let result = fetch_from_api()?;
// Cache the result
cache.put(key, result.clone());
Ok(result)
}
})
.build()
Completion Timeouts
Protect against slow completion functions that could hang the shell:
use flag_rs::completion_timeout::make_timeout_completion;
use std::time::Duration;
CommandBuilder::new("search")
.arg_completion(make_timeout_completion(
Duration::from_millis(100),
|ctx, prefix| {
// This will timeout if it takes > 100ms
search_database(prefix)
}
))
.build()
Modular Command Structure
For larger applications, Flag supports a modular structure where commands register themselves:
// src/cmd/mod.rs
pub fn register_commands(root: &mut Command) {
serve::register(root);
deploy::register(root);
status::register(root);
}
// src/cmd/serve.rs
pub fn register(parent: &mut Command) {
let cmd = CommandBuilder::new("serve")
.short("Start the server")
.run(|ctx| {
// Implementation
Ok(())
})
.build();
parent.add_command(cmd);
}
Shell Completions
Flag-rs generates working completion scripts for bash, zsh, and fish. Unlike some frameworks, nested subcommands work correctly in zsh.
Adding Completion Support
// Add a completion command to your CLI
CommandBuilder::new("completion")
.short("Generate shell completion scripts")
.run(|ctx| {
let shell = ctx.args().first()
.ok_or("Shell name required")?;
let script = match shell.as_str() {
"bash" => app.generate_completion(Shell::Bash),
"zsh" => app.generate_completion(Shell::Zsh),
"fish" => app.generate_completion(Shell::Fish),
_ => return Err("Unsupported shell"),
};
println!("{}", script);
Ok(())
})
.build()
Enabling Completions
Users can enable completions in their shell:
# Bash
source <(myapp completion bash)
# Zsh - works correctly for nested subcommands!
source <(myapp completion zsh)
# Fish
myapp completion fish | source
What Works
All shells support:
- ✅ Root command completion
- ✅ Nested subcommand completion (including zsh!)
- ✅ Flag completion with descriptions (zsh and fish show descriptions)
- ✅ Dynamic argument completion
- ✅ Flag value completion
The completion system navigates the full command hierarchy, so typing myapp deploy <TAB> correctly shows subcommands at that level, even in zsh.
Flag Types
Flag-rs supports multiple value types:
String- Text valuesBool- Boolean flagsInt- Integer valuesFloat- Floating point valuesStringSlice- Multiple string valuesStringArray- Array of string valuesChoice(Vec<String>)- Enumerated choices with validationRange(i64, i64)- Integer range validationFile- File path validationDirectory- Directory path validation
Advanced Flag Features
Flag Constraints
Flag-rs supports advanced flag relationships:
use flag_rs::{Flag, FlagConstraint};
// Required if another flag is set
Flag::new("cert")
.value_type(FlagType::File)
.constraint(FlagConstraint::RequiredIf("tls".to_string()))
// Conflicts with other flags
Flag::new("json")
.value_type(FlagType::Bool)
.constraint(FlagConstraint::ConflictsWith(vec!["yaml".to_string(), "xml".to_string()]))
// Requires other flags
Flag::new("ssl-verify")
.value_type(FlagType::Bool)
.constraint(FlagConstraint::Requires(vec!["ssl".to_string()]))
Choice Validation
Flag::new("environment")
.value_type(FlagType::Choice(vec![
"dev".to_string(),
"staging".to_string(),
"prod".to_string()
]))
Range Validation
Flag::new("workers")
.value_type(FlagType::Range(1, 100))
.default(FlagValue::Int(4))
Error Handling
Flag uses idiomatic Rust error handling with no forced dependencies:
pub enum Error {
CommandNotFound(String),
SubcommandRequired(String),
FlagParsing(String),
ArgumentParsing(String),
ValidationError(String),
Completion(String),
Io(std::io::Error),
Custom(Box<dyn std::error::Error + Send + Sync>),
}
Examples
See the examples/ directory for complete examples:
kubectl.rs- A kubectl-like CLI demonstrating three-level nested commands (kubectl get pods) with dynamic completions and working zsh tab completionkubectl_modular/- A modular kubectl implementation showing command self-registration across multiple filesadvanced_flags_demo.rs- Demonstrates advanced flag types and constraintscaching_demo.rs- Shows completion caching for expensive operationstimeout_demo.rs- Demonstrates timeout protection for slow completionsmemory_optimization_demo.rs- Shows memory optimization techniquesbenchmark.rs- Performance benchmarking suite
Quick test of nested commands with completion:
# Build the kubectl example
cargo build --example kubectl
# Try the three-level command structure
./target/debug/examples/kubectl get pods nginx-7fb96c846b-8xvnl
# Generate and test zsh completion
source <(./target/debug/examples/kubectl completion zsh)
kubectl get <TAB> # Shows: pods, services, deployments
kubectl get pods <TAB> # Shows actual pod names with dynamic completion
Performance & Memory Optimization
Flag-rs includes several performance optimizations for large-scale CLI applications:
String Interning
Reduce memory usage for repeated strings:
use flag_rs::string_pool;
// Intern frequently used strings
let cmd_name = string_pool::intern("kubectl");
let flag_name = string_pool::intern("namespace");
Optimized Completions
Use memory-efficient completion structures:
use flag_rs::completion_optimized::{CompletionResultOptimized, CompletionItem};
use std::borrow::Cow;
CompletionResultOptimized::new()
.add_with_description(
Cow::Borrowed("static-value"), // No allocation
Cow::Borrowed("Static description")
)
Performance Characteristics
Based on our benchmarks:
| Operation | Performance | Notes |
|---|---|---|
| Simple command creation | ~500 ns | 2 flags |
| Complex CLI (50 subcommands) | ~150 μs | 500 total commands |
| Flag parsing | ~2 μs | Per flag |
| Subcommand lookup | ~50 ns | O(1) HashMap |
| Dynamic completion | ~15 μs | 100 items |
| Cached completion | ~1 μs | Cache hit |
See examples/benchmark.rs for detailed performance measurements.
Design Philosophy
Flag is designed to be a foundational library with zero dependencies. This ensures:
- Fast compilation times
- No dependency conflicts
- Maximum flexibility for users
- Minimal binary size
Users can add their own dependencies for features like colored output, async runtime, or configuration file support without conflicts.
Development
The justfile is the single source of truth for build/test/lint. CI calls
just ci and nothing else, so running it locally exactly matches CI:
just ci # fmt-check + lint + test + build (run this before pushing)
just fmt # apply formatting
just test # cargo test --locked --workspace --all-targets
just lint # strict clippy (pedantic + nursery + cargo + unwrap/expect_used)
just stats # LOC, largest files, module tree (needs scc + cargo-modules)
just --list # show all recipes
The Rust toolchain is pinned in rust-toolchain.toml (currently 1.95.0). The
same version is pinned in the CI workflow — both must agree.
Continuous Integration
CI runs on Forgejo Actions (.forgejo/workflows/ci-linux.yml) on every PR to
main and on manual dispatch. It checks out the repo, installs just and the
pinned Rust toolchain, then runs just ci.
Publishing Releases
Releases run on Forgejo Actions (.forgejo/workflows/release.yml) and are
triggered by pushing a v* tag. The workflow rewrites the version in
Cargo.toml to match the tag (with the v stripped), commits the bump to
main, runs the test suite, then publishes to crates.io.
git tag v0.9.0
git push origin v0.9.0
That's it. The tag's v is stripped before anything touches Cargo.toml or
crates.io, so the published version is plain semver (0.9.0) — continuous
with the existing publish history.
Required Forgejo repo secrets (Settings → Actions → Secrets):
PAT— Forgejo personal access token withwrite:repositoryscope. Needed so the workflow can push the version-bump commit back tomain.CRATES_IO_TOKEN— API token from https://crates.io/settings/tokens.
License
MIT
Developer env
source <(./target/debug/examples/kubectl completion zsh)