use std::{env, fmt, process}; #[derive(Debug, Default, PartialEq)] struct Config { verbose: bool, output: Option, count: u32, inputs: Vec, } #[derive(Debug, PartialEq)] enum ParseError { MissingValue(&'static str), InvalidCount(String), UnknownOption(String), } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingValue(option) => write!(f, "missing value for {option}"), Self::InvalidCount(value) => write!(f, "invalid count: {value}"), Self::UnknownOption(option) => write!(f, "unknown option: {option}"), } } } fn parse_args(args: I) -> Result where I: IntoIterator, S: Into, { let mut args = args.into_iter().map(Into::into).peekable(); let mut config = Config::default(); let mut positional_only = false; while let Some(arg) = args.next() { if positional_only { config.inputs.push(arg); continue; } match arg.as_str() { "--" => positional_only = true, "-v" | "--verbose" => config.verbose = true, "-o" | "--output" => { config.output = Some( args.next() .ok_or(ParseError::MissingValue("--output"))?, ); } "-n" | "--count" => { let value = args .next() .ok_or(ParseError::MissingValue("--count"))?; config.count = value .parse() .map_err(|_| ParseError::InvalidCount(value))?; } _ if arg.starts_with("--output=") => { config.output = Some(arg["--output=".len()..].to_owned()); } _ if arg.starts_with("--count=") => { let value = &arg["--count=".len()..]; config.count = value .parse() .map_err(|_| ParseError::InvalidCount(value.to_owned()))?; } _ if arg.starts_with('-') => return Err(ParseError::UnknownOption(arg)), _ => config.inputs.push(arg), } } Ok(config) } fn print_usage(program: &str) { eprintln!( "Usage: {program} [OPTIONS] [INPUT ...]\n\ Options:\n\ \x20 -v, --verbose Enable verbose output\n\ \x20 -o, --output PATH Set the output path\n\ \x20 -n, --count NUMBER Set a repetition count" ); } fn main() { let mut args = env::args(); let program = args.next().unwrap_or_else(|| "app".to_owned()); match parse_args(args) { Ok(config) => println!("{config:#?}"), Err(error) => { eprintln!("error: {error}"); print_usage(&program); process::exit(2); } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_options_and_inputs() { let config = parse_args([ "--verbose", "--output=result.txt", "-n", "3", "first.txt", "second.txt", ]) .unwrap(); assert_eq!( config, Config { verbose: true, output: Some("result.txt".to_owned()), count: 3, inputs: vec!["first.txt".to_owned(), "second.txt".to_owned()], } ); } #[test] fn double_dash_stops_option_parsing() { let config = parse_args(["--", "--verbose", "-n"]).unwrap(); assert!(!config.verbose); assert_eq!(config.inputs, ["--verbose", "-n"]); } #[test] fn rejects_unknown_options() { assert_eq!( parse_args(["--mystery"]), Err(ParseError::UnknownOption("--mystery".to_owned())) ); } }