use std::{env, fmt, path::PathBuf, process}; #[derive(Debug, PartialEq)] struct Args { input: PathBuf, output: Option, count: u32, verbose: bool, } #[derive(Debug, PartialEq)] enum ParseError { HelpRequested, MissingInput, MissingValue(&'static str), InvalidCount(String), UnknownOption(String), UnexpectedArgument(String), } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::HelpRequested => write!(f, "help requested"), Self::MissingInput => write!(f, "missing input file"), 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}"), Self::UnexpectedArgument(arg) => write!(f, "unexpected argument: {arg}"), } } } fn parse_args(arguments: I) -> Result where I: IntoIterator, S: Into, { let mut args = arguments.into_iter().map(Into::into); let mut input = None; let mut output = None; let mut count = 1; let mut verbose = false; let mut positional_only = false; while let Some(arg) = args.next() { if !positional_only { match arg.as_str() { "--" => { positional_only = true; continue; } "-h" | "--help" => return Err(ParseError::HelpRequested), "-v" | "--verbose" => { verbose = true; continue; } "-o" | "--output" => { output = Some(PathBuf::from( args.next().ok_or(ParseError::MissingValue("--output"))?, )); continue; } "-n" | "--count" => { let value = args.next().ok_or(ParseError::MissingValue("--count"))?; count = value .parse::() .ok() .filter(|value| *value > 0) .ok_or_else(|| ParseError::InvalidCount(value.clone()))?; continue; } _ if arg.starts_with('-') => return Err(ParseError::UnknownOption(arg)), _ => {} } } if input.replace(PathBuf::from(&arg)).is_some() { return Err(ParseError::UnexpectedArgument(arg)); } } Ok(Args { input: input.ok_or(ParseError::MissingInput)?, output, count, verbose, }) } fn usage(program: &str) { println!( "Usage: {program} [OPTIONS] \n\ \n\ Options:\n\ \x20 -o, --output Write to FILE\n\ \x20 -n, --count Repeat N times (default: 1)\n\ \x20 -v, --verbose Enable verbose output\n\ \x20 -h, --help Show this help" ); } fn main() { let mut raw = env::args(); let program = raw.next().unwrap_or_else(|| "app".to_owned()); match parse_args(raw) { Ok(args) => println!("{args:?}"), Err(ParseError::HelpRequested) => usage(&program), Err(error) => { eprintln!("error: {error}\n"); usage(&program); process::exit(2); } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_options_and_input() { let args = parse_args(["-v", "--count", "3", "-o", "out.txt", "in.txt"]).unwrap(); assert_eq!( args, Args { input: PathBuf::from("in.txt"), output: Some(PathBuf::from("out.txt")), count: 3, verbose: true, } ); } #[test] fn rejects_unknown_options() { assert_eq!( parse_args(["--wat", "in.txt"]), Err(ParseError::UnknownOption("--wat".to_owned())) ); } #[test] fn double_dash_allows_dash_prefixed_input() { let args = parse_args(["--", "-input.txt"]).unwrap(); assert_eq!(args.input, PathBuf::from("-input.txt")); } }