summary refs log tree commit diff
path: root/src/argument.rs
diff options
context:
space:
mode:
authorZach Reizner <zachr@google.com>2017-08-26 18:05:48 -0700
committerchrome-bot <chrome-bot@chromium.org>2017-09-02 00:18:25 -0700
commitefe957849b5d645b115f677701ad779d42ef7574 (patch)
tree4911944e10bcfb6581d5191b76ab4d051c8c6d53 /src/argument.rs
parente9321023861f8584df4237ecc4f3c8fbc01f90f3 (diff)
downloadcrosvm-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.rs410
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());
+    }
+}