use std::fmt::Debug;
use std::fmt::Formatter;
use std::slice::Iter;
use error::Error;
pub enum Opt<'a> {
Switch(&'a str, &'a mut bool),
Arg(&'a str, &'a mut String),
SubSwitch(&'a str, FnSubSwitch<'a>),
SubArg(&'a str, FnSubArg<'a>),
Free(FnFree<'a>),
}
pub mod error {
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Clone,Debug,Eq,PartialEq)]
pub enum Error {
NeedArgument(String),
Unexpected(String),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
match self {
Error::NeedArgument(s) => write!(f, "need argument for '{}'", s),
Error::Unexpected(s) => write!(f, "unexpected argument '{}'", s),
}
}
}
}
pub type FnSubSwitch<'a> = &'a dyn Fn(&str);
pub type FnSubArg<'a> = &'a dyn Fn(&str,&str);
pub type FnFree<'a> = &'a dyn Fn(&str);
impl Debug for Opt<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Opt::Switch(x, b) => f.debug_tuple("Opt::Switch").field(x).field(b).finish(),
Opt::Arg(x, s) => f.debug_tuple("Opt::Arg").field(x).field(s).finish(),
Opt::SubArg(x, _) => f.debug_tuple("Opt::SubArg").field(x).field(&"Fn").finish(),
Opt::SubSwitch(x, _) => f.debug_tuple("Opt::SubSwitch").field(x).field(&"Fn").finish(),
Opt::Free(_) => f.debug_tuple("Opt::Free").field(&"Fn").finish(),
}
}
}
fn split_off(slice: &str) -> Result<(&str, &str),&str> {
let split = slice.split_once('|');
if let Some((s1, s2)) = split {
Ok((s1, s2))
}
else {
Err(slice)
}
}
fn match_short(ch: char, switch: &str) -> bool {
let mut buf = [0u8; 4];
let ch: &str = ch.encode_utf8(&mut buf);
let split = split_off(switch);
if let Ok((s1, s2)) = split {
if s1 == ch {
return true;
}
else if s2 == ch {
return true;
}
}
else if let Err(s) = split {
if s == ch {
return true;
}
}
return false;
}
fn match_long(arg: &str, switch: &str) -> bool {
if arg.chars().count() == 1 {
return false;
}
let split = split_off(switch);
if let Ok((s1, s2)) = split {
if s1 == arg {
return true;
}
else if s2 == arg {
return true;
}
}
else if let Err(s) = split {
if s == arg {
return true;
}
}
return false;
}
fn handle_short_opt(args_iter: &mut Iter<&str>, arg: &str, opts: &mut [Opt]) -> Result<bool,Error> {
let mut chars_iter = arg.chars().peekable();
let mut parsed = false;
while let Some(ch) = chars_iter.next() {
let mut iter = opts.iter_mut();
while let Some(opt) = iter.next() {
if let Opt::Switch(switch, &mut ref mut b) = opt {
if match_short(ch, switch) {
*b = true;
parsed = true;
}
}
else if let Opt::Arg(switch, &mut ref mut s) = opt {
if match_short(ch, switch) {
if chars_iter.peek().is_some() {
let a: String = chars_iter.collect();
*s = a;
return Ok(true);
}
else if let Some(a) = args_iter.next() {
*s = a.to_string();
parsed = true;
}
else {
return Err(Error::NeedArgument(format!("-{}", arg)));
}
}
}
else if let Opt::SubArg(switch, func) = opt {
if match_short(ch, switch) {
if let Some(a) = args_iter.next() {
func(arg, *a);
parsed = true;
}
else {
return Err(Error::NeedArgument(format!("-{}", arg)));
}
}
}
else if let Opt::SubSwitch(switch, func) = opt {
if match_short(ch, switch) {
func(arg);
parsed = true;
}
}
}
}
if parsed {
return Ok(true);
}
Err(Error::Unexpected(format!("-{}", arg)))
}
fn extract_equals(arg: &str) -> Option<(&str,&str)> {
let split = arg.split_once('=');
if let Some((arg, equals_value)) = split {
Some((arg, equals_value))
}
else {
None
}
}
fn handle_long_opt(args_iter: &mut Iter<&str>, arg: &str, opts: &mut [Opt]) -> Result<bool,Error> {
for opt in &mut *opts {
if let Opt::Switch(switch, &mut ref mut b) = opt {
if switch.chars().count() == 1 {
continue;
}
if match_long(arg, switch) {
*b = true;
return Ok(true);
}
}
else if let Opt::Arg(switch, &mut ref mut s) = opt {
let (arg, equals_value) = extract_equals(arg).unwrap_or((arg, ""));
if match_long(arg, switch) {
if equals_value != "" {
*s = (equals_value).to_string();
return Ok(true);
}
else if let Some(a) = args_iter.next() {
*s = (*a).to_string();
return Ok(true);
}
else {
return Err(Error::NeedArgument(format!("--{}", arg)));
}
}
}
else if let Opt::SubArg(switch, func) = opt {
let (arg, equals_value) = extract_equals(arg).unwrap_or((arg, ""));
if match_long(arg, switch) {
if equals_value != "" {
func(arg, equals_value);
return Ok(true);
}
else if let Some(a) = args_iter.next() {
func(arg, *a);
return Ok(true);
}
else {
return Err(Error::NeedArgument(format!("--{}", arg)));
}
}
}
else if let Opt::SubSwitch(switch, func) = opt {
if match_long(arg, switch) {
func(arg);
return Ok(true);
}
}
}
Err(Error::Unexpected(format!("--{}", arg)))
}
fn handle_free_arg(arg: &str, opts: &mut [Opt]) -> Result<(),Error> {
for opt in opts.iter_mut() {
if let Opt::Free(f) = opt {
let f: FnFree = f;
f(arg);
return Ok(());
}
}
if arg == "--" {
return Ok(());
}
Err(Error::Unexpected(arg.to_string()))
}
pub fn get_options_env(opts: &mut [Opt]) -> Result<(),Error> {
let env_args: Vec<String> = std::env::args().collect();
get_options(opts, &env_args)
}
pub fn get_options(opts: &mut [Opt], args: &[String]) -> Result<(),Error> {
let args: Vec<&str> = args.iter().map(|x| x.as_ref()).collect();
get_options_str(opts, &args)
}
pub fn get_options_str(opts: &mut [Opt], args: &[&str]) -> Result<(),Error> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if *arg == "--" {
handle_free_arg(arg, opts)?;
while let Some(arg) = iter.next() {
handle_free_arg(arg, opts)?;
return Ok(());
}
}
else if arg.starts_with("--") {
let mut chars = arg.chars();
chars.next();
chars.next();
let arg = chars.as_str();
handle_long_opt(&mut iter, arg, opts)?;
}
else if arg.starts_with("-") {
let mut chars = arg.chars();
chars.next();
let arg = chars.as_str();
handle_short_opt(&mut iter, arg, opts)?;
}
else {
handle_free_arg(arg, opts)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
#[test]
fn test_no_input() {
let args = [];
let result = get_options_str(&mut [], &args);
assert!(result.is_ok());
}
#[test]
fn test_short_switch() {
let args = ["-a"];
let mut a_opt = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut a_opt),
], &args);
assert!(result.is_ok());
assert!(a_opt);
}
#[test]
fn test_short_switch_correct() {
let args = ["-c"];
let mut a_opt = false;
let mut b_opt = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut a_opt),
Opt::Switch("b", &mut b_opt),
], &args);
assert!(!a_opt);
assert!(!b_opt);
assert!(result.is_err());
}
#[test]
fn test_long_switch() {
let args = ["--long-flag"];
let mut long_flag = false;
let mut another_flag = false;
let result = get_options_str(&mut [
Opt::Switch("long-flag", &mut long_flag),
Opt::Switch("another-flag", &mut another_flag),
], &args);
assert!(result.is_ok());
assert!(long_flag);
assert!(!another_flag);
}
#[test]
fn test_long_and_short_switch() {
let args = ["-a", "--long-flag"];
let mut short_flag = false;
let mut another_short_flag = false;
let mut long_flag = false;
let mut another_flag = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut short_flag),
Opt::Switch("2", &mut another_short_flag),
Opt::Switch("long-flag", &mut long_flag),
Opt::Switch("another-flag", &mut another_flag),
], &args);
assert!(result.is_ok());
assert!(long_flag);
assert!(!another_flag);
}
#[test]
fn test_short_or_long_flag() {
let args = ["--a-flag"];
let mut flag = false;
let mut flag2 = false;
let result = get_options_str(&mut [
Opt::Switch("a|a-flag", &mut flag),
Opt::Switch("b|b-flag", &mut flag2),
], &args);
assert!(result.is_ok());
assert!(flag);
assert!(!flag2);
let args = ["-a"];
let mut flag = false;
let mut flag2 = false;
let result = get_options_str(&mut [
Opt::Switch("a|a-flag", &mut flag),
Opt::Switch("b|b-flag", &mut flag2),
], &args);
assert!(result.is_ok());
assert!(flag);
assert!(!flag2);
}
#[test]
fn test_option_argument() {
let args = ["-a", "foo bar"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert!(result.is_ok());
assert_eq!(opt, "foo bar");
let args = ["--a-flag", "foo bar"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert!(result.is_ok());
assert_eq!(opt, "foo bar");
}
#[test]
fn test_option_argument_take_last() {
let args = ["-a", "foo bar", "--a-flag", "bar baz"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert!(result.is_ok());
assert_eq!(opt, "bar baz");
let args = ["--a-flag", "foo bar", "-a", "bar baz"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert!(result.is_ok());
assert_eq!(opt, "bar baz");
}
#[test]
fn test_option_argument_underflow() {
let args = ["-a"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert_eq!(result, Err(Error::NeedArgument("-a".to_string())));
assert_eq!(result.unwrap_err().to_string(), "need argument for '-a'");
let args = ["--a-flag"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert_eq!(result, Err(Error::NeedArgument("--a-flag".to_string())));
}
#[test]
fn test_callback() {
let args = ["-f", "foo", "-f", "bar"];
let opt = RefCell::new(String::new());
let result = get_options_str(&mut [
Opt::SubArg("flag|f", &|_arg, val| { opt.replace_with(|opt| format!("{}{}", opt, val)); }),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(opt.into_inner(), "foobar");
let args = ["--flag", "bar", "--flag", "foo"];
let opt = RefCell::new(String::new());
let result = get_options_str(&mut [
Opt::SubArg("flag|f", &|_arg, val| { opt.replace_with(|opt| format!("{}{}", opt, val)); }),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(opt.into_inner(), "barfoo");
}
#[test]
fn test_callback_switch() {
let args = ["-v", "-f", "-v", "-e", "-g"];
let opt: RefCell<i64> = 0.into();
let sub: &dyn for<'a> Fn(&'a str) = &|arg| {
if arg == "f" {
*opt.borrow_mut() += 2;
}
else if arg == "g" {
*opt.borrow_mut() -= 3;
}
};
let result = get_options_str(&mut [
Opt::SubSwitch("verbose|v", &mut |_arg| {
*opt.borrow_mut() += 1;
}),
Opt::SubSwitch("e", &mut |_arg| {
*opt.borrow_mut() += 4;
}),
Opt::SubSwitch("f|g", sub),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(opt, 5.into());
}
#[test]
fn test_long_equals() {
let args = ["--a-flag=boofar"];
let mut opt = String::new();
let result = get_options_str(&mut [
Opt::Arg("a|a-flag", &mut opt),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(opt, "boofar");
}
#[test]
fn test_free_arguments() {
let args = ["a", "bb", "ccc"];
let free = RefCell::new(vec![]);
let sub: FnFree = &|s| {
free.borrow_mut().push(s.to_string());
};
let result = get_options_str(&mut [
Opt::Free(sub),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(free.into_inner(), vec!["a", "bb", "ccc"]);
}
#[test]
fn test_double_dash_no_omit() {
let args = ["--"];
let free = RefCell::new(vec![]);
let sub: FnFree = &|s| {
free.borrow_mut().push(s.to_string());
};
let result = get_options_str(&mut [
Opt::Free(sub),
], &args);
assert_eq!(result, Ok(()));
assert!(free.into_inner().len() == 1);
}
#[test]
fn test_double_dash_escape() {
let args = ["--", "-f"];
let mut flag = false;
let free = RefCell::new(vec![]);
let sub: FnFree = &|s| {
free.borrow_mut().push(s.to_string());
};
let result = get_options_str(&mut [
Opt::Switch("f|flag", &mut flag),
Opt::Free(sub),
], &args);
let free = free.into_inner();
assert_eq!(result, Ok(()));
assert!(!flag);
assert!(free.len() == 2);
assert_eq!(free, vec!("--", "-f"));
}
#[test]
fn test_switch_bundling() {
let args = ["-ab"];
let mut flag_a = false;
let mut flag_b = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut flag_a),
Opt::Switch("b", &mut flag_b),
], &args);
assert!(result.is_ok());
assert!(flag_a);
assert!(flag_b);
}
#[test]
fn test_switch_arg_bundling() {
let args = ["-ab", "2"];
let mut flag_a = false;
let mut flag_b = String::new();
let result = get_options_str(&mut [
Opt::Switch("a", &mut flag_a),
Opt::Arg("b", &mut flag_b),
], &args);
assert!(result.is_ok());
assert!(flag_a);
assert_eq!(flag_b, "2");
}
#[test]
fn test_arg_nospace() {
let args = ["-a7"];
let mut flag_a = String::new();
let result = get_options_str(&mut [
Opt::Arg("a", &mut flag_a),
], &args);
assert!(result.is_ok());
assert_eq!(flag_a, "7");
}
#[test]
fn test_arg_nospace_longer() {
let args = ["-aFooBarBaz"];
let mut flag_a = String::new();
let result = get_options_str(&mut [
Opt::Arg("a", &mut flag_a),
], &args);
assert!(result.is_ok());
assert_eq!(flag_a, "FooBarBaz");
}
#[test]
fn test_bundle_switch_arg_nospace() {
let args = ["-ba8"];
let mut flag_a = String::new();
let mut flag_b = false;
let result = get_options_str(&mut [
Opt::Arg("a", &mut flag_a),
Opt::Switch("b", &mut flag_b),
], &args);
assert!(result.is_ok());
assert_eq!(flag_a, "8");
assert!(flag_b);
}
#[test]
fn test_bundle_arg_switch_nospace() {
let args = ["-ab8"];
let mut flag_a = String::new();
let mut flag_b = false;
let result = get_options_str(&mut [
Opt::Arg("a", &mut flag_a),
Opt::Switch("b", &mut flag_b),
], &args);
assert!(result.is_ok());
assert_eq!(flag_a, "b8");
assert!(!flag_b);
}
#[test]
fn test_flag_invalid() {
let args = ["-f"];
let mut flag_a = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut flag_a),
], &args);
assert_eq!(result, Err(Error::Unexpected("-f".to_string())));
assert!(!flag_a);
let args = ["--foo", "7"];
let mut flag_a = false;
let free = RefCell::new(vec![]);
let sub: FnFree = &|s| {
free.borrow_mut().push(s.to_string());
};
let result = get_options_str(&mut [
Opt::Switch("a", &mut flag_a),
Opt::Free(sub),
], &args);
assert_eq!(result, Err(Error::Unexpected("--foo".to_string())));
assert_eq!(result.unwrap_err().to_string(), "unexpected argument '--foo'");
assert_eq!(free.into_inner().len(), 0);
assert!(!flag_a);
}
#[test]
fn test_undeclared_free() {
let args = ["foo", "bar", "baz"];
let mut flag_a = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut flag_a),
], &args);
assert_eq!(result, Err(Error::Unexpected("foo".to_string())));
assert!(!flag_a);
let args = ["--", "-7"];
let mut flag_a = false;
let result = get_options_str(&mut [
Opt::Switch("a", &mut flag_a),
], &args);
assert!(!flag_a);
assert_eq!(result, Err(Error::Unexpected("-7".to_string())));
assert_eq!(result.unwrap_err().to_string(), "unexpected argument '-7'");
}
#[test]
fn test_callback_underfloaw() {
let args = ["--foo", "7", "--bar"];
let flag_a = RefCell::new(String::new());
let flag_b = RefCell::new(String::new());
let result = get_options_str(&mut [
Opt::SubArg("foo", &mut |_arg, val| {
flag_a.replace_with(|_| val.to_owned());
}),
Opt::SubArg("bar", &mut |_arg, val| {
flag_b.replace_with(|_| val.to_owned());
}),
], &args);
assert_eq!(result, Err(Error::NeedArgument("--bar".to_string())));
assert_eq!(flag_b.into_inner(), "");
}
#[test]
fn test_mutually_exclusive() {
let args = ["--foo", "--baz", "--bar", "--baz"];
let command = RefCell::new(String::new());
let sub: &dyn for<'a> Fn(&'a str) = &|arg| {
let mut command = command.borrow_mut();
*command = format!("command-{}", arg);
};
let result = get_options_str(&mut [
Opt::SubSwitch("foo", sub),
Opt::SubSwitch("bar", sub),
Opt::SubSwitch("baz", sub),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(command.into_inner(), "command-baz");
}
#[test]
fn test_sledgehammer_example() {
let args: Vec<String> = vec!["/usr/local/bin/backup-profile", "--list", "-n", "--backup-dir=/mnt/usr1/backups", "--backup-dir", "/mnt/usr1/backups-test", "-v", "--", "-f.txt"].into_iter().map(|x| x.to_string()).collect();
let prog = args[0].clone();
let args: Vec<String> = args.into_iter().skip(1).collect();
let usage = || {
let prog = std::path::Path::new(&prog).file_name().unwrap().to_string_lossy();
println!("usage: {prog} [-hlcpnv] [-b BACKUP] [--] [TAR_ARGUMENTS]");
println!(" backup firefox profiles on win32 from cygwin");
println!(" -h,--help display this usage");
println!(" -b,--backup-dir BACKUP output backup files to BACKUP (default '.')");
println!(" -l,--list list profiles and their paths");
println!(" -n,--dry-run perform dry run with commands printed");
println!(" -p,--print print Firefox configuration root");
println!(" -v,--verbose be noisier");
};
let mut backup_dir = String::new();
let mut dry_run = false;
let free_args = RefCell::new(vec![]);
let mut list = false;
let mut print = false;
let mut verbose = false;
let sub: FnFree = &|s| {
free_args.borrow_mut().push(s.to_string());
};
let result = get_options(&mut [
Opt::SubSwitch("help|h", &mut |_| { usage(); std::process::exit(0); }),
Opt::Switch ("list|l", &mut list),
Opt::Switch ("p|print", &mut print),
Opt::Switch ("dry-run|n", &mut dry_run),
Opt::Arg ("backup-dir|b", &mut backup_dir),
Opt::Switch ("verbose|v", &mut verbose),
Opt::Free (sub),
], &args);
if let Err(ref err) = result {
eprintln!("ERROR: parsing command line arguments: {err}");
}
assert_eq!(backup_dir, "/mnt/usr1/backups-test");
assert!(dry_run);
assert_eq!(free_args.into_inner(), vec!["--", "-f.txt"]);
assert!(list);
assert!(!print);
assert!(verbose);
assert_eq!(result, Ok(()));
}
}