//use std::cmp::Ordering; #[derive(Copy, Clone)] pub enum Nargs { None, Count(usize), Remainder /*Optional, *ZeroOrMore, *OneOrMore */ } impl Default for Nargs { fn default() -> Self { Nargs::None } } type Handler = fn(spec: &Spec, ctx: &mut C, args: &Vec); /* type HandlerStr = fn(spec: &Spec, ctx: &mut C, arg: &str); type HandlerString = fn(spec: &Spec, ctx: &mut C, arg: String); type HandlerStrVec = fn(spec: &Spec, ctx: &mut C, args: I) where I: IntoIterator, S: AsRef; enum Handler { Str(HandlerStr), String(HandlerString) } */ /// Parser option/argument specification builder. //#[derive(Default)] pub struct Builder { sopt: Option, lopt: Option, name: Option, nargs: Nargs, exit: bool, required: bool, metanames: Vec, desc: Vec, /// Whether to hide this entry from the help text. hidden: bool } impl Builder { pub fn new() -> Self { Builder { sopt: None, lopt: None, name: None, nargs: Nargs::None, exit: false, required: false, metanames: Vec::new(), desc: Vec::new(), hidden: false } } /// Make argument specification a single-character option. pub fn sopt(&mut self, sopt: char) -> &mut Self { self.sopt = Some(sopt); self } /// Make argument specification a long option name. pub fn lopt(&mut self, lopt: &str) -> &mut Self { self.lopt = Some(String::from(lopt)); self } /// Assign argument specification a name. This is required for positional /// arguments. pub fn name(&mut self, name: &str) -> &mut Self { self.name = Some(String::from(name)); self } /// Declare that this argument specification takes arguments. pub fn nargs(&mut self, nargs: Nargs, metanames: I) -> &mut Self where I: IntoIterator, S: AsRef { self.nargs = nargs; self.metanames(metanames); self } /// Assign meta-names to the arguments. pub fn metanames(&mut self, metanames: I) -> &mut Self where I: IntoIterator, S: AsRef { self.metanames.clear(); let nargs = match self.nargs { Nargs::Remainder => 1, Nargs::Count(n) => n, _ => 0 }; let names = metanames .into_iter() .map(|x| String::from(x.as_ref())) .collect::>(); if nargs > 0 { for i in 0..nargs { if i < names.len() { self.metanames.push(names[i].clone()); } else { self.metanames.push("ARG".to_string()); } } } self } /// Specify if encountering this argument specification should terminate the /// parser. /// /// This is useful for options that should terminate normal program /// behavor, such as `--help` or if a positional argumen has been encountered /// which should abort the parser because the rest of the arguments should be /// parsed by a different parser. pub fn exit(&mut self, exit: bool) -> &mut Self { self.exit = exit; self } /// Tell the parser that the argument must be processed. This is only useful /// for positional arguments. pub fn required(&mut self, req: bool) -> &mut Self { self.required = req; self } /// Hidden arguments exist and work as usual, but they are not displayed in /// the help screen. pub fn hidden(&mut self, hidden: bool) -> &mut Self { self.hidden = hidden; self } pub fn help(&mut self, text: I) -> &mut Self where I: IntoIterator, S: AsRef { for x in text.into_iter() { //println!("moo: {}", x.as_ref()); self.desc.push(String::from(x.as_ref())); //println!("{:?}", self.desc); } /* self.desc.clear(); //text.into_iter().map(|x| self.desc.push(String::from(x.as_ref()))); text.into_iter().map(|x| println!("moo: {:?}", x.as_ref())); */ self } pub fn build(&self, proc: Handler) -> Spec { Spec { sopt: self.sopt, lopt: self.lopt.clone(), name: self.name.clone(), nargs: self.nargs.clone(), exit: self.exit, required: self.required, metanames: self.metanames.clone(), desc: self.desc.clone(), hidden: self.hidden, proc } } } /// Option/argument specification. //#[derive(Default)] pub struct Spec { /// Optional short option. This must be unique within a `[Parser]` context. /// If this is `Some()` value then this is not a positional argument. pub(crate) sopt: Option, /// Optional long option. This must be unique within a `[Parser]` context. /// If this is `Some()` value then this is not a positional argument. pub(crate) lopt: Option, /// Optional argument name. If this is `Some()` value and both `[sopt]` and /// `[lopt]` are None then this is a positional argument. pub(crate) name: Option, nargs: Nargs, pub(crate) exit: bool, required: bool, metanames: Vec, desc: Vec, hidden: bool, pub(crate) proc: Handler } impl Spec { /// Return a boolean indicating whether this arg spec is configured to /// capture all the remaining arguments. pub fn is_capture_rest(&self) -> bool { match self.nargs { Nargs::None => false, Nargs::Count(_n) => false, Nargs::Remainder => true } } /// Return boolean indicating whether this arg spec will abort the parser. pub fn is_exit(&self) -> bool { self.exit } /// Return boolean indicating whether this arg spec represents an "option". /// /// "Options" come in two forms: short or long /// /// Short options are in the form "", /// like "-h", "-f". /// /// Long options are in the form "", like "--help", /// --file". /// /// This function will return true for either form. pub fn is_opt(&self) -> bool { self.sopt.is_some() || self.lopt.is_some() } /// Return boolean indicating whether this arg is a positional argument. pub fn is_pos(&self) -> bool { self.sopt.is_none() && self.lopt.is_none() } pub fn is_req(&self) -> bool { self.required } pub fn is_hidden(&self) -> bool { self.hidden } // ToDo: Don't panic!(), return Result instead. pub fn get_nargs(&self) -> usize { match self.nargs { Nargs::None => 0, Nargs::Count(n) => n, Nargs::Remainder => { panic!("Can't get number of arguments for a capture-all spec."); } } } pub fn req_args(&self) -> bool { match self.nargs { Nargs::None => false, Nargs::Count(n) => { if n > 0 { return true; } false } Nargs::Remainder => false } } /// Generate a string representation of a short option. /// Does not include any arguments. /// /// Examples: "-h", "-f" fn get_sopt_str(&self) -> Option { if let Some(sopt) = self.sopt { let mut ret = '-'.to_string(); let soptstr = sopt.to_string(); ret.push_str(&soptstr); return Some(ret); } None } /// Generate a string representation of a long option. /// Does not include any arguments. /// /// Examples: "--help", "--file" fn get_lopt_str(&self) -> Option { if let Some(ref lopt) = self.lopt { let mut ret = "--".to_string(); ret.push_str(&lopt); return Some(ret); } None } fn get_joined_meta_str(&self) -> Option { match self.nargs { Nargs::None => None, Nargs::Count(_n) => Some(self.metanames.join(" ")), Nargs::Remainder => { // ARG [ARG ...] let metaname = if self.metanames.len() > 0 { &self.metanames[0] } else { "ARG" }; Some(metaname.to_string()) } } } /// Get a short option argument string. /// /// Example formats: /// - Some("-h") /// - Some("-f FILE") /// - Some("-p XCOORD YCOORD") fn get_soptarg_str(&self) -> Option { if let Some(optstr) = self.get_sopt_str() { let mut ret = optstr.to_owned(); if let Some(metastr) = self.get_joined_meta_str() { ret.push_str(" "); ret.push_str(&metastr); } return Some(ret); } None } fn get_loptarg_str(&self) -> Option { if let Some(optstr) = self.get_lopt_str() { let mut ret = optstr.to_owned(); if let Some(metastr) = self.get_joined_meta_str() { ret.push_str(" "); ret.push_str(&metastr); } return Some(ret); } None } #[cfg(test)] fn get_sopt_usage_str(&self) -> Option { if let Some(optstr) = self.get_soptarg_str() { let mut ret: String; if self.required { ret = String::from("<"); } else { ret = String::from("["); } ret.push_str(&optstr); if self.required { ret.push_str(">"); } else { ret.push_str("]"); } return Some(ret); } None } #[cfg(test)] fn get_lopt_usage_str(&self) -> Option { if let Some(optstr) = self.get_loptarg_str() { let mut ret: String; if self.required { ret = String::from("<"); } else { ret = String::from("["); } ret.push_str(&optstr); if self.required { ret.push_str(">"); } else { ret.push_str("]"); } return Some(ret); } None } // "-f FILE, --file FILE" // "-f FILE" // "--file FILE" pub fn get_opts_usage_str(&self) -> String { let mut args: Vec = Vec::new(); if let Some(lstr) = self.get_soptarg_str() { args.push(lstr); } if let Some(rstr) = self.get_loptarg_str() { args.push(rstr); } if args.len() == 0 { if let Some(posarg) = self.get_joined_meta_str() { args.push(posarg); } } return args.join(", "); } /// Generate a string that shows a representation of this argument /// specification suitable for use in a "Usage: " string. /// /// Required parameters are enclosed by '<' and '>' charcters. /// Optional parameters are enclossed by '[' and ']' characters. pub fn get_usage_str(&self) -> String { let mut ret: String; if self.required { ret = '<'.to_string(); } else { ret = '['.to_string(); } if let Some(optstr) = self.get_loptarg_str() { ret.push_str(&optstr); } else if let Some(optstr) = self.get_soptarg_str() { ret.push_str(&optstr); } else if let Some(metastr) = self.get_joined_meta_str() { let s = match self.nargs { Nargs::Count(_) => metastr.clone(), Nargs::Remainder => format!("{0} [{0} ...]", metastr), _ => panic!( "Attempted to print positional argument spec with no arguments" ) }; ret.push_str(&s); } else { panic!("Unexpected state"); } if self.required { ret.push_str(">"); } else { ret.push_str("]"); } return ret; } pub fn get_help_title_str(&self) -> String { let mut args: Vec = Vec::new(); if let Some(lstr) = self.get_soptarg_str() { args.push(lstr); } if let Some(rstr) = self.get_loptarg_str() { args.push(rstr); } if args.len() == 0 { if let Some(posarg) = self.get_joined_meta_str() { let s = match self.nargs { Nargs::Count(_) => posarg.clone(), Nargs::Remainder => posarg.clone(), _ => panic!( "Attempted to print positional argument spec with no arguments" ) }; args.push(s); } } return args.join(", "); } pub fn get_help_text(&self) -> &Vec { &self.desc } } /* * Sort order * - Compare sopt to sopt * - Compare lopt to lopt * - Some(sopt) < lopt:None * - posarg > optarg * - posargs compare their index */ /* impl Default for Spec { fn default() -> Self { let metanames = Vec::new(); let desc = Vec::new(); Spec { sopt: None, lopt: None, name: None, nargs: Nargs::None, exit: false, required: false, metanames, desc, proc: Callback::None } } } */ /* impl PartialEq for Spec { fn eq(&self, other: &Spec) -> bool { true } } impl PartialOrd for Spec { fn partial_cmp(&self, other: &Spec) -> Option { if self.is_pos() && !other.is_pos() { return Ordering::Less; } else if !self.is_pos() && other.is_pos() { return Ordering::Greater; } Ordering::Equal } } */ #[cfg(test)] mod tests { use std::collections::HashMap; #[derive(Default)] pub(super) struct TestCtx { pub(super) do_help: bool, pub(super) verbosity: u8, pub(super) fname: String, pub(super) params: HashMap } pub(super) fn help_proc( _spec: &super::Spec, ctx: &mut TestCtx, _args: &Vec ) { ctx.do_help = true; } pub(super) fn verbose_proc( _spec: &super::Spec, ctx: &mut TestCtx, _args: &Vec ) { ctx.verbosity += 1; } pub(super) fn file_proc( _spec: &super::Spec, ctx: &mut TestCtx, args: &Vec ) { ctx.fname = args[0].clone(); } pub(super) fn param_proc( _spec: &super::Spec, ctx: &mut TestCtx, args: &Vec ) { ctx.params.insert(args[0].clone(), args[1].clone()); } pub(super) fn args_proc( _spec: &super::Spec, _ctx: &mut TestCtx, _args: &Vec ) { } pub(super) fn mkhelp(sopt: bool, lopt: bool) -> super::Spec { let mut bldr = super::Builder::new(); if sopt { bldr.sopt('h'); } if lopt { bldr.lopt("help"); } bldr.build(help_proc) } pub(super) fn mkfile( sopt: bool, lopt: bool, name: bool, argname: bool ) -> super::Spec { let mut bldr = super::Builder::new(); if sopt { bldr.sopt('f'); } if lopt { bldr.lopt("file"); } if name { bldr.name("file"); } if argname { bldr.nargs(super::Nargs::Count(1), &["FILE"]); } else { let nm: Vec = Vec::new(); bldr.nargs(super::Nargs::Count(1), &nm); } bldr.build(file_proc) } pub(super) fn mkparam( sopt: bool, lopt: bool, name: bool, defargs: usize ) -> super::Spec { let mut bldr = super::Builder::new(); if sopt { bldr.sopt('p'); } if lopt { bldr.lopt("param"); } if name { bldr.name("param"); } if defargs > 1 { bldr.nargs(super::Nargs::Count(2), &["KEY", "VALUE"]); } else if defargs == 1 { bldr.nargs(super::Nargs::Count(1), &["KEY"]); } else { let nm: Vec = Vec::new(); bldr.nargs(super::Nargs::Count(0), &nm); } bldr.build(param_proc) } } // Make this a macro? #[cfg(test)] fn expect_opt_str(optstr: &Option, expect: &str) { if let Some(optstr) = optstr { assert_eq!(optstr, expect); } else { panic!("This shouldn't be possbile."); } } #[test] fn test_metanames() { let spec = Builder::new() .sopt('h') .lopt("help") .exit(true) .build(tests::help_proc); // No meta names for plain switches assert_eq!(spec.get_joined_meta_str(), None); let spec = Builder::new() .sopt('f') .lopt("file") .name("file") .nargs(Nargs::Count(1), &["FILE"]) .build(tests::file_proc); expect_opt_str(&spec.get_joined_meta_str(), "FILE"); expect_opt_str(&spec.get_joined_meta_str(), "FILE"); expect_opt_str(&spec.get_soptarg_str(), "-f FILE"); expect_opt_str(&spec.get_loptarg_str(), "--file FILE"); let nm: Vec = Vec::new(); let spec = Builder::new() .sopt('f') .lopt("file") .name("file") .nargs(Nargs::Count(1), &nm) .build(tests::file_proc); expect_opt_str(&spec.get_joined_meta_str(), "ARG"); expect_opt_str(&spec.get_joined_meta_str(), "ARG"); expect_opt_str(&spec.get_soptarg_str(), "-f ARG"); expect_opt_str(&spec.get_loptarg_str(), "--file ARG"); let spec = super::Builder::new() .sopt('p') .lopt("param") .name("param") .nargs(super::Nargs::Count(2), &["KEY", "VALUE"]) .build(tests::param_proc); expect_opt_str(&spec.get_joined_meta_str(), "KEY VALUE"); let spec = super::Builder::new() .sopt('p') .lopt("param") .name("param") .nargs(super::Nargs::Count(2), &["KEY"]) .build(tests::param_proc); expect_opt_str(&spec.get_joined_meta_str(), "KEY ARG"); let nm: Vec = Vec::new(); let spec = super::Builder::new() .sopt('p') .lopt("param") .name("param") .nargs(super::Nargs::Count(2), &nm) .build(tests::param_proc); expect_opt_str(&spec.get_joined_meta_str(), "ARG ARG"); } #[test] fn test_help_switch() { let spec = Builder::new() .sopt('h') .lopt("help") .exit(true) .build(tests::help_proc); assert_eq!(spec.is_exit(), true); expect_opt_str(&spec.get_sopt_str(), "-h"); expect_opt_str(&spec.get_lopt_str(), "--help"); expect_opt_str(&spec.get_soptarg_str(), "-h"); expect_opt_str(&spec.get_loptarg_str(), "--help"); expect_opt_str(&spec.get_sopt_usage_str(), "[-h]"); expect_opt_str(&spec.get_lopt_usage_str(), "[--help]"); // No meta names for plain switches assert_eq!(spec.get_joined_meta_str(), None); } #[test] fn test_switch_noshort() { let spec = Builder::new() .lopt("help") .exit(true) .build(tests::help_proc); assert_eq!(spec.is_exit(), true); let soptstr = spec.get_sopt_str(); assert_eq!(soptstr.is_none(), true); } #[test] fn test_switch_nolong() { let spec = Builder::new().sopt('h').exit(true).build(tests::help_proc); assert_eq!(spec.is_exit(), true); let loptstr = spec.get_lopt_str(); assert_eq!(loptstr.is_none(), true); } #[test] fn test_file_optarg() { let spec = Builder::new() .sopt('f') .lopt("file") .name("file") .nargs(Nargs::Count(1), &["FILE"]) .build(tests::file_proc); expect_opt_str(&spec.get_joined_meta_str(), "FILE"); expect_opt_str(&spec.get_soptarg_str(), "-f FILE"); expect_opt_str(&spec.get_loptarg_str(), "--file FILE"); let nm: Vec = Vec::new(); let spec = Builder::new() .sopt('f') .lopt("file") .name("file") .nargs(Nargs::Count(1), &nm) .build(tests::file_proc); expect_opt_str(&spec.get_joined_meta_str(), "ARG"); expect_opt_str(&spec.get_soptarg_str(), "-f ARG"); expect_opt_str(&spec.get_loptarg_str(), "--file ARG"); } #[test] fn test_param_optarg() { let spec = super::Builder::new() .sopt('p') .lopt("param") .name("param") .nargs(super::Nargs::Count(2), &["KEY", "VALUE"]) .build(tests::param_proc); expect_opt_str(&spec.get_joined_meta_str(), "KEY VALUE"); let spec = super::Builder::new() .sopt('p') .lopt("param") .name("param") .nargs(super::Nargs::Count(2), &["KEY"]) .build(tests::param_proc); expect_opt_str(&spec.get_joined_meta_str(), "KEY ARG"); let nm: Vec = Vec::new(); let spec = super::Builder::new() .sopt('p') .lopt("param") .name("param") .nargs(super::Nargs::Count(2), &nm) .build(tests::param_proc); expect_opt_str(&spec.get_joined_meta_str(), "ARG ARG"); } /* #[test] fn test_optarg_usage_str() { let spec = tests::mkfile(true, true, false, true); expect_opt_str(&spec.get_sopt_usage_str(), "[-f FILE]"); expect_opt_str(&spec.get_lopt_usage_str(), "[--file FILE]"); } #[test] fn test_opts_usage_str() { let spec = tests::mkhelp(true, true); assert_eq!(spec.get_opts_usage_str(), "-h, --help"); let spec = tests::mkhelp(false, true); assert_eq!(spec.get_opts_usage_str(), "--help"); let spec = tests::mkhelp(true, false); assert_eq!(spec.get_opts_usage_str(), "-h"); let spec = tests::mkfile(true, true, false, true); assert_eq!(&spec.get_opts_usage_str(), "-f FILE, --file FILE"); let spec = tests::mkfile(false, true, false, true); assert_eq!(&spec.get_opts_usage_str(), "--file FILE"); let spec = tests::mkfile(true, false, false, true); assert_eq!(&spec.get_opts_usage_str(), "-f FILE"); } #[test] fn test_usage_str() { let spec = tests::mkhelp(true, true); assert_eq!(spec.get_usage_str(), "[--help]"); let spec = tests::mkhelp(false, true); assert_eq!(spec.get_usage_str(), "[--help]"); let spec = tests::mkhelp(true, false); assert_eq!(spec.get_usage_str(), "[-h]"); } #[test] fn test_usage_req_str() { let mut spec = Spec::new_opt(Some('h'), Some("help"), tests::help_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), "<--help>"); let mut spec = Spec::new_opt(None, Some("help"), tests::help_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), "<--help>"); let mut spec = Spec::new_opt(Some('h'), None::, tests::help_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), "<-h>"); } #[test] fn test_usage_arg_str() { let spec = Spec::new_argopt(Some('f'), Some("file"), Nargs::Count(1), &["FILE"], tests::file_proc); assert_eq!(spec.get_usage_str(), "[--file FILE]"); let spec = Spec::new_argopt(None, Some("file"), Nargs::Count(1), &["FILE"], tests::file_proc); assert_eq!(spec.get_usage_str(), "[--file FILE]"); let spec = Spec::new_argopt(Some('f'), None::, Nargs::Count(1), &["FILE"], tests::file_proc); assert_eq!(spec.get_usage_str(), "[-f FILE]"); } #[test] fn test_usage_arg_req_str() { let mut spec = Spec::new_argopt(Some('f'), Some("file"), Nargs::Count(1), &["FILE"], tests::file_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), "<--file FILE>"); let mut spec = Spec::new_argopt(None, Some("file"), Nargs::Count(1), &["FILE"], tests::file_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), "<--file FILE>"); let mut spec = Spec::new_argopt(Some('f'), None::, Nargs::Count(1), &["FILE"], tests::file_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), "<-f FILE>"); } #[test] fn test_usage_posarg_str() { let spec = Spec::new_posarg("cmd", Nargs::Count(1), &["COMMAND"], tests::args_proc); assert_eq!(spec.get_usage_str(), "[COMMAND]"); let spec = Spec::new_posarg("cmd", Nargs::Count(2), &["COMMAND"], tests::args_proc); assert_eq!(spec.get_usage_str(), "[COMMAND ARG]"); } #[test] fn test_usage_posarg_req_str() { let mut spec = Spec::new_posarg("cmd", Nargs::Count(1), &["COMMAND"], tests::args_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), ""); let mut spec = Spec::new_posarg("cmd", Nargs::Count(2), &["COMMAND"], tests::args_proc); spec.set_required(true); assert_eq!(spec.get_usage_str(), ""); } */ // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :