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(())
}