//! A small, dependency-free CLI argument parser. use std::{collections::HashMap, env, fmt, process}; #[derive(Debug, Default, PartialEq, Eq)] struct Arguments { verbose: bool, output: Option, count: usize, inputs: Vec, } #[derive(Debug, PartialEq, Eq)] 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 parsed = Arguments { count: 1, ..Arguments::default() }; let mut aliases = HashMap::new(); aliases.insert("-v", "--verbose"); aliases.insert("-o", "--output"); aliases.insert("-n", "--count"); while let Some(argument) = args.next() { let option = aliases .get(argument.as_str()) .copied() .unwrap_or(argument.as_str()); match option { "--verbose" => parsed.verbose = true, "--output" => { parsed.output = Some( args.next() .ok_or(ParseError::MissingValue("--output"))?, ); } "--count" => { let value = args .next() .ok_or(ParseError::MissingValue("--count"))?; parsed.count = value .parse() .map_err(|_| ParseError::InvalidCount(value))?; } "--" => { parsed.inputs.extend(args); break; } value if value.starts_with('-') => { return Err(ParseError::UnknownOption(value.to_owned())); } value => parsed.inputs.push(value.to_owned()), } } Ok(parsed) } fn print_help(program: &str) { println!( "\ Usage: {program} [OPTIONS] [INPUT...] Options: -v, --verbose Enable verbose output -o, --output PATH Set the output path -n, --count NUMBER Set the repetition count (default: 1) -h, --help Print 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_help(&program); return; } match parse_args(remaining) { Ok(arguments) => { if arguments.verbose { eprintln!("arguments: {arguments:?}"); } for _ in 0..arguments.count { for input in &arguments.inputs { println!("{input}"); } } } Err(error) => { eprintln!("error: {error}"); eprintln!("Try '{program} --help' for more information."); process::exit(2); } } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_options_and_positionals() { let parsed = parse_args([ "-v", "--output", "result.txt", "-n", "3", "first", "second", ]) .unwrap(); assert_eq!( parsed, Arguments { verbose: true, output: Some("result.txt".into()), count: 3, inputs: vec!["first".into(), "second".into()], } ); } #[test] fn double_dash_stops_option_parsing() { let parsed = parse_args(["--", "--not-an-option"]).unwrap(); assert_eq!(parsed.inputs, ["--not-an-option"]); } #[test] fn reports_missing_values() { assert_eq!( parse_args(["--output"]), Err(ParseError::MissingValue("--output")) ); } }