diff options
author | Zach Reizner <zachr@google.com> | 2017-08-26 18:05:48 -0700 |
---|---|---|
committer | chrome-bot <chrome-bot@chromium.org> | 2017-09-02 00:18:25 -0700 |
commit | efe957849b5d645b115f677701ad779d42ef7574 (patch) | |
tree | 4911944e10bcfb6581d5191b76ab4d051c8c6d53 /src/argument.rs | |
parent | e9321023861f8584df4237ecc4f3c8fbc01f90f3 (diff) | |
download | crosvm-efe957849b5d645b115f677701ad779d42ef7574.tar crosvm-efe957849b5d645b115f677701ad779d42ef7574.tar.gz crosvm-efe957849b5d645b115f677701ad779d42ef7574.tar.bz2 crosvm-efe957849b5d645b115f677701ad779d42ef7574.tar.lz crosvm-efe957849b5d645b115f677701ad779d42ef7574.tar.xz crosvm-efe957849b5d645b115f677701ad779d42ef7574.tar.zst crosvm-efe957849b5d645b115f677701ad779d42ef7574.zip |
crosvm: argument parsing without clap
This removes the clap dependency by replacing that functionality with a custom written parser. Binary size is reduced by about 60% in optimized and stripped mode. TEST=cargo run -- run -h BUG=None Change-Id: I2eaf6fcff121ab16613c444693d95fdf3ad04da3 Reviewed-on: https://chromium-review.googlesource.com/636011 Commit-Ready: Zach Reizner <zachr@chromium.org> Tested-by: Zach Reizner <zachr@chromium.org> Reviewed-by: Dylan Reid <dgreid@chromium.org>
Diffstat (limited to 'src/argument.rs')
-rw-r--r-- | src/argument.rs | 410 |
1 files changed, 410 insertions, 0 deletions
diff --git a/src/argument.rs b/src/argument.rs new file mode 100644 index 0000000..03eda0e --- /dev/null +++ b/src/argument.rs @@ -0,0 +1,410 @@ +// Copyright 2017 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//! Handles argument parsing. +//! +//! # Example +//! +//! ``` +//! const ARGUMENTS: &'static [Argument] = &[ +//! Argument::positional("FILES", "files to operate on"), +//! Argument::short_value('p', "program", "PROGRAM", "Program to apply to each file"), +//! Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)"), +//! Argument::flag("unmount", "Unmount the root"), +//! Argument::short_flag('h', "help", "Print help message."), +//! ]; +//! +//! let match_res = set_arguments(args, ARGUMENTS, |name, value| { +//! match name { +//! "" => println!("positional arg! {}", value.unwrap()), +//! "program" => println!("gonna use program {}", value.unwrap()), +//! "cpus" => { +//! let v: u32 = value.unwrap().parse().map_err(|_| { +//! Error::InvalidValue { +//! value: value.unwrap().to_owned(), +//! expected: "this value for `cpus` needs to be integer", +//! } +//! })?; +//! } +//! "unmount" => println!("gonna unmount"), +//! "help" => return Err(Error::PrintHelp), +//! _ => unreachable!(), +//! } +//! } +//! +//! match match_res { +//! Ok(_) => println!("running with settings"), +//! Err(Error::PrintHelp) => print_help("best_program", "FILES", ARGUMENTS), +//! Err(e) => println!("{}", e), +//! } +//! ``` + +use std::fmt; +use std::result; + +/// An error with argument parsing. +pub enum Error { + /// There was a syntax error with the argument. + Syntax(String), + /// The argumen's name is unused. + UnknownArgument(String), + /// The argument was required. + ExpectedArgument(String), + /// The argument's given value is invalid. + InvalidValue { + value: String, + expected: &'static str, + }, + /// The argument was already given and none more are expected. + TooManyArguments(String), + /// The argument expects a value. + ExpectedValue(String), + /// The help information was requested + PrintHelp, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &Error::Syntax(ref s) => write!(f, "syntax error: {}", s), + &Error::UnknownArgument(ref s) => write!(f, "unknown argument: {}", s), + &Error::ExpectedArgument(ref s) => write!(f, "expected argument: {}", s), + &Error::InvalidValue { + ref value, + expected, + } => write!(f, "invalid value {:?}: {}", value, expected), + &Error::TooManyArguments(ref s) => write!(f, "too many arguments: {}", s), + &Error::ExpectedValue(ref s) => write!(f, "expected parameter value: {}", s), + &Error::PrintHelp => write!(f, "help was requested"), + } + } +} + +/// Result of a argument parsing. +pub type Result<T> = result::Result<T, Error>; + +/// Information about an argument expected from the command line. +/// +/// # Examples +/// +/// To indicate a flag style argument: +/// +/// ``` +/// Argument::short_flag('f', "flag", "enable awesome mode") +/// ``` +/// +/// To indicate a parameter style argument that expects a value: +/// +/// ``` +/// // "VALUE" and "NETMASK" are placeholder values displayed in the help message for these +/// // arguments. +/// Argument::short_value('v', "val", "VALUE", "how much do you value this usage information") +/// Argument::value("netmask", "NETMASK", "hides your netface") +/// ``` +/// +/// To indicate an argument with no short version: +/// +/// ``` +/// Argument::flag("verbose", "this option is hard to type quickly") +/// ``` +/// +/// To indicate a positional argument: +/// +/// ``` +/// Argument::positional("VALUES", "these are positional arguments") +/// ``` +#[derive(Default)] +pub struct Argument { + /// The name of the value to display in the usage information. Use None to indicate that there + /// is no value expected for this argument. + pub value: Option<&'static str>, + /// Optional single character shortened argument name. + pub short: Option<char>, + /// The long name of this argument. + pub long: &'static str, + /// Helpfuly usage information for this argument to display to the user. + pub help: &'static str, +} + +impl Argument { + pub fn positional(value: &'static str, help: &'static str) -> Argument { + Argument { + value: Some(value), + long: "", + help: help, + ..Default::default() + } + } + + pub fn value(long: &'static str, value: &'static str, help: &'static str) -> Argument { + Argument { + value: Some(value), + long: long, + help: help, + ..Default::default() + } + } + + pub fn short_value(short: char, + long: &'static str, + value: &'static str, + help: &'static str) + -> Argument { + Argument { + value: Some(value), + short: Some(short), + long: long, + help: help, + } + } + + pub fn flag(long: &'static str, help: &'static str) -> Argument { + Argument { + long: long, + help: help, + ..Default::default() + } + } + + pub fn short_flag(short: char, long: &'static str, help: &'static str) -> Argument { + Argument { + short: Some(short), + long: long, + help: help, + ..Default::default() + } + } +} + +fn parse_arguments<I, R, F>(args: I, mut f: F) -> Result<()> + where I: Iterator<Item = R>, + R: AsRef<str>, + F: FnMut(&str, Option<&str>) -> Result<()> +{ + enum State { + // Initial state at the start and after finishing a single argument/value. + Top, + // The remaining arguments are all positional. + Positional, + // The next string is the value for the argument `name`. + Value { name: String }, + } + let mut s = State::Top; + for arg in args { + let arg = arg.as_ref(); + s = match s { + State::Top => { + if arg == "--" { + State::Positional + } else if arg.starts_with("--") { + let param = arg.trim_left_matches('-'); + if param.contains('=') { + let mut iter = param.splitn(2, '='); + let name = iter.next().unwrap(); + let value = iter.next().unwrap(); + if name.is_empty() { + return Err(Error::Syntax("expected parameter name before `=`" + .to_owned())); + } + if value.is_empty() { + return Err(Error::Syntax("expected parameter value after `=`" + .to_owned())); + } + f(name, Some(value))?; + State::Top + } else { + if let Err(e) = f(param, None) { + if let Error::ExpectedValue(_) = e { + State::Value { name: param.to_owned() } + } else { + return Err(e); + } + } else { + State::Top + } + } + } else if arg.starts_with("-") { + if arg.len() == 1 { + return Err(Error::Syntax("expected argument short name after `-`" + .to_owned())); + } + let name = &arg[1..2]; + let value = if arg.len() > 2 { Some(&arg[2..]) } else { None }; + if let Err(e) = f(name, value) { + if let Error::ExpectedValue(_) = e { + State::Value { name: name.to_owned() } + } else { + return Err(e); + } + } else { + State::Top + } + } else { + f("", Some(&arg))?; + State::Positional + } + } + State::Positional => { + f("", Some(&arg))?; + State::Positional + } + State::Value { name } => { + f(&name, Some(&arg))?; + State::Top + } + }; + } + Ok(()) +} + +/// Parses the given `args` against the list of know arguments `arg_list` and calls `f` with each +/// present argument and value if required. +/// +/// This function guarantees that only valid long argument names from `arg_list` are sent to the +/// callback `f`. It is also guaranteed that if an arg requires a value (i.e. +/// `arg.value.is_some()`), the value will be `Some` in the callbacks arguments. If the callback +/// returns `Err`, this function will end parsing and return that `Err`. +/// +/// See the [module level](index.html) example for a usage example. +pub fn set_arguments<I, R, F>(args: I, arg_list: &[Argument], mut f: F) -> Result<()> + where I: Iterator<Item = R>, + R: AsRef<str>, + F: FnMut(&str, Option<&str>) -> Result<()> +{ + parse_arguments(args, |name, value| { + let mut matches = None; + for arg in arg_list { + if let Some(short) = arg.short { + if name.len() == 1 && name.starts_with(short) { + if value.is_some() != arg.value.is_some() { + return Err(Error::ExpectedValue(short.to_string())); + } + matches = Some(arg.long); + } + } + if matches.is_none() && arg.long == name { + if value.is_some() != arg.value.is_some() { + return Err(Error::ExpectedValue(arg.long.to_owned())); + } + matches = Some(arg.long); + } + } + match matches { + Some(long) => f(long, value), + None => Err(Error::UnknownArgument(name.to_owned())), + } + }) +} + +/// Prints command line usage information to stdout. +/// +/// Usage information is printed according to the help fields in `args` with a leading usage line. +/// The usage line is of the format "`program_name` [ARGUMENTS] `required_arg`". +pub fn print_help(program_name: &str, required_arg: &str, args: &[Argument]) { + println!("Usage: {} {}{}\n", + program_name, + if args.is_empty() { "" } else { "[ARGUMENTS] " }, + required_arg); + if args.is_empty() { + return; + } + println!("Argument{}:", if args.len() > 1 { "s" } else { "" }); + for arg in args { + match arg.short { + Some(ref s) => print!(" -{}, ", s), + None => print!(" "), + } + if arg.long.is_empty() { + print!(" "); + } else { + print!("--"); + } + print!("{:<12}", arg.long); + if let Some(v) = arg.value { + if arg.long.is_empty() { + print!(" "); + } else { + print!("="); + } + print!("{:<10}", v); + } else { + print!("{:<11}", ""); + } + println!("{}", arg.help); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_help() { + let arguments = [Argument::short_flag('h', "help", "Print help message.")]; + + let match_res = set_arguments(["-h"].iter(), &arguments[..], |name, _| { + match name { + "help" => return Err(Error::PrintHelp), + _ => unreachable!(), + }; + }); + match match_res { + Err(Error::PrintHelp) => {} + _ => unreachable!(), + } + } + + #[test] + fn mixed_args() { + let arguments = + [Argument::positional("FILES", "files to operate on"), + Argument::short_value('p', "program", "PROGRAM", "Program to apply to each file"), + Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)"), + Argument::flag("unmount", "Unmount the root"), + Argument::short_flag('h', "help", "Print help message.")]; + + let mut unmount = false; + let match_res = set_arguments(["--cpus", "3", "--program", "hello", "--unmount", "file"] + .iter(), + &arguments[..], + |name, value| { + match name { + "" => assert_eq!(value.unwrap(), "file"), + "program" => assert_eq!(value.unwrap(), "hello"), + "cpus" => { + let c: u32 = value + .unwrap() + .parse() + .map_err(|_| { + Error::InvalidValue { + value: value.unwrap().to_owned(), + expected: "this value for `cpus` needs to be integer", + } + })?; + assert_eq!(c, 3); + } + "unmount" => unmount = true, + "help" => return Err(Error::PrintHelp), + _ => unreachable!(), + }; + Ok(()) + }); + assert!(match_res.is_ok()); + assert!(unmount); + } + + #[test] + fn name_value_pair() { + let arguments = + [Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)")]; + let match_res = set_arguments(["-c", "5", "--cpus", "5", "-c5", "--cpus=5"].iter(), + &arguments[..], + |name, value| { + assert_eq!(name, "cpus"); + assert_eq!(value, Some("5")); + Ok(()) + }); + assert!(match_res.is_ok()); + } +} |