use std::{env, fmt, process}; #[derive(Debug, PartialEq)] struct Options { verbose: bool, output: Option, count: u32, inputs: Vec, } #[derive(Debug, PartialEq)] struct ParseError(String); impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } fn parse_args(args: I) -> Result where I: IntoIterator, { let mut args = args.into_iter(); let mut options = Options { verbose: false, output: None, count: 1, inputs: Vec::new(), }; let mut positional_only = false; while let Some(arg) = args.next() { if positional_only { options.inputs.push(arg); continue; } match arg.as_str() { "--" => positional_only = true, "-v" | "--verbose" => options.verbose = true, "-o" | "--output" => { options.output = Some( args.next() .ok_or_else(|| ParseError(format!("{arg} requires a value")))?, ); } "-c" | "--count" => { let value = args .next() .ok_or_else(|| ParseError(format!("{arg} requires a value")))?; options.count = value .parse() .map_err(|_| ParseError(format!("invalid count: {value}")))?; } "-h" | "--help" => { print_usage(); process::exit(0); } _ if arg.starts_with('-') => { return Err(ParseError(format!("unknown option: {arg}"))); } _ => options.inputs.push(arg), } } if options.inputs.is_empty() { return Err(ParseError("at least one input is required".into())); } Ok(options) } fn print_usage() { println!( "Usage: mini-cli [OPTIONS] ...\n\ \n\ Options:\n\ \x20 -v, --verbose Enable verbose output\n\ \x20 -o, --output Set the output path\n\ \x20 -c, --count Set the repetition count (default: 1)\n\ \x20 -h, --help Show this help" ); } fn main() { match parse_args(env::args().skip(1)) { Ok(options) => { if options.verbose { eprintln!("options: {options:?}"); } for input in &options.inputs { for _ in 0..options.count { println!("{input}"); } } } Err(error) => { eprintln!("error: {error}\n"); print_usage(); process::exit(2); } } } #[cfg(test)] mod tests { use super::*; fn strings(values: &[&str]) -> Vec { values.iter().map(|value| value.to_string()).collect() } #[test] fn parses_options_and_inputs() { let options = parse_args(strings(&[ "--verbose", "-o", "result.txt", "--count", "3", "first", "second", ])) .unwrap(); assert_eq!( options, Options { verbose: true, output: Some("result.txt".into()), count: 3, inputs: strings(&["first", "second"]), } ); } #[test] fn double_dash_stops_option_parsing() { let options = parse_args(strings(&["--", "-literal"])).unwrap(); assert_eq!(options.inputs, strings(&["-literal"])); } #[test] fn rejects_unknown_options() { let error = parse_args(strings(&["--unknown", "input"])).unwrap_err(); assert_eq!(error, ParseError("unknown option: --unknown".into())); } }