Type-safe, ergonomic command-line bindings for Rust
Find a file
2026-01-14 12:26:30 +01:00
examples
src fix: ignore write errors stdin 2026-01-14 12:26:30 +01:00
.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 tokio integration
  • 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 execute
  • ArgsStructName: The args struct
  • validator_function: A function fn(&CommandOutput) -> bool
  • pre: 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