use std::{env, fmt, process}; #[derive(Debug, PartialEq)] struct Args { input: Option, output: Option, count: u32, verbose: bool, } #[derive(Debug, PartialEq)] enum ParseError { HelpRequested, 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::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}"), } } } // Parses arguments after the program name. fn parse_args(arguments: I) -> Result where I: IntoIterator, S: Into, { let mut arguments = arguments.into_iter().map(Into::into); let mut parsed = Args { input: None, output: None, count: 1, verbose: false, }; let mut options_enabled = true; while let Some(argument) = arguments.next() { if options_enabled { match argument.as_str() { "--" => { options_enabled = false; continue; } "-h" | "--help" => return Err(ParseError::HelpRequested), "-v" | "--verbose" => { parsed.verbose = true; continue; } "-o" | "--output" => { parsed.output = Some( arguments .next() .ok_or(ParseError::MissingValue("--output"))?, ); continue; } "-n" | "--count" => { let value = arguments .next() .ok_or(ParseError::MissingValue("--count"))?; parsed.count = value .parse() .map_err(|_| ParseError::InvalidCount(value))?; continue; } _ if argument.starts_with('-') => { return Err(ParseError::UnknownOption(argument)); } _ => {} } } if parsed.input.is_some() { return Err(ParseError::UnexpectedArgument(argument)); } parsed.input = Some(argument); } Ok(parsed) } fn print_usage(program: &str) { println!( "Usage: {program} [OPTIONS] [INPUT] Options: -o, --output PATH Write to PATH -n, --count N Repeat N times (default: 1) -v, --verbose Enable verbose output -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) => { if args.verbose { eprintln!("arguments: {args:?}"); } let input = args.input.as_deref().unwrap_or("world"); let text = format!("Hello, {input}!\n").repeat(args.count as usize); if let Some(path) = args.output { if let Err(error) = std::fs::write(&path, text) { eprintln!("{program}: cannot write {path:?}: {error}"); process::exit(1); } } else { print!("{text}"); } } Err(ParseError::HelpRequested) => print_usage(&program), 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", "--count", "3", "-o", "out.txt", "Alice"]).unwrap(); assert_eq!( args, Args { input: Some("Alice".into()), output: Some("out.txt".into()), count: 3, verbose: true, } ); } #[test] fn double_dash_allows_dash_prefixed_input() { let args = parse_args(["--", "-filename"]).unwrap(); assert_eq!(args.input.as_deref(), Some("-filename")); } #[test] fn rejects_invalid_count() { assert_eq!( parse_args(["--count", "many"]), Err(ParseError::InvalidCount("many".into())) ); } }