use std::{env, fmt, process}; #[derive(Debug, Default, PartialEq)] struct Args { verbose: bool, output: Option, input: Option, } #[derive(Debug, PartialEq)] enum ParseError { MissingValue(&'static str), UnknownOption(String), UnexpectedArgument(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::UnknownOption(option) => write!(f, "unknown option: {option}"), Self::UnexpectedArgument(arg) => write!(f, "unexpected argument: {arg}"), } } } // Parses: [-v|--verbose] [-o|--output FILE] [INPUT] fn parse_args(args: I) -> Result where I: IntoIterator, S: Into, { let mut args = args.into_iter().map(Into::into); let mut parsed = Args::default(); let mut positional_only = false; while let Some(arg) = args.next() { if !positional_only { match arg.as_str() { "--" => { positional_only = true; continue; } "-v" | "--verbose" => { parsed.verbose = true; continue; } "-o" | "--output" => { parsed.output = Some( args.next() .ok_or(ParseError::MissingValue("--output"))?, ); continue; } _ if arg.starts_with("--output=") => { parsed.output = Some(arg["--output=".len()..].to_owned()); continue; } _ if arg.starts_with('-') => { return Err(ParseError::UnknownOption(arg)); } _ => {} } } if parsed.input.is_some() { return Err(ParseError::UnexpectedArgument(arg)); } parsed.input = Some(arg); } Ok(parsed) } fn print_usage(program: &str) { println!("Usage: {program} [OPTIONS] [INPUT]"); println!(); println!("Options:"); println!(" -v, --verbose Enable verbose output"); println!(" -o, --output FILE Write output to FILE"); println!(" -h, --help Show this help"); } fn main() { let mut raw = env::args(); let program = raw.next().unwrap_or_else(|| "app".to_owned()); let remaining: Vec = raw.collect(); if remaining.iter().any(|arg| arg == "-h" || arg == "--help") { print_usage(&program); return; } match parse_args(remaining) { Ok(args) => { if args.verbose { eprintln!("parsed arguments: {args:?}"); } let input = args.input.as_deref().unwrap_or(""); let output = args.output.as_deref().unwrap_or(""); println!("input: {input}"); println!("output: {output}"); } Err(error) => { eprintln!("{program}: {error}"); eprintln!("Try '{program} --help' for more information."); process::exit(2); } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_options_and_input() { let args = parse_args(["-v", "--output=result.txt", "input.txt"]).unwrap(); assert_eq!( args, Args { verbose: true, output: Some("result.txt".to_owned()), input: Some("input.txt".to_owned()), } ); } #[test] fn rejects_unknown_options() { assert_eq!( parse_args(["--quiet"]), Err(ParseError::UnknownOption("--quiet".to_owned())) ); } #[test] fn double_dash_allows_dash_prefixed_input() { let args = parse_args(["--", "-input.txt"]).unwrap(); assert_eq!(args.input.as_deref(), Some("-input.txt")); } }