use std::{env, process}; #[derive(Debug, PartialEq)] struct Config { verbose: bool, output: Option, input: String, } fn parse_args(args: I) -> Result where I: IntoIterator, { let mut args = args.into_iter(); let mut verbose = false; let mut output = None; let mut input = None; while let Some(arg) = args.next() { match arg.as_str() { "-v" | "--verbose" => verbose = true, "-o" | "--output" => { output = Some( args.next() .ok_or_else(|| format!("{arg} requires a value"))?, ); } "-h" | "--help" => return Err(usage()), "--" => { for value in args { set_input(&mut input, value)?; } break; } _ if arg.starts_with('-') => return Err(format!("unknown option: {arg}")), _ => set_input(&mut input, arg)?, } } Ok(Config { verbose, output, input: input.ok_or_else(|| "missing input file".to_string())?, }) } fn set_input(input: &mut Option, value: String) -> Result<(), String> { if input.replace(value).is_some() { Err("only one input file may be specified".to_string()) } else { Ok(()) } } fn usage() -> String { "Usage: mini-cli [OPTIONS] \n\ Options:\n\ -v, --verbose Enable verbose output\n\ -o, --output Set the output file\n\ -h, --help Show this help" .to_string() } fn main() { match parse_args(env::args().skip(1)) { Ok(config) => { if config.verbose { eprintln!("input: {}", config.input); } println!( "Would process {}{}", config.input, config .output .as_deref() .map(|path| format!(" into {path}")) .unwrap_or_default() ); } Err(message) => { eprintln!("{message}"); 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_input() { let config = parse_args(strings(&["--verbose", "-o", "result.txt", "input.txt"])).unwrap(); assert_eq!( config, Config { verbose: true, output: Some("result.txt".to_string()), input: "input.txt".to_string(), } ); } #[test] fn rejects_unknown_options() { let error = parse_args(strings(&["--nope", "input.txt"])).unwrap_err(); assert_eq!(error, "unknown option: --nope"); } #[test] fn accepts_dash_prefixed_input_after_separator() { let config = parse_args(strings(&["--", "-input.txt"])).unwrap(); assert_eq!(config.input, "-input.txt"); } }