Step 2 of 6
Flags and options
Real tools take three kinds of arguments:
- Positional arguments: meaning comes from position, like the pattern and file in
grep error app.log. - Flags: on/off switches, a short form
-iand a long form--ignore-case. - Options: a name followed by a value, like
--max 5or-m 5.
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:
while let Some(arg) = iter.next()instead offor: an option needs to take the next argument as its value, whichiter.next()inside the loop does.match arg.as_str()matches aStringagainst string literals;|gives the short and long forms the same arm.ok_or("...")turns theNoneof a missing value into an error, and?returns it. A&strerror converts into theStringerror type automatically.- The guard
flag if flag.starts_with('-')catches unknown flags, so a typo like--ignore-cseis reported instead of being treated as a file name. - All the results land in a Config struct. The rest of the program reads the config, never the raw arguments.
Conventions users expect
-h/--helpprints help;-V/--versionprints the version.--means "end of options": everything after it is positional, so you can search for a pattern that starts with-.-as a file name means standard input (step 5).- Order usually doesn't matter:
grep -i errorandgrep error -iboth work (ours does too).
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
- Using a
forloop and then callingiter.next()inside it:for arg in itermoves the iterator into the loop (borrow of moved value: iter), andfor arg in &mut iterborrows it mutably for the whole loop, so a seconditer.next()is refused. Usewhile let. - Not rejecting unknown flags: they silently become positional arguments, and the user's typo becomes "file not found: --verbos".
- Parsing in
mainwithenv::args()directly: then you can only test it by running the program. A function taking&[String]can be tested with plain vectors.
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