CLI#

The stdlib exposes std::env::args() for raw command-line parsing; for anything beyond a flag or two, the operator reaches for clap, the de-facto parser in the Rust ecosystem.

/io`. For packaging and shipping a binary, see Runtime.

std::env::args#

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("program: {}", args[0]);
    for (i, a) in args.iter().enumerate().skip(1) {
        println!("{i} {a}");
    }
}
$ ./scan -v --port 8080 host
program: ./scan
1 -v
2 --port
3 8080
4 host

For Unicode-safe arguments (no panic on non-UTF8), args_os returns OsString.

clap (derive API)#

The default. The derive API drives a struct that both parses and gives typed access to the result.

# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

#[derive(Parser)]
#[command(version, about = "Quick port scanner.")]
struct Cli {
    /// Target host.
    host: String,

    /// Port to probe.
    #[arg(short, long, default_value_t = 80)]
    port: u16,

    /// Verbose output.
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();
    println!("{} {} {}", cli.host, cli.port, cli.verbose);
}
$ ./scan --help
Quick port scanner.

Usage: scan [OPTIONS] <HOST>
...

Subcommands.

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    Up,
    Down,
    Status { #[arg(short)] verbose: bool },
}

fn main() {
    match Cli::parse().cmd {
        Cmd::Up               => start(),
        Cmd::Down             => stop(),
        Cmd::Status { verbose } => status(verbose),
    }
}

clap (builder API)#

For runtime-constructed arg specs, the builder API.

use clap::{Arg, Command};

let matches = Command::new("scan")
    .arg(Arg::new("host").required(true))
    .arg(Arg::new("port").short('p').long("port").default_value("80"))
    .arg(Arg::new("verbose").short('v').long("verbose").action(clap::ArgAction::SetTrue))
    .get_matches();

let host: &String = matches.get_one("host").unwrap();
let port: u16 = matches.get_one::<String>("port").unwrap().parse()?;

argh and pico-args#

For minimal-dependency CLIs pick argh (derive-based, smaller than clap) or pico-args (50 KB, no dependencies).

use argh::FromArgs;

/// Scan a target.
#[derive(FromArgs)]
struct Cli {
    /// host
    #[argh(positional)]
    host: String,

    /// port
    #[argh(option, short = 'p', default = "80")]
    port: u16,
}

Exit codes#

std::process::exit(code) exits immediately with the given code. The operator’s preferred form is to return ExitCode from main, which lets Drop impls run.

use std::process::ExitCode;

fn main() -> ExitCode {
    match run() {
        Ok(())  => ExitCode::SUCCESS,
        Err(e)  => { eprintln!("error: {e}"); ExitCode::from(1) }
    }
}

For anyhow::Result flow, main() -> anyhow::Result<()> prints the chained error and exits non-zero automatically.

fn main() -> anyhow::Result<()> { run() }

Convention (same as every Unix tool).

Code

Meaning

0

success

1

generic error

2

usage error

130

terminated by SIGINT (Ctrl-C)

Streaming#

Filter-style scripts read stdin, transform, write stdout.

use std::io::{self, BufRead, Write};

fn main() -> io::Result<()> {
    let stdin = io::stdin();
    let stdout = io::stdout();
    let mut out = stdout.lock();
    for line in stdin.lock().lines() {
        writeln!(out, "{}", line?.to_uppercase())?;
    }
    Ok(())
}
$ echo -e "one\ntwo" | ./upper
ONE
TWO

Signals#

Rust has no stdlib signal API; reach for ctrlc (simple) or signal-hook (full POSIX surface).

[dependencies]
ctrlc = "3"
ctrlc::set_handler(|| {
    println!("\nbye");
    std::process::exit(130);
})?;

loop { work(); }

For async runtimes, tokio::signal::ctrl_c returns a future that completes on Ctrl-C.

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tokio::select! {
        _ = work() => {}
        _ = tokio::signal::ctrl_c() => println!("shutting down"),
    }
    Ok(())
}

References#