RustArena

Step 2 of 6

Flags and options

Real tools take three kinds of arguments:

The standard way to parse them by hand is a loop over an iterator, with match on each argument. Taking a slice of strings instead of reading env::args() inside the function makes the parser easy to test, so main just does let args: Vec<String> = env::args().skip(1).collect(); and passes &args:

struct Config {
    pattern: String,
    files: Vec<String>,
    ignore_case: bool,
    max: Option<usize>,
}

fn parse_args(args: &[String]) -> Result<Config, String> {
    let mut pattern = None;
    let mut files = Vec::new();
    let mut ignore_case = false;
    let mut max = None;
    let mut iter = args.iter();
    while let Some(arg) = iter.next() {
        match arg.as_str() {
            "-i" | "--ignore-case" => ignore_case = true,
            "-m" | "--max" => {
                let value = iter.next().ok_or("missing value for --max")?;
                let n = value.parse().map_err(|_| format!("invalid number for --max: {value}"))?;
                max = Some(n);
            }
            flag if flag.starts_with('-') => return Err(format!("unknown flag: {flag}")),
            _ if pattern.is_none() => pattern = Some(arg.clone()),
            _ => files.push(arg.clone()),
        }
    }
    let pattern = pattern.ok_or("missing pattern")?;
    Ok(Config { pattern, files, ignore_case, max })
}

fn main() {
    let tries = [
        vec!["-i", "error", "app.log"],
        vec!["--max", "2", "warn", "a.log", "b.log"],
        vec!["error", "-x"],
        vec!["-m"],
    ];
    for t in tries {
        let args: Vec<String> = t.iter().map(|s| s.to_string()).collect();
        match parse_args(&args) {
            Ok(c) => println!("pattern={} files={:?} ignore_case={} max={:?}", c.pattern, c.files, c.ignore_case, c.max),
            Err(e) => println!("error: {e}"),
        }
    }
}
pattern=error files=["app.log"] ignore_case=true max=None
pattern=warn files=["a.log", "b.log"] ignore_case=false max=Some(2)
error: unknown flag: -x
error: missing value for --max

Piece by piece:

Conventions users expect

In real projects the clap crate handles all of this and generates --help for you (last step). Writing a parser once by hand shows you what clap does and helps when you write small tools without dependencies.

Common mistakes

Your task

Write parse_args for a log-search tool. Flags: -i/--ignore-case, -n/--line-numbers, and the option -m N/--max N. The first positional argument is the pattern, the optional second one is the file; a third positional is an error. Errors (exact text): missing pattern, unexpected argument: <arg>, unknown flag: <flag>, missing value for --max, invalid number for --max: <value>.

Challenge 2

Users also type --max=5 and group short flags as -in. Write fn expand(args: &[String]) -> Vec<String> that normalizes the arguments before parsing: a long option with = (--max=5) becomes two arguments (--max, 5); a single dash followed by two or more letters (-in) becomes one flag per letter (-i, -n). Everything else is kept as is, and after a -- argument nothing more is changed (the -- itself is kept).

Challenge 3

Write seq: seq [-s SEP] [-w] FIRST LAST prints the whole numbers from FIRST to LAST (inclusive, counting down if LAST is smaller), separated by SEP (default: a newline). With -w, pad every number with zeros to the width of the widest one (08, 09, 10). The options may come before or after the numbers, and you can assume the arguments are valid.

Previous: Command-line arguments Next: Exit codes and stderr