| Crates.io | unilang |
| lib.rs | unilang |
| version | 0.46.0 |
| created_at | 2025-05-11 00:58:51.602444+00 |
| updated_at | 2025-12-19 21:16:22.35594+00 |
| description | Define your command-line utility interface once and get consistent interaction across multiple modalities β CLI, GUI, TUI, AUI, Web APIs, and moreβessentially for free. |
| homepage | https://github.com/Wandalen/wTools/tree/master/module/core/unilang/readme.md |
| repository | https://github.com/Wandalen/wTools/tree/master/module/core/unilang |
| max_upload_size | |
| id | 1668977 |
| size | 3,180,658 |
Zero-overhead command framework with compile-time command registration
Getting Started
Understanding Performance
Core Concepts
Advanced Topics
Reference
unilang processes command definitions at compile-time, generating optimized static command registries that provide O(1) command lookups with zero runtime overhead and zero additional dependencies for downstream crates. This approach delivers:
Developer writes YAML
|
v
unilang.commands.yaml
- name: ".greet"
description: "..."
arguments: [...]
|
v
build.rs (automatic)
- Discovers YAML files
- Validates definitions
- Generates static command map
|
v
OUT_DIR/static_commands.rs
static STATIC_COMMANDS: [...]
// Static commands with O(1) lookups
|
v
Binary compiled
(zero-overhead ready)
User: ".greet name::Alice"
|
v
Parser
(SIMD accelerated)
|
v
Static Registry Lookup
O(1) static map access
~80ns (no hashing!)
|
v
Semantic Analyzer
- Validate types
- Apply defaults
- Check constraints
|
v
Command Routine
Execute business logic
|
v
Output Result
Key Benefits:
Add dependency:
[dependencies]
unilang = "0.35"
Create unilang.commands.yaml in project root:
- name: ".greet"
description: "Greeting command"
arguments:
- name: "name"
kind: "String"
attributes:
optional: true
default: "World"
Run example:
cargo run --example static_01_basic_compile_time
What just happened? Commands auto-discovered from YAML, compiled into zero-overhead static registry, ready for O(1) lookups.
Next: See detailed guide for production setup.
Common issues and solutions for new users:
Symptom: Error: Command '.greet' not found in registry
Cause: Command name missing dot prefix
Solution:
# β Wrong
- name: "greet"
# β
Correct
- name: ".greet"
All command names MUST start with . (dot prefix)
Symptom: Type hint warnings during cargo build
Cause: YAML default values quoted when they shouldn't be
Solution:
# β Wrong - quoted values
arguments:
- name: "dry"
kind: "Boolean"
attributes:
default: 'false' # String, not Boolean!
# β
Correct - unquoted primitives
arguments:
- name: "dry"
kind: "Boolean"
attributes:
default: false # Boolean type
Rule: Boolean, Integer, Float values must NOT be quoted in YAML
Symptom: error: couldn't read .../static_commands.rs: No such file or directory
Cause: Wrong feature flag or missing build-time approach
Solution:
# Ensure you have a build-time approach enabled
[dependencies]
unilang = "0.35" # Default includes approach_yaml_multi_build
Check that you have at least one .yaml file with commands, or enable a different build-time approach feature.
Symptom: Lookups still slow after switching to build-time approach
Cause: Using runtime registration in production CLI (10-50x performance penalty)
Solution:
// β Wrong for production CLIs - 10-50x slower than static registration
// β
Appropriate for REPL/plugins/prototyping where flexibility > performance
let registry = CommandRegistry::new();
// β
Correct - using static registry
let registry = StaticCommandRegistry::from_commands(&STATIC_COMMANDS);
Verify you're using StaticCommandRegistry, not CommandRegistry::new().
Symptom: Error: Unknown parameter 'nam'
Cause: Typo in parameter name
Solution: Check error message for "Did you mean...?" suggestion:
Error: Unknown parameter 'nam'. Did you mean 'name'?
Use '.greet ??' for help
Parameter names are validated strictly - use ?? operator to see valid parameters.
cargo run --example static_01_basic_compile_timeRUST_LOG=debugcargo tree -f "{p} {f}"For comprehensive usage patterns, best practices, and quick reference: See usage.md for concise guide covering:
Create unilang.commands.yaml:
- name: ".greet" # RECOMMENDED: Show full command name
namespace: "" # Leave empty for root-level commands
description: "High-performance greeting command"
arguments:
- name: "name"
kind: "String"
attributes:
optional: true
default: "World"
YAML Command Naming:
name: ".greet" with namespace: "" - shows exact command users will typename: "greet" with namespace: ".session" - requires understanding concatenationname or namespaceThe default configuration already enables build-time YAML processing:
[dependencies]
# Multi-YAML build-time approach is enabled by default
unilang = "0.35"
For single-file YAML approach, use:
[dependencies]
unilang = { version = "0.35", default-features = false, features = [
"enabled",
"approach_yaml_single_build" # Single YAML file at compile-time
]}
Decision Tree:
Using default multi-YAML approach?
ββ YES β β
No build.rs needed (auto-discovery)
ββ NO β Using single-file YAML?
ββ YES β β
Add minimal build.rs below
ββ NO β β
Custom approach - implement discovery logic
For Single-File YAML Only (approach_yaml_single_build):
fn main()
{
// Rebuild if YAML file changes
println!( "cargo:rerun-if-changed=unilang.commands.yaml" );
// Static registry generation happens automatically
}
Default Multi-YAML: No build.rs needed - automatic discovery of all .yaml files!
use unilang::prelude::*;
// Include compile-time generated commands (created automatically by build system)
include!( concat!( env!( "OUT_DIR" ), "/static_commands.rs" ) );
fn main() -> Result< (), unilang::Error >
{
// StaticCommandRegistry requires approach_yaml_single_build,
// approach_yaml_multi_build, or other build-time approach feature
let registry = StaticCommandRegistry::from_commands( &STATIC_COMMANDS );
let pipeline = Pipeline::new( registry );
// O(1) lookup - no hashing overhead
let result = pipeline.process_command_simple( ".greet name::Alice" );
println!( "Output: {}", result.outputs[ 0 ].content );
Ok( () )
}
| Approach | Lookup Time | Memory Overhead | Binary Size |
|---|---|---|---|
| Build-Time (Static) | ~80ns | Zero | Smaller |
| Runtime (HashMap) | ~4,000ns | Hash tables + allocations | Larger |
Benchmark Results:
Hardware: AMD Ryzen 9 5950X, 64GB RAM, NVMe SSD
Framework: benchkit - specialized benchmarking framework for unilang
Test Scenario: Lookup 100 commands from registry of 1,000 commands
Iterations: 10,000 runs per approach, median reported
Tool: cargo run --example static_03_performance_comparison
What's measured:
Run benchmarks yourself:
cargo run --release --example static_03_performance_comparison
See /examples/static_03_performance_comparison.rs for full benchmark implementation.
How unilang differs from popular Rust CLI frameworks:
| Feature | unilang | clap | structopt | argh |
|---|---|---|---|---|
| Command Lookup | O(1) static | Runtime HashMap | Runtime | Runtime |
| Definition Style | YAML/JSON/Rust DSL | Rust builder API | Derive macros | Derive macros |
| Modality Support | CLI, REPL, Web API | CLI only | CLI only | CLI only |
| Multi-file Organization | Auto-discovery | Manual | Manual | Manual |
| Runtime Registration | Hybrid (10-50x slowerβ ) | No | No | No |
| Build-time Validation | Yes | No | Yes (compile) | Yes (compile) |
| REPL Support | Built-in | Manual | Manual | Manual |
| Help Generation | Auto + 3 operators | Auto | Auto | Auto |
| Performance | ~80ns lookup | ~200-500ns | ~200-500ns | ~100-300ns |
| CLI Aggregation | Built-in | Manual | Manual | Manual |
| Learning Curve | Medium | Low | Low | Very Low |
β Runtime registration appropriate for REPL applications, plugin systems, and prototyping. Not recommended for production CLIs due to performance penalty.
Choose unilang if you need:
Choose alternatives if:
argh - simplest)structopt/clap)clap has most)argh is fastest to learn)unilang:
clap/structopt/argh:
β οΈ IMPORTANT: Opinionated Defaults
unilang ONLY enables Approach #2 by default. To use any other approach, you must explicitly enable its feature flag in Cargo.toml.
10 approaches are currently implemented (see full comparison):
| # | Approach | Feature Flag | Default | Lookup Speed | When to Use |
|---|---|---|---|---|---|
| 1 | YAML file β Build-time static | approach_yaml_single_build |
β | ~80ns | Single-file projects, compile-time validation |
| 2 | Multi-YAML files β Build-time static | approach_yaml_multi_build |
β DEFAULT | ~80ns | Modular projects, best DX, auto-discovery |
| 3 | YAML file β Runtime loading | approach_yaml_runtime |
β | ~4,200ns | Plugin configs, runtime loading |
| 4 | JSON file β Build-time static | approach_json_single_build |
β | ~80ns | JSON-first projects, API generation |
| 5 | Multi-JSON files β Build-time static | approach_json_multi_build |
β | ~80ns | Large JSON projects, modular organization |
| 6 | JSON file β Runtime loading | approach_json_runtime |
β | ~4,200ns | Runtime config loading, dynamic commands |
| 7 | Rust DSL (builder API) | (always available) | β | ~4,200ns | Core API, prototyping, type-safe definitions |
| 8 | Rust DSL (const fn + static) | approach_rust_dsl_const |
β | ~80ns | High-performance DSL, compile-time |
| 18 | Hybrid (static + runtime) | approach_hybrid |
β | Mixed | Base CLI + plugin system |
Why Approach #2 is Default:
See full comparison: 21 approaches documented including planned features like declarative macros, proc macros, TOML, RON, Protobuf, GraphQL, OpenAPI.
// CLI applications: Avoids shell quoting issues
let result = pipeline.process_command_from_argv( &std::env::args().collect() );
// REPL/interactive applications: String-based parsing
let result = pipeline.process_command_simple( ".greet name::Alice" );
β Commands should start with a dot:
.greet name::Alice # Recommended
greet name::Alice # Not recommended
Choose Your Configuration:
| Use Case | Configuration | What You Get |
|---|---|---|
| Production (Recommended) | unilang = "0.35" |
Multi-YAML + SIMD + Enhanced REPL |
| Custom Approach | unilang = { version = "0.35", default-features = false, features = ["enabled", "approach_json_single_build"] } |
Switch to specific approach |
| Minimal (Core API Only) | unilang = { version = "0.35", default-features = false, features = ["enabled"] } |
Rust DSL only, no build-time |
| Full (Development/Testing) | unilang = { version = "0.35", features = ["full"] } |
All 21 approaches available |
Most users: Just use unilang = "0.35" and you're done.
Feature Architecture:
The framework uses approach-based feature flags:
approach_yaml_multi_build)static_registry, yaml_parser, etc.)Three methods available:
.command ? # Traditional operator (bypasses validation)
.command ?? # Modern parameter
.command.help # Auto-generated help command
Unknown parameters are always detected with Levenshtein distance suggestions. This cannot be disabled and ensures command correctness.
enhanced_repl feature for history, completion, secure inputThe following sections cover advanced usage patterns, migration strategies, and specialized deployments. New users should start with the Getting Started section above.
β οΈ Feature Required: multi_file (automatically enabled by default approach_yaml_multi_build)
unilang excels at aggregating multiple CLI tools into a single unified command interface. This is essential for organizations that want to consolidate developer tools while maintaining namespace isolation.
// Requires: approach_yaml_multi_build (default) or manually enable 'multi_file' feature
use unilang::multi_yaml::CliBuilder;
// Aggregate multiple CLI tools into one unified command
let unified_cli = CliBuilder::new()
.static_module_with_prefix( "database", "db", database_commands )
.static_module_with_prefix( "filesystem", "fs", file_commands )
.static_module_with_prefix( "network", "net", network_commands )
.static_module_with_prefix( "build", "build", build_commands )
.detect_conflicts( true )
.build_static();
// Usage: unified-cli .db.migrate, unified-cli .fs.copy src dest
Before Aggregation:
# Separate tools requiring individual installation and learning
db-cli migrate --direction up
file-cli copy --src ./source --dest ./target --recursive
net-cli ping google.com --count 10
build-cli compile --target release
After Aggregation:
# Single unified tool with consistent interface
unified-cli .db.migrate direction::up
unified-cli .fs.copy source::./source destination::./target recursive::true
unified-cli .net.ping host::google.com count::10
unified-cli .build.compile target::release
Each CLI module maintains its own command space with automatic prefix application:
Database commands become .db.migrate, .db.backup
File commands become .fs.copy, .fs.move
Network commands become .net.ping, .net.trace
No naming conflicts between modules
let registry = CliBuilder::new()
.static_module_with_prefix( "tools", "tool", cli_a_commands )
.static_module_with_prefix( "utils", "tool", cli_b_commands ) // Conflict!
.detect_conflicts( true ) // Catches duplicate prefixes at build time
.build_static();
# All aggregated commands support unified help
unified-cli .db.migrate.help # Detailed help for database migrations
unified-cli .fs.copy ?? # Interactive help during command construction
unified-cli .net.ping ? # Traditional help operator
let registry = CliBuilder::new()
.conditional_module( "docker", docker_commands, &[ "feature_docker" ] )
.conditional_module( "k8s", kubernetes_commands, &[ "feature_k8s" ] )
.build_static();
// Only includes modules when features are enabled
use std::path::PathBuf;
// Combine static commands (in-memory) and dynamic YAML loading
let registry = CliBuilder::new()
.static_module_with_prefix( "core", ".core", core_commands )
.dynamic_module_with_prefix( "plugins", PathBuf::from( "plugins.yaml" ), ".plugins" )
.build_hybrid();
// Key differences:
// - static_module_with_prefix(name, prefix, Vec<CommandDefinition>)
// β Commands already in memory, fast O(1) lookup (~80-100ns)
// - dynamic_module_with_prefix(name, PathBuf, prefix)
// β Commands loaded from YAML file at runtime (~4,000ns, 50x slower)
| Approach | Lookup Time | Memory Overhead | Conflict Detection |
|---|---|---|---|
| Build-Time | O(1) static | Zero | Build-time |
| Runtime | O(log n) | Hash tables | Runtime |
Aggregation Scaling:
See examples/practical_cli_aggregation.rs for a comprehensive demonstration showing:
# Run the complete aggregation demo
cargo run --example practical_cli_aggregation
This example demonstrates aggregating database, file, network, and build CLIs into a single unified tool while maintaining type safety, performance, and usability.
- name: ".command_name" # Required: Command identifier (must start with dot)
namespace: "" # Optional: Hierarchical organization (e.g., "math", "file")
description: "What it does" # Required: User-facing description
arguments: # Optional: Command parameters
- name: "arg_name"
kind: "String" # String, Integer, Float, Boolean, Path, etc.
attributes:
optional: false # Required by default
default: "value" # Default value if optional
arguments:
- name: "count"
kind: "Integer"
validation_rules:
- Min: 1
- Max: 100
- name: "email"
kind: "String"
validation_rules:
- Pattern: "^[^@]+@[^@]+\\.[^@]+$"
- MinLength: 5
β οΈ YAML permits type coercion, but unilang enforces strict typing. The build system now emits type hints for these common mistakes:
β WRONG - Quoted defaults for typed arguments:
- name: "dry"
kind: "Boolean"
attributes:
default: 'false' # String literal, not boolean
- name: "verbosity"
kind: "Integer"
attributes:
default: '2' # String literal, not integer
β CORRECT - Unquoted defaults matching argument type:
- name: "dry"
kind: "Boolean"
attributes:
default: false # Boolean value
- name: "verbosity"
kind: "Integer"
attributes:
default: 2 # Integer value
π‘ Type Hint Detection: The build system analyzes argument definitions and emits non-blocking hints during cargo build when it detects potential type mismatches. To suppress hints for intentional cases (e.g., version strings like "1.0"), add suppress_type_hint: true to the argument's attributes.
Help Generation:
auto_help_enabled: true # Default: true. Controls auto-generation of .command.help
# Set to false to prevent .command.help creation
# Help still available via ? and ?? operators
let result = pipeline.process_command_simple( ".namespace.command arg::value" );
if result.success
{
println!( "Success: {}", result.outputs[ 0 ].content );
}
let commands = vec!
[
".file.create name::test.txt",
".file.write name::test.txt content::data",
".file.list pattern::*.txt",
];
let batch_result = pipeline.process_batch( &commands, ExecutionContext::default() );
println!( "Success rate: {:.1}%", batch_result.success_rate() * 100.0 );
match pipeline.process_command_simple( ".command arg::value" )
{
result if result.success =>
{
// Process successful execution
for output in result.outputs
{
println!( "Output: {}", output.content );
}
}
result =>
{
if let Some( error ) = result.error
{
eprintln!( "Command failed: {}", error );
}
}
}
unilang provides comprehensive help with three access methods:
.command ? # Instant help, bypasses validation
.command ?? # Clean help access
.command arg1::value ?? # Help with partial arguments
.command.help # Direct help command access
.namespace.command.help # Works with namespaced commands
[dependencies]
unilang = "0.10" # Default: enhanced_repl + simd + enabled
[dependencies]
unilang = { version = "0.10", features = ["simd", "enhanced_repl"] }
[dependencies]
unilang = { version = "0.10", default-features = false, features = ["enabled"] }
enabled - Core functionality (required)simd - SIMD optimizations for 4-25x parsing performanceenhanced_repl - Advanced REPL with history, completion, secure inputrepl - Basic REPL functionalityon_unknown_suggest - Fuzzy command suggestions1. Quick Start (Runtime, Educational Only)
00_quick_start.rs - Get something working in 5 minutes (β οΈ runtime registration, slow)01_basic_command_registration.rs - Understand the runtime API (β οΈ 50x slower than build-time)2. Production Approach (Build-Time, Recommended)
static_01_basic_compile_time.rs - READ THIS FIRST - Explains proper YAML + build.rs patternstatic_02_yaml_build_integration.rs - Multi-YAML file aggregationstatic_03_performance_comparison.rs - Benchmark compile-time vs runtime (proves 50x speedup)static_04_multi_module_aggregation.rs - Organize commands across modules3. Advanced Type System
02_argument_types.rs - String, Integer, Float, Boolean, Path, etc. (β οΈ requires json_parser)03_collection_types.rs - Lists, Maps with custom delimiters14_advanced_types_validation.rs - Complex validation rules (β οΈ requires json_parser)4. Help & User Experience
06_help_system.rs - Comprehensive help system18_help_conventions_demo.rs - Three help access methods (?, ??, .help)5. REPL Applications
12_repl_loop.rs - Basic REPL implementation15_interactive_repl_mode.rs - Interactive arguments + secure input (β οΈ requires enhanced_repl)17_advanced_repl_features.rs - History, completion, recovery (β οΈ requires enhanced_repl)6. Complete Applications
full_cli_example.rs - Full-featured CLI with all concepts integratedpractical_cli_aggregation.rs - Real-world multi-tool aggregation (β οΈ requires multi_file)json_parser = Requires JSON support featureenhanced_repl = Requires advanced REPL featuresmulti_file = Requires multi-file aggregation (default includes this)unilang provides full WebAssembly compatibility for browser deployment:
cd examples/wasm-repl
wasm-pack build --target web --release
cd www && python3 -m http.server 8000
WASM Features:
β οΈ PERFORMANCE NOTICE: Runtime command registration (CommandRegistry::new() and command_add_runtime()) has 10-50x slower performance than compile-time registration.
When to use runtime registration:
When to use compile-time registration:
This section helps migrate performance-critical code from runtime to compile-time registration for 50x speedup.
Migrate from runtime registration (10-50x slower) to build-time registration (β‘ 50x faster) in 4 steps when performance matters.
Before (Runtime, in main.rs): β οΈ NOT RECOMMENDED FOR PRODUCTION CLIs (10-50x slower)
let mut registry = CommandRegistry::new();
let greet_cmd = CommandDefinition {
name: ".greet".to_string(),
namespace: String::new(),
description: "Greeting command".to_string(),
hint: "Say hello".to_string(),
arguments: vec![
ArgumentDefinition {
name: "name".to_string(),
kind: Kind::String,
description: "Person's name".to_string(),
hint: "Name".to_string(),
attributes: ArgumentAttributes {
optional: true,
default: Some("World".to_string()),
..Default::default()
},
validation_rules: vec![],
aliases: vec![],
tags: vec![],
}
],
// ... other fields
status: "stable".to_string(),
version: "1.0.0".to_string(),
aliases: vec![],
tags: vec![],
permissions: vec![],
idempotent: true,
deprecation_message: String::new(),
http_method_hint: String::new(),
examples: vec![],
routine_link: None,
auto_help_enabled: false,
};
registry.command_add_runtime(&greet_cmd, greet_routine)?;
After (Build-Time, in unilang.commands.yaml):
- name: ".greet"
namespace: ""
description: "Greeting command"
hint: "Say hello"
status: "stable"
version: "1.0.0"
tags: []
aliases: []
permissions: []
idempotent: true
deprecation_message: ""
http_method_hint: ""
auto_help_enabled: false
examples: []
arguments:
- name: "name"
kind: "String"
description: "Person's name"
hint: "Name"
attributes:
optional: true
default: "World"
multiple: false
interactive: false
sensitive: false
validation_rules: []
aliases: []
tags: []
routine_link: null
Add feature flag:
[dependencies]
# Enable single-file YAML compile-time approach
unilang = { version = "0.35", features = ["approach_yaml_single_build"] }
# Or use default (multi-file auto-discovery)
unilang = "0.35"
For approach_yaml_single_build, create build.rs:
fn main()
{
// Rebuild if YAML file changes
println!("cargo:rerun-if-changed=unilang.commands.yaml");
// Static registry generation happens automatically
// No manual code needed - the feature flag handles it
}
Note: With default approach_yaml_multi_build, no build.rs needed - auto-discovery handles everything!
Before (Runtime): β οΈ 10-50x SLOWER (not recommended for production)
use unilang::prelude::*;
fn main() -> Result<(), unilang::Error> {
let mut registry = CommandRegistry::new();
// Manual registration (10-50x slower than static)
registry.command_add_runtime(&greet_cmd, greet_routine)?;
let pipeline = Pipeline::new(registry);
let result = pipeline.process_command_simple(".greet name::Alice");
Ok(())
}
After (Build-Time):
use unilang::prelude::*;
// Include build-time generated commands (auto-generated by build system)
include!(concat!(env!("OUT_DIR"), "/static_commands.rs"));
fn main() -> Result<(), unilang::Error> {
// Zero-cost static registry (~80ns lookup vs ~4,000ns runtime)
let registry = StaticCommandRegistry::from_commands(&STATIC_COMMANDS);
let pipeline = Pipeline::new(registry);
let result = pipeline.process_command_simple(".greet name::Alice");
Ok(())
}
Run benchmarks:
cargo run --example static_03_performance_comparison
Expected results: