§ Language Reference
Rust ● stable
Adapters for clap (derive and builder APIs) and argh. The Rust implementation shares the same wire format and exit code taxonomy as the Go reference.
Installation
tomlCargo.toml
[dependencies]
murli = { version = "0.1", features = ["clap"] } # clap adapter
murli = { version = "0.1", features = ["argh"] } # argh adapter
murli = { version = "0.1", features = ["clap", "argh"] } # both
clap — derive API
Flatten GlobalArgs into your top-level struct, then call handle_builtins before dispatching commands.
rustmain.rs
use clap::{CommandFactory, Parser};
use murli::clap::GlobalArgs;
use murli::Writer;
use serde_json::json;
#[derive(Parser)]
#[command(name = "mytool", about = "My deployment tool")]
struct Cli {
#[command(flatten)]
murli: GlobalArgs, // --agent --schema --force --dry-run --output --profile
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand)]
enum Commands {
Deploy { env: String },
}
fn main() {
let args = Cli::parse();
// handles --schema and mutation guard; exits if consumed
murli::clap::handle_builtins(&args.murli, &Cli::command(), None);
let mut writer = Writer::from_args(&args.murli);
match args.command {
Commands::Deploy { env } => {
if writer.is_dry_run() {
writer.write_plan(&format!("Would deploy to {env}"), &json!({"env": env}));
} else {
writer.write_success(&format!("Deployed to {env}"), &json!({"env": env}));
}
}
}
}
clap — builder API
murli::clap::enable adds all murli flags to an existing Command and mounts describe, doctor, and profile as real subcommands.
rustmain.rs
fn main() {
let mut cmd = build_my_command();
murli::clap::enable(&mut cmd);
let matches = cmd.get_matches();
// exits if describe/doctor/profile/--schema was invoked
murli::clap::handle_matches(&matches, &build_my_command());
let writer = murli::Writer::new(
matches.get_flag("agent"),
None, false, false, env!("CARGO_PKG_VERSION"),
);
// dispatch using matches
}
argh
argh 0.1.x limitation
argh 0.1.x does not support
#[argh(flatten)]. Parse GlobalArgs separately alongside your own args struct, or wait for flatten support in a future argh release.
rustmain.rs
use argh::FromArgs;
use murli::argh::GlobalArgs;
use serde_json::json;
#[derive(FromArgs, Debug)]
#[argh(description = "My deployment tool")]
struct Cli {
/// Target environment
#[argh(option)]
env: String,
}
fn main() {
let raw: Vec<String> = std::env::args().skip(1).collect();
let strs: Vec<&str> = raw.iter().map(|s| s.as_str()).collect();
let murli_args = GlobalArgs::from_args(&["mytool"], &strs).unwrap_or_default();
murli::argh::handle_builtins(&murli_args, None, "mytool", "My deployment tool");
let mut writer = murli::argh::writer_from_args(&murli_args);
let args: Cli = argh::from_env();
writer.write_success(&format!("Deployed to {}", args.env), &json!({"env": args.env}));
}
Writer
Obtain via Writer::from_args(&global_args) (derive) or Writer::new(...) (builder/custom).
Output methods
| Method | Writes to | When |
|---|---|---|
| write_success(&str, &Value) | stdout | Command completed successfully |
| write_plan(&str, &Value) | stdout | Dry-run: show what would happen |
| write_error(AgentError) | stderr + exit | Command failed; process exits with error code |
| write_event(&Value) | stdout | One NDJSON line per call for streaming output |
| write_progress(ProgressEvent) | stdout (agent) / stderr (TTY) | Typed progress update |
| log(&str) | stderr | Diagnostic line with deduplication |
State methods
| Method | Returns |
|---|---|
| is_tty() → bool | True if stdout is a terminal |
| is_forced() → bool | True if --force or --yes was passed |
| is_dry_run() → bool | True if --dry-run was passed |
Testing
Use
write_error_to_streams instead of write_error in tests — it writes the error envelope without calling process::exit, so the test process continues.
AgentError
Convenience constructors
rust
writer.write_error(AgentError::user_error("flag --env is required", "pass --env prod"));
writer.write_error(AgentError::tool_error("database connection failed"));
writer.write_error(AgentError::not_found("index not found", "run `mytool index build`"));
writer.write_error(AgentError::rate_limited("API limit hit", 5000));
Full struct
rust
writer.write_error(AgentError {
code: EXIT_NOT_FOUND,
error_type: "index_missing".into(),
message: "Semantic index not found".into(),
suggestion: "Run `mytool index build`".into(),
recoverable: false,
doc_url: "https://example.com/docs".into(),
..Default::default()
});
Differences from the Go implementation
| Feature | murli-go | murli-rs |
|---|---|---|
| Flag injection | Dynamic at runtime | clap: #[command(flatten)]; argh: separate parse |
| argh flatten | N/A | Not available in argh 0.1.x |
--yes alias | Sibling flag | visible_alias = "yes" on --force |
enum field name | Enum []string | enum_values: Vec<String> (reserved word) |
| argh describe | Auto-introspected at runtime | Metadata-driven (argh has no runtime command tree) |
| Process exit hook | murli.ExitFunc | write_error_to_streams test helper |
| Profile path | ~/. | dirs::config_dir()/ |