Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Parse command line arguments

Basic Argument Parsing

This section covers a basic implementation of commandline argument parsing. Use args to get the arguments this process was started with:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    assert!(!args.is_empty());

    // Skip the program name, it's usually the first argument
    let args: Vec<String> = env::args().skip(1).collect();
}

Some args, e.g. file paths, may not be valid UTF-8. Use args_os to handle those safely:

use std::{env, ffi::OsString};

fn main() {
    let os_args: Vec<OsString> = env::args_os().collect();
    assert!(!os_args.is_empty());
}

Calculate File Stats

In this section we parse CLI args and compute per-file stats (line/word counts). A mini wc-like utility

Flags:

  • –lines, -l Count lines per file
  • –words, -w Count words per file
  • –paths, -p Remaining args treated as file paths

-p acts as a terminator. Everything after it is consumed as file paths and parsing stops immediately.

CliArgs holds the parsed state of the process args. It uses args_os over args to safely handle non-UTF-8 paths, which can occur on Linux. skip(1) drops the bin name since it is always the first arg. Once -p flag is is encountered, all remaining args are collected into paths.

Stats<'p> represents the computed result for a single file. It borrows filepath directly from CliArgs rather than cloning it, with the lifetime ’p tying each Stats instance to its source CliArgs. Both lines and words are Option<usize>, where None means the flag was absent and the stat was never computed, while Some(n) means the flag was set and the count is n. This distinction matters because it avoids confusing “not requested” with a legitimate count of zero.

stat_files is the core logic. Iterates over cli_args.paths and builds a Vec<Stats<'_>>. Non-files and IO errors are skipped silently via continue for simplicity. Each file is read to a string buffer with fs::read_to_string() for processing. Line count uses content.lines().count(). Word count splits each line on whitespace via split_whitespace(), counts tokens per line, then sums across all lines, which correctly handles tabs and multiple consecutive spaces. Both stats are computed only if their respective flag was set, so there is no wasted work.

The result is output as one line per file. Stats omitted by the user are excluded from the output entirely rather than printed as zero or a placeholder.

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

// Represents the arguments passed to this process during initialization
#[derive(Debug)]
struct CliArgs {
    lines: bool,
    words: bool,
    paths: Vec<PathBuf>,
}

impl CliArgs {
    fn parse() -> Self {
        let mut lines = false;
        let mut words = false;
        let mut paths = Vec::new();

        let args = env::args_os().skip(1);
        let mut args = args.into_iter();
        while let Some(arg) = args.next() {
            match arg.to_str().unwrap_or_default() {
                "-l" | "--lines" => lines = true,
                "-w" | "--words" => words = true,
                "-p" | "--paths" => {
                    paths.extend(args.by_ref().map(PathBuf::from));
                    break;
                }
                _ => {}
            }
        }

        CliArgs {
            lines,
            words,
            paths,
        }
    }
}

// Represents the statistics of a given filepath
struct Stats<'p> {
    filepath: &'p Path,
    lines: Option<usize>,
    words: Option<usize>,
}

fn stat_files(cli_args: &CliArgs) -> Vec<Stats<'_>> {
    let mut stat_collection: Vec<Stats> = Vec::new();
    // If no flag is set, do no work
    if !cli_args.words && !cli_args.lines {
        return stat_collection;
    }

    for filepath in &cli_args.paths {
        // Skip anything that ain't a file
        if !filepath.is_file() {
            continue;
        }

        let Ok(content) = fs::read_to_string(filepath) else {
            continue;
        };

        let lines = match cli_args.lines {
            true => Some(content.lines().count()),
            false => None,
        };

        let words = match cli_args.words {
            true => Some(
                content
                    .lines()
                    .map(|line| line.split_whitespace().count())
                    .sum(),
            ),
            false => None,
        };

        let stats = Stats {
            filepath,
            lines,
            words,
        };

        stat_collection.push(stats);
    }

    stat_collection
}

fn main() {
    let cli_args = CliArgs::parse();
    let stats = stat_files(&cli_args);
    // Print individual file statistics
    for stat in stats {
        println!(
            "Path: {:?} {} {}",
            stat.filepath,
            if let Some(words) = stat.words {
                format!("Words: {}", words)
            } else {
                "".to_string()
            },
            if let Some(lines) = stat.lines {
                format!("Lines: {}", lines)
            } else {
                "".to_string()
            },
        );
    }
}

Example Usage:

cargo run --release -- -l -w -p src/main.rs Cargo.toml
# Path: "src/main.rs" Words: 312 Lines: 84
# Path: "Cargo.toml" Words: 21 Lines: 9

cargo run --release -- -l -p src/main.rs
# Path: "src/main.rs" Lines: 84

Clap Basic

clap-badge cat-command-line-badge

This application describes the structure of its command-line interface using clap’s builder style. The documentation gives two other possible ways to instantiate an application.

In the builder style, each possible argument is described by an Arg struct. The string given to Arg::new() is the internal name of the argument. The short and long options control the flag the user will be expected to type; short flags look like -f and long flags look like --file.

The get_one() method is used to get an argument’s value. It returns Some(&value) if the argument was supplied by the user, else None.

The use of PathBuf is to allow file paths which are legal in Linux and MacOS, but not in Rust UTF-8 strings. This is best practice: one encounters such paths quite rarely in practice, but when it happens it is really frustrating without this.

use std::path::PathBuf;

use clap::{Arg, Command, builder::PathBufValueParser};

fn main() {
    let matches = Command::new("My Test Program")
        .version("0.1.0")
        .about("Teaches argument parsing")
        .arg(Arg::new("file")
                 .short('f')
                 .long("file")
                 .help("A cool file")
                 .value_parser(PathBufValueParser::default()))
        .arg(Arg::new("num")
                 .short('n')
                 .long("number")
                 .help("Five less than your favorite number"))
        .get_matches();

    let default_file = PathBuf::from("input.txt");
    let myfile: &PathBuf = matches.get_one("file").unwrap_or(&default_file);
    println!("The file passed is: {}", myfile.display());

    let num_str: Option<&String> = matches.get_one("num");
    match num_str {
        None => println!("No idea what your favorite number is."),
        Some(s) => {
            let parsed: Result<i32, _> = s.parse();
            match parsed {
                Ok(n) => println!("Your favorite number must be {}.", n + 5),
                Err(_) => println!("That's not a number! {}", s),
            }
        }
    }
}

Usage information is generated by clap -h. The usage for the example application looks like this.

Teaches argument parsing

Usage: clap-cookbook [OPTIONS]

Options:
  -f, --file <file>   A cool file
  -n, --number <num>  Five less than your favorite number
  -h, --help          Print help
  -V, --version       Print version

We can test the application by running a command like the following.

$ cargo run -- -f myfile.txt -n 251

The output is:

The file passed is: myfile.txt
Your favorite number must be 256.