Type-safe, ergonomic command-line bindings for Rust
| examples | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
cmdbind
Type-safe, ergonomic command-line bindings for Rust.
cmdbind lets you call external binaries with the convenience and type-safety of native Rust functions. Think of it as FFI, but for command-line processes.
Features
- 🎯 Type-safe - Compile-time checked arguments
- 🔧 Ergonomic - Fluent builder API
- ⚡ Async support - Optional
tokiointegration - ✅ Error Handling - Define success conditions per command
- 🔍 Binary detection - Check if commands are available at runtime
Quick Start
Add cmdbind and derive_builder to your Cargo.toml:
[dependencies]
derive_builder = "0.20.2"
cmdbind = { git = "https://git.hydrar.de/jmarya/cmdbind" }
# For async support
cmdbind = { git = "https://git.hydrar.de/jmarya/cmdbind", features = ["async"] }
Basic Usage
use cmdbind::{wrap_binary, RunnableCommand, validators::non_zero_only};
// Wrap the `file` command
#[derive(Debug, Default, derive_builder::Builder, serde::Serialize)]
#[builder(setter(into), default)]
pub struct FileArgs {
mime: bool,
positional_file: String
}
wrap_binary!(FileCmd, "file", FileArgs, non_zero_only);
fn main() {
// Equivalent to: file --mime Cargo.toml
let cmd = FileCmd::new()
.mime(true)
.positional_file("Cargo.toml".to_string());
match cmd.run(None) {
Ok(output) => {
println!("MIME type: {}", output.stdout_str().unwrap());
}
Err(e) => eprintln!("Error: {:?}", e),
}
}
Error Handling
use cmdbind::{RunnableCommand, errors::CommandError, validators::non_zero_only, wrap_binary};
#[derive(Debug, Default, derive_builder::Builder, serde::Serialize)]
#[builder(setter(into), default)]
pub struct GrepArgs {
quiet: bool,
positional_pattern: String,
positional_file: String
}
wrap_binary!(GrepCmd, "grep", GrepArgs, non_zero_only);
fn main() {
let cmd = GrepCmd::new()
.positional_pattern("TODO".to_string())
.positional_file("src/main.rs".to_string());
match cmd.run(None) {
Ok(output) => {
println!("Matches found:\n{}", output.stdout_str().unwrap());
}
Err(CommandError::Internal(e)) => {
eprintln!("Failed to execute grep: {}", e);
}
Err(CommandError::Output(output)) => {
eprintln!("grep exited with code: {:?}", output.status());
eprintln!("stderr: {}", output.stderr_str().unwrap());
}
}
}
Async Execution
use cmdbind::{wrap_binary, RunnableCommand, validators::non_zero_only};
#[derive(Debug, Default, derive_builder::Builder, serde::Serialize)]
#[builder(setter(into), default)]
pub struct CurlArgs {
silent: bool,
positional_url: String
}
wrap_binary!(CurlCmd, "curl", CurlsArgs, non_zero_only);
#[tokio::main]
async fn main() {
let cmd = CurlCmd::new()
.silent(true)
.positional_url("https://example.com".to_string());
let output = cmd.run_async(None).await.expect("Failed to fetch URL");
println!("{}", output.stdout_str().unwrap());
}
Custom Validators
Validators determine whether a command execution is considered successful:
use cmdbind::CommandOutput;
// grep returns 0 on match, 1 on no match, 2 on error
fn grep_validator(output: &CommandOutput) -> bool {
matches!(output.status(), Some(0) | Some(1))
}
wrap_binary!(GrepCmd, "grep", GrepArgs {
positional_pattern: String,
positional_file: String
}, grep_validator);
Checking Binary Availability
if !FileCmd::binary_available() {
eprintln!("Error: 'file' command not found in PATH");
std::process::exit(1);
}
// Proceed with command execution...
API Reference
wrap_binary! Macro
#[derive(Debug, Default, derive_builder::Builder, serde::Serialize)]
#[builder(setter(into), default)]
pub struct ArgsStructName {
field_name: Type,
// ...
}
wrap_binary!(
CommandName,
"binary-name",
ArgsStruct,
validator_function,
"pre"
);
Arguments:
CommandName: The Rust struct name for the command"binary-name": The actual binary to executeArgsStructName: The args structvalidator_function: A functionfn(&CommandOutput) -> boolpre: pre arguments (eg. a command)
Special field naming:
positional_*: Positional arguments (e.g.,positional_file,positional_0)- Other fields: Converted to
--flag-name
RunnableCommand Trait
Automatically implemented by wrap_binary!:
trait RunnableCommand {
fn binary() -> &'static str;
fn binary_available() -> bool;
fn run(self, env: Option<&CommandEnvironment>) -> Result<CommandOutput, CommandError>;
fn run_async(self, env: Option<&CommandEnvironment>) -> impl Future<Output = Result<CommandOutput, CommandError>>;
}
CommandOutput
impl CommandOutput {
fn status(&self) -> Option<i32>;
fn stdout(&self) -> &[u8];
fn stderr(&self) -> &[u8];
fn stdout_str(&self) -> Result<String, FromUtf8Error>;
fn stderr_str(&self) -> Result<String, FromUtf8Error>;
}
Built-in Validators
validators::always_success // Always returns true
validators::non_zero_only // Success only on exit code 0