Merge #297

297: uses clap for the CLI r=kbknapp a=kbknapp



Co-authored-by: Kevin K <kbknapp@gmail.com>
diff --git a/Cargo.lock b/Cargo.lock
index b937461..886fb82 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -152,7 +152,7 @@
 dependencies = [
  "anyhow",
  "cargo",
- "docopt",
+ "clap",
  "env_logger",
  "git2-curl",
  "semver",
@@ -220,7 +220,7 @@
  "ansi_term",
  "atty",
  "bitflags",
- "strsim 0.8.0",
+ "strsim",
  "textwrap",
  "unicode-width",
  "vec_map",
@@ -337,18 +337,6 @@
 ]
 
 [[package]]
-name = "docopt"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f"
-dependencies = [
- "lazy_static",
- "regex",
- "serde",
- "strsim 0.10.0",
-]
-
-[[package]]
 name = "either"
 version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1036,12 +1024,6 @@
 checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 
 [[package]]
-name = "strsim"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
-
-[[package]]
 name = "syn"
 version = "1.0.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 1ac8fde..305ff4e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@
     "Ricky H. <ricky.hosfelt@gmail.com>",
 ]
 categories = ["development-tools", "development-tools::cargo-plugins"]
-edition = "2018"
+edition = "2021"
 exclude = ["*.png"]
 keywords = [
     "cargo",
@@ -30,7 +30,6 @@
 [dependencies]
 anyhow = "1.0"
 cargo = "0.57.0"
-docopt = "1.1.0"
 env_logger = "0.9.0"
 git2-curl = "0.14.0"
 semver = "1.0.0"
@@ -40,6 +39,7 @@
 tabwriter = "1.2.1"
 tempfile = "3"
 toml = "~0.5.0"
+clap = "2.33.3"
 
 [dependencies.termcolor]
 optional = true
diff --git a/src/cargo_ops/elaborate_workspace.rs b/src/cargo_ops/elaborate_workspace.rs
index e6b37e4..2f1f016 100644
--- a/src/cargo_ops/elaborate_workspace.rs
+++ b/src/cargo_ops/elaborate_workspace.rs
@@ -66,7 +66,7 @@
     ) -> CargoResult<ElaborateWorkspace<'ela>> {
         // new in cargo 0.54.0
         let flag_features: BTreeSet<FeatureValue> = options
-            .flag_features
+            .features
             .iter()
             .map(|feature| FeatureValue::new(InternedString::from(feature)))
             .collect();
@@ -118,13 +118,13 @@
             pkgs,
             pkg_deps,
             pkg_status: RefCell::new(HashMap::new()),
-            workspace_mode: options.flag_workspace || workspace.current().is_err(),
+            workspace_mode: options.workspace || workspace.current().is_err(),
         })
     }
 
     /// Determine root package based on current workspace and CLI options
     pub fn determine_root(&self, options: &Options) -> CargoResult<PackageId> {
-        if let Some(ref root_name) = options.flag_root {
+        if let Some(ref root_name) = options.root {
             if let Ok(workspace_root) = self.workspace.current() {
                 if root_name == workspace_root.name().as_str() {
                     Ok(workspace_root.package_id())
@@ -239,7 +239,7 @@
             self.pkg_status.borrow_mut().insert(path.clone(), status);
             // next layer
             // this unwrap is safe since we first check if it is None :)
-            if options.flag_depth.is_none() || depth < options.flag_depth.unwrap() {
+            if options.depth.is_none() || depth < options.depth.unwrap() {
                 self.pkg_deps[pkg]
                     .keys()
                     .filter(|dep| !path.contains(dep))
@@ -279,7 +279,7 @@
             let pkg = path.last().ok_or(OutdatedError::EmptyPath)?;
             let name = pkg.name().to_string();
 
-            if options.flag_ignore.contains(&name) {
+            if options.ignore.contains(&name) {
                 continue;
             }
 
@@ -287,7 +287,7 @@
             // generate lines
             let status = &self.pkg_status.borrow_mut()[&path];
             if (status.compat.is_changed() || status.latest.is_changed())
-                && (options.flag_packages.is_empty() || options.flag_packages.contains(&name))
+                && (options.packages.is_empty() || options.packages.contains(&name))
             {
                 // name version compatible latest kind platform
                 let parent = path.get(path.len() - 2);
@@ -326,7 +326,7 @@
             }
             // next layer
             // this unwrap is safe since we first check if it is None :)
-            if options.flag_depth.is_none() || depth < options.flag_depth.unwrap() {
+            if options.depth.is_none() || depth < options.depth.unwrap() {
                 self.pkg_deps[pkg]
                     .keys()
                     .filter(|dep| !path.contains(dep))
@@ -379,7 +379,7 @@
             let pkg = path.last().ok_or(OutdatedError::EmptyPath)?;
             let name = pkg.name().to_string();
 
-            if options.flag_ignore.contains(&name) {
+            if options.ignore.contains(&name) {
                 continue;
             }
 
@@ -387,7 +387,7 @@
             // generate lines
             let status = &self.pkg_status.borrow_mut()[&path];
             if (status.compat.is_changed() || status.latest.is_changed())
-                && (options.flag_packages.is_empty() || options.flag_packages.contains(&name))
+                && (options.packages.is_empty() || options.packages.contains(&name))
             {
                 // name version compatible latest kind platform
                 // safely get the parent index
@@ -438,7 +438,7 @@
             }
             // next layer
             // this unwrap is safe since we first check if it is None :)
-            if options.flag_depth.is_none() || depth < options.flag_depth.unwrap() {
+            if options.depth.is_none() || depth < options.depth.unwrap() {
                 self.pkg_deps[pkg]
                     .keys()
                     .filter(|dep| !path.contains(dep))
diff --git a/src/cargo_ops/temp_project.rs b/src/cargo_ops/temp_project.rs
index 38acad2..826e89e 100644
--- a/src/cargo_ops/temp_project.rs
+++ b/src/cargo_ops/temp_project.rs
@@ -189,11 +189,11 @@
         let mut config = Config::new(shell, cwd, homedir);
         config.configure(
             0,
-            options.flag_verbose == 0,
-            options.flag_color.as_deref(),
+            options.verbose == 0,
+            Some(&options.color.to_string().to_ascii_lowercase()),
             options.frozen(),
             options.locked(),
-            options.flag_offline,
+            options.offline,
             &cargo_home_path,
             &[],
             &[],
@@ -396,7 +396,7 @@
             } else if find_latest {
                 // this unwrap is safe since we check if `version_req` is `None` before this
                 // (which is only `None` if `requirement` is `None`)
-                self.options.flag_aggressive
+                self.options.aggressive
                     || valid_latest_version(requirement.unwrap(), summary.version())
             } else {
                 // this unwrap is safe since we check if `version_req` is `None` before this
@@ -436,12 +436,7 @@
         if self.options.all_features() {
             return true;
         }
-        if !optional
-            && self
-                .options
-                .flag_features
-                .contains(&String::from("default"))
-        {
+        if !optional && self.options.features.contains(&String::from("default")) {
             return true;
         }
         let features_table = match *features_table {
@@ -450,7 +445,7 @@
         };
         let mut to_resolve: Vec<&str> = self
             .options
-            .flag_features
+            .features
             .iter()
             .filter(|f| !f.is_empty())
             .map(String::as_str)
@@ -495,7 +490,7 @@
             // In short this allows cargo to build the package with semver minor compatibilities issues
             // https://github.com/rust-lang/cargo/issues/6584
             // https://github.com/kbknapp/cargo-outdated/issues/230
-            if self.options.flag_exclude.contains(&dep_key) {
+            if self.options.exclude.contains(&dep_key) {
                 continue;
             }
 
@@ -682,13 +677,11 @@
 
     fn warn<T: ::std::fmt::Display>(&self, message: T) -> CargoResult<()> {
         let original_verbosity = self.config.shell().verbosity();
-        self.config
-            .shell()
-            .set_verbosity(if self.options.flag_quiet {
-                Verbosity::Quiet
-            } else {
-                Verbosity::Normal
-            });
+        self.config.shell().set_verbosity(if self.options.quiet {
+            Verbosity::Quiet
+        } else {
+            Verbosity::Normal
+        });
         self.config.shell().warn(message)?;
         self.config.shell().set_verbosity(original_verbosity);
         Ok(())
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..65df241
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,476 @@
+use clap::{
+    arg_enum, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches,
+    SubCommand,
+};
+
+arg_enum! {
+    #[derive(Copy, Clone, Debug, PartialEq)]
+    pub enum Format {
+        List,
+        Json,
+    }
+}
+
+impl Default for Format {
+    fn default() -> Self { Format::List }
+}
+
+arg_enum! {
+    #[derive(Copy, Clone, Debug, PartialEq)]
+    pub enum Color {
+        Auto,
+        Never,
+        Always
+    }
+}
+
+impl Default for Color {
+    fn default() -> Self { Color::Auto }
+}
+
+/// Options from CLI arguments
+#[derive(Debug, PartialEq, Default)]
+pub struct Options {
+    pub format: Format,
+    pub color: Color,
+    pub features: Vec<String>,
+    pub ignore: Vec<String>,
+    pub exclude: Vec<String>,
+    pub manifest_path: Option<String>,
+    pub quiet: bool,
+    pub verbose: u64,
+    pub exit_code: i32,
+    pub packages: Vec<String>,
+    pub root: Option<String>,
+    pub depth: Option<i32>,
+    pub root_deps_only: bool,
+    pub workspace: bool,
+    pub aggressive: bool,
+    pub offline: bool,
+}
+
+impl Options {
+    pub fn all_features(&self) -> bool { self.features.is_empty() }
+
+    pub fn no_default_features(&self) -> bool {
+        !(self.features.is_empty() || self.features.contains(&"default".to_owned()))
+    }
+
+    pub fn locked(&self) -> bool { false }
+
+    pub fn frozen(&self) -> bool { false }
+}
+
+impl<'a> From<&ArgMatches<'a>> for Options {
+    fn from(m: &ArgMatches<'a>) -> Self {
+        let mut opts = Options {
+            format: value_t_or_exit!(m.value_of("format"), Format),
+            color: value_t_or_exit!(m.value_of("color"), Color),
+            features: m
+                .values_of("features")
+                .map(|vals| {
+                    vals.flat_map(|x| x.split_ascii_whitespace().collect::<Vec<_>>())
+                        .map(ToOwned::to_owned)
+                        .collect()
+                })
+                .unwrap_or_else(Vec::new),
+            ignore: m
+                .values_of("ignore")
+                .map(|vals| {
+                    vals.flat_map(|x| x.split_ascii_whitespace().collect::<Vec<_>>())
+                        .map(ToOwned::to_owned)
+                        .collect()
+                })
+                .unwrap_or_else(Vec::new),
+            exclude: m
+                .values_of("exclude")
+                .map(|vals| {
+                    vals.flat_map(|x| x.split_ascii_whitespace().collect::<Vec<_>>())
+                        .map(ToOwned::to_owned)
+                        .collect()
+                })
+                .unwrap_or_else(Vec::new),
+            manifest_path: m.value_of("manifest-path").map(ToOwned::to_owned),
+            quiet: m.is_present("quiet"),
+            verbose: m.occurrences_of("verbose"),
+            exit_code: value_t!(m, "exit-code", i32).ok().unwrap_or(0),
+            packages: m
+                .values_of("packages")
+                .map(|vals| {
+                    vals.flat_map(|x| x.split_ascii_whitespace().collect::<Vec<_>>())
+                        .map(ToOwned::to_owned)
+                        .collect::<Vec<_>>()
+                })
+                .unwrap_or_else(Vec::new),
+            root: m.value_of("root").map(ToOwned::to_owned),
+            depth: value_t!(m, "depth", i32).ok(),
+            root_deps_only: m.is_present("root-deps-only"),
+            workspace: m.is_present("workspace"),
+            aggressive: m.is_present("aggressive"),
+            offline: m.is_present("offline"),
+        };
+
+        if m.is_present("root-deps-only") {
+            opts.depth = Some(1);
+        }
+
+        opts
+    }
+}
+
+fn build() -> App<'static, 'static> {
+    App::new("cargo-outdated")
+        .bin_name("cargo")
+        .setting(AppSettings::SubcommandRequired)
+        .subcommand(
+            SubCommand::with_name("outdated")
+                .setting(AppSettings::UnifiedHelpMessage)
+                .about("Displays information about project dependency versions")
+                .version(crate_version!())
+                .arg(
+                    Arg::with_name("aggressive")
+                        .short("a")
+                        .long("aggresssive")
+                        .help("Ignores channels for latest updates"),
+                )
+                .arg(
+                    Arg::with_name("quiet")
+                        .short("q")
+                        .long("quiet")
+                        .help("Suppresses warnings"),
+                )
+                .arg(
+                    Arg::with_name("root-deps-only")
+                        .short("R")
+                        .long("root-deps-only")
+                        .help("Only check root dependencies (Equivalent to --depth=1)"),
+                )
+                .arg(
+                    Arg::with_name("workspace")
+                        .short("w")
+                        .long("workspace")
+                        .help("Checks updates for all workspace members rather than only the root package"),
+                )
+                .arg(
+                    Arg::with_name("offline")
+                        .short("o")
+                        .long("offline")
+                        .help("Run without accessing the network (useful for testing w/ local registries)"),
+                )
+                .arg(
+                    Arg::with_name("format")
+                        .long("format")
+                        .default_value("list")
+                        .case_insensitive(true)
+                        .possible_values(&Format::variants())
+                        .value_name("FORMAT")
+                        .help("Output formatting"),
+                )
+                .arg(
+                    Arg::with_name("ignore")
+                        .short("i")
+                        .long("ignore")
+                        .help("Dependencies to not print in the output (comma separated or one per '--ignore' argument)")
+                        .value_delimiter(",")
+                        .number_of_values(1)
+                        .multiple(true)
+                        .value_name("DEPENDENCIES"),
+                )
+                .arg(
+                    Arg::with_name("exclude")
+                        .short("x")
+                        .long("exclude")
+                        .help("Dependencies to exclude from building (comma separated or one per '--exclude' argument)")
+                        .value_delimiter(",")
+                        .multiple(true)
+                        .number_of_values(1)
+                        .value_name("DEPENDENCIES"),
+                )
+                .arg(
+                    Arg::with_name("verbose")
+                        .short("v")
+                        .long("verbose")
+                        .multiple(true)
+                        .help("Use verbose output")
+                )
+                .arg(
+                    Arg::with_name("color")
+                        .long("color")
+                        .possible_values(&Color::variants())
+                        .default_value("auto")
+                        .value_name("COLOR")
+                        .case_insensitive(true)
+                        .help("Output coloring")
+                )
+                .arg(
+                    Arg::with_name("depth")
+                        .short("d")
+                        .long("depth")
+                        .value_name("NUM")
+                        .help("How deep in the dependency chain to search (Defaults to all dependencies when omitted)")
+                )
+                .arg(
+                    Arg::with_name("exit-code")
+                        .long("exit-code")
+                        .help("The exit code to return on new versions found")
+                        .default_value("0")
+                        .value_name("NUM"))
+                .arg(
+                    Arg::with_name("manifest-path")
+                        .short("m")
+                        .long("manifest-path")
+                        .help("Path to the Cargo.toml file to use (Defaults to Cargo.toml in project root)")
+                        .value_name("PATH"))
+                .arg(
+                    Arg::with_name("root")
+                        .short("r")
+                        .long("root")
+                        .help("Package to treat as the root package")
+                        .value_name("ROOT"))
+                .arg(
+                    Arg::with_name("packages")
+                        .short("p")
+                        .long("packages")
+                        .help("Packages to inspect for updates (comma separated or one per '--packages' argument)")
+                        .value_delimiter(",")
+                        .number_of_values(1)
+                        .multiple(true)
+                        .value_name("PKGS"))
+                .arg(
+                    Arg::with_name("features")
+                        .long("features")
+                        .value_delimiter(",")
+                        .help("Space-separated list of features")
+                        .multiple(true)
+                        .number_of_values(1)
+                        .value_name("FEATURES"))
+            )
+}
+
+pub fn parse() -> Options {
+    let matches = build().get_matches();
+
+    Options::from(matches.subcommand_matches("outdated").unwrap())
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    fn options(args: &[&str]) -> Options {
+        let mut argv = vec!["cargo", "outdated"];
+        argv.extend(args);
+        let m = build().get_matches_from(argv);
+        Options::from(m.subcommand_matches("outdated").unwrap())
+    }
+
+    fn options_fail(args: &[&str]) -> clap::Result<ArgMatches<'static>> {
+        let mut argv = vec!["cargo", "outdated"];
+        argv.extend(args);
+        build().get_matches_from_safe(argv)
+    }
+
+    #[test]
+    fn default() {
+        let opts = options(&[]);
+        assert_eq!(Options::default(), opts)
+    }
+
+    #[test]
+    fn root_only() {
+        let opts = options(&["--root-deps-only"]);
+        assert_eq!(
+            Options {
+                depth: Some(1),
+                root_deps_only: true,
+                ..Options::default()
+            },
+            opts
+        )
+    }
+
+    #[test]
+    fn features() {
+        let opts1 = options(&["--features=one,two,three"]);
+        let opts2 = options(&["--features", "one,two,three"]);
+        let opts3 = options(&["--features", "one two three"]);
+        let opts4 = options(&[
+            "--features",
+            "one",
+            "--features",
+            "two",
+            "--features",
+            "three",
+        ]);
+        let opts5 = options(&["--features", "one", "--features", "two,three"]);
+
+        let correct = Options {
+            features: vec!["one".into(), "two".into(), "three".into()],
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+        assert_eq!(correct, opts2);
+        assert_eq!(correct, opts3);
+        assert_eq!(correct, opts4);
+        assert_eq!(correct, opts5);
+    }
+
+    #[test]
+    fn features_fail() {
+        let res = options_fail(&["--features", "one", "two"]);
+        assert!(res.is_err());
+        assert_eq!(
+            res.as_ref().unwrap_err().kind,
+            clap::ErrorKind::UnknownArgument,
+            "{:?}",
+            res.as_ref().unwrap_err().kind
+        );
+    }
+
+    #[test]
+    fn exclude() {
+        let opts1 = options(&["--exclude=one,two,three"]);
+        let opts2 = options(&["--exclude", "one,two,three"]);
+        let opts3 = options(&["--exclude", "one two three"]);
+        let opts4 = options(&["--exclude", "one", "--exclude", "two", "--exclude", "three"]);
+        let opts5 = options(&["--exclude", "one", "--exclude", "two,three"]);
+        let correct = Options {
+            exclude: vec!["one".into(), "two".into(), "three".into()],
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+        assert_eq!(correct, opts2);
+        assert_eq!(correct, opts3);
+        assert_eq!(correct, opts4);
+        assert_eq!(correct, opts5);
+    }
+
+    #[test]
+    fn exclude_fail() {
+        let res = options_fail(&["--exclude", "one", "two"]);
+        assert!(res.is_err());
+        assert_eq!(
+            res.as_ref().unwrap_err().kind,
+            clap::ErrorKind::UnknownArgument,
+            "{:?}",
+            res.as_ref().unwrap_err().kind
+        );
+    }
+
+    #[test]
+    fn ignore() {
+        let opts1 = options(&["--ignore=one,two,three"]);
+        let opts2 = options(&["--ignore", "one,two,three"]);
+        let opts3 = options(&["--ignore", "one two three"]);
+        let opts4 = options(&["--ignore", "one", "--ignore", "two", "--ignore", "three"]);
+        let opts5 = options(&["--ignore", "one", "--ignore", "two,three"]);
+        let correct = Options {
+            ignore: vec!["one".into(), "two".into(), "three".into()],
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+        assert_eq!(correct, opts2);
+        assert_eq!(correct, opts3);
+        assert_eq!(correct, opts4);
+        assert_eq!(correct, opts5);
+    }
+
+    #[test]
+    fn ignore_fail() {
+        let res = options_fail(&["--ignore", "one", "two"]);
+        assert!(res.is_err());
+        assert_eq!(
+            res.as_ref().unwrap_err().kind,
+            clap::ErrorKind::UnknownArgument,
+            "{:?}",
+            res.as_ref().unwrap_err().kind
+        );
+    }
+
+    #[test]
+    fn verbose() {
+        let opts1 = options(&["--verbose", "--verbose", "--verbose"]);
+        let correct = Options {
+            verbose: 3,
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+    }
+
+    #[test]
+    fn packages() {
+        let opts1 = options(&["--packages", "one,two"]);
+        let opts2 = options(&["--packages", "one two"]);
+        let opts3 = options(&["--packages", "one", "--packages", "two"]);
+        let correct = Options {
+            packages: vec!["one".into(), "two".into()],
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+        assert_eq!(correct, opts2);
+        assert_eq!(correct, opts3);
+    }
+
+    #[test]
+    fn packages_fail() {
+        let res = options_fail(&["--packages", "one", "two"]);
+        assert!(res.is_err());
+        assert_eq!(
+            res.as_ref().unwrap_err().kind,
+            clap::ErrorKind::UnknownArgument,
+            "{:?}",
+            res.as_ref().unwrap_err().kind
+        );
+    }
+
+    #[test]
+    fn format_case() {
+        let opts1 = options(&["--format", "JsOn"]);
+        let correct = Options {
+            format: Format::Json,
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+    }
+
+    #[test]
+    fn format_unknown() {
+        let res = options_fail(&["--format", "foobar"]);
+        assert!(res.is_err());
+        assert_eq!(
+            res.as_ref().unwrap_err().kind,
+            clap::ErrorKind::InvalidValue,
+            "{:?}",
+            res.as_ref().unwrap_err().kind
+        );
+    }
+
+    #[test]
+    fn color_case() {
+        let opts1 = options(&["--color", "NeVeR"]);
+        let correct = Options {
+            color: Color::Never,
+            ..Options::default()
+        };
+
+        assert_eq!(correct, opts1);
+    }
+
+    #[test]
+    fn color_unknown() {
+        let res = options_fail(&["--color", "foobar"]);
+        assert!(res.is_err());
+        assert_eq!(
+            res.as_ref().unwrap_err().kind,
+            clap::ErrorKind::InvalidValue,
+            "{:?}",
+            res.as_ref().unwrap_err().kind
+        );
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 92e39ce..050c9f4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,114 +6,24 @@
 #[macro_use]
 mod macros;
 mod cargo_ops;
+mod cli;
 mod error;
 
-use crate::{
-    cargo_ops::{ElaborateWorkspace, TempProject},
-    error::OutdatedError,
-};
-
 use cargo::core::shell::Verbosity;
 use cargo::core::Workspace;
 use cargo::ops::needs_custom_http_transport;
 use cargo::util::important_paths::find_root_manifest_for_wd;
 use cargo::util::{CargoResult, CliError, Config};
-use docopt::Docopt;
 
-/// usage message for --help
-pub const USAGE: &str = "
-Displays information about project dependency versions
-
-USAGE:
-    cargo outdated [options]
-
-Options:
-    -a, --aggressive            Ignores channels for latest updates
-    -h, --help                  Prints help information
-        --format FORMAT         Output formatting [default: list]
-                                [values: list, json]
-    -i, --ignore DEPENDENCIES   Comma separated list of dependencies to not print in the output
-    -x, --exclude DEPENDENCIES  Comma separated list of dependencies to exclude from building
-    -q, --quiet                 Suppresses warnings
-    -R, --root-deps-only        Only check root dependencies (Equivalent to --depth=1)
-    -V, --version               Prints version information
-    -v, --verbose ...           Use verbose output
-    -w, --workspace             Checks updates for all workspace members rather than
-                                only the root package
-        --color COLOR           Coloring: auto, always, never [default: auto]
-                                [values: auto, always, never]
-    -d, --depth NUM             How deep in the dependency chain to search
-                                (Defaults to all dependencies when omitted)
-        --exit-code NUM         The exit code to return on new versions found [default: 0]
-        --features FEATURES     Space-separated list of features
-    -m, --manifest-path FILE    Path to the Cargo.toml file to use
-                                (Defaults to Cargo.toml in project root)
-    -p, --packages PKGS         Packages to inspect for updates
-    -r, --root ROOT             Package to treat as the root package
-    -o, --offline               Run without accessing the network (useful for testing w/ local registries)
-";
-
-/// Options from CLI arguments
-#[derive(serde_derive::Deserialize, Debug, PartialEq, Default)]
-pub struct Options {
-    flag_format: Option<String>,
-    flag_color: Option<String>,
-    flag_features: Vec<String>,
-    flag_ignore: Vec<String>,
-    flag_exclude: Vec<String>,
-    flag_manifest_path: Option<String>,
-    flag_quiet: bool,
-    flag_verbose: u32,
-    flag_exit_code: i32,
-    flag_packages: Vec<String>,
-    flag_root: Option<String>,
-    flag_depth: Option<i32>,
-    flag_root_deps_only: bool,
-    flag_workspace: bool,
-    flag_aggressive: bool,
-    flag_offline: bool,
-}
-
-impl Options {
-    fn all_features(&self) -> bool { self.flag_features.is_empty() }
-
-    fn no_default_features(&self) -> bool {
-        !(self.flag_features.is_empty() || self.flag_features.contains(&"default".to_owned()))
-    }
-
-    fn locked(&self) -> bool { false }
-
-    fn frozen(&self) -> bool { false }
-}
+use crate::{
+    cargo_ops::{ElaborateWorkspace, TempProject},
+    cli::{Format, Options},
+    error::OutdatedError,
+};
 
 fn main() {
     env_logger::init();
-    let options = {
-        let mut options: Options = Docopt::new(USAGE)
-            .and_then(|d| {
-                d.version(Some(
-                    concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION")).to_owned(),
-                ))
-                .deserialize()
-            })
-            .unwrap_or_else(|e| e.exit());
-        fn flat_split(arg: &[String]) -> Vec<String> {
-            arg.iter()
-                .flat_map(|s| s.split_whitespace())
-                .flat_map(|s| s.split(','))
-                .filter(|s| !s.is_empty())
-                .map(ToString::to_string)
-                .collect()
-        }
-        options.flag_features = flat_split(&options.flag_features);
-        options.flag_ignore = flat_split(&options.flag_ignore);
-        options.flag_exclude = flat_split(&options.flag_exclude);
-        options.flag_packages = flat_split(&options.flag_packages);
-        if options.flag_root_deps_only {
-            options.flag_depth = Some(1);
-        }
-        options
-    };
+    let options = cli::parse();
 
     let mut config = match Config::default() {
         Ok(cfg) => cfg,
@@ -137,7 +47,7 @@
         }
     }
 
-    let exit_code = options.flag_exit_code;
+    let exit_code = options.exit_code;
     let result = execute(options, &mut config);
     match result {
         Err(e) => {
@@ -165,12 +75,15 @@
     config.nightly_features_allowed = true;
 
     config.configure(
-        options.flag_verbose,
-        options.flag_quiet,
-        options.flag_color.as_deref(),
+        options
+            .verbose
+            .try_into()
+            .expect("--verbose used too many times"),
+        options.quiet,
+        Some(&options.color.to_string().to_ascii_lowercase()),
         options.frozen(),
         options.locked(),
-        options.flag_offline,
+        options.offline,
         &cargo_home_path,
         &[],
         &[],
@@ -180,7 +93,7 @@
     verbose!(config, "Parsing...", "current workspace");
     // the Cargo.toml that we are actually working on
     let mut manifest_abspath: std::path::PathBuf;
-    let curr_manifest = if let Some(ref manifest_path) = options.flag_manifest_path {
+    let curr_manifest = if let Some(ref manifest_path) = options.manifest_path {
         manifest_abspath = manifest_path.into();
         if manifest_abspath.is_relative() {
             verbose!(config, "Resolving...", "absolute path of manifest");
@@ -192,11 +105,11 @@
     };
     let curr_workspace = Workspace::new(&curr_manifest, config)?;
     verbose!(config, "Resolving...", "current workspace");
-    if options.flag_verbose == 0 {
+    if options.verbose == 0 {
         config.shell().set_verbosity(Verbosity::Quiet);
     }
     let ela_curr = ElaborateWorkspace::from_workspace(&curr_workspace, &options)?;
-    if options.flag_verbose > 0 {
+    if options.verbose > 0 {
         config.shell().set_verbosity(Verbosity::Verbose);
     } else {
         config.shell().set_verbosity(Verbosity::Normal);
@@ -242,10 +155,9 @@
 
     if ela_curr.workspace_mode {
         let mut sum = 0;
-        if options.flag_format == Some("list".to_string()) {
-            verbose!(config, "Printing...", "Package status in list format");
-        } else if options.flag_format == Some("json".to_string()) {
-            verbose!(config, "Printing...", "Package status in json format");
+        match options.format {
+            Format::List => verbose!(config, "Printing...", "Package status in list format"),
+            Format::Json => verbose!(config, "Printing...", "Package status in json format"),
         }
 
         for member in ela_curr.workspace.members() {
@@ -256,10 +168,13 @@
                 config,
                 member.package_id(),
             )?;
-            if options.flag_format == Some("list".to_string()) {
-                sum += ela_curr.print_list(&options, member.package_id(), sum > 0)?;
-            } else if options.flag_format == Some("json".to_string()) {
-                sum += ela_curr.print_json(&options, member.package_id())?;
+            match options.format {
+                Format::List => {
+                    sum += ela_curr.print_list(&options, member.package_id(), sum > 0)?;
+                }
+                Format::Json => {
+                    sum += ela_curr.print_json(&options, member.package_id())?;
+                }
             }
         }
         if sum == 0 {
@@ -273,173 +188,15 @@
         verbose!(config, "Printing...", "list format");
         let mut count = 0;
 
-        if options.flag_format == Some("list".to_string()) {
-            count = ela_curr.print_list(&options, root, false)?;
-        } else if options.flag_format == Some("json".to_string()) {
-            ela_curr.print_json(&options, root)?;
-        } else {
-            println!("Error, did not specify list or json output formatting");
-            std::process::exit(2);
+        match options.format {
+            Format::List => {
+                count = ela_curr.print_list(&options, root, false)?;
+            }
+            Format::Json => {
+                ela_curr.print_json(&options, root)?;
+            }
         }
 
         Ok(count)
     }
 }
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    fn options(args: &[&str]) -> Options {
-        let mut argv = vec!["cargo", "outdated"];
-        if !args.is_empty() {
-            argv.extend(args);
-        }
-        let mut options: Options = Docopt::new(USAGE)
-            .and_then(|d| {
-                d.version(Some(
-                    concat!(env!("CARGO_PKG_NAME"), " v", env!("CARGO_PKG_VERSION")).to_owned(),
-                ))
-                .argv(argv)
-                .deserialize()
-            })
-            .unwrap_or_else(|e| e.exit());
-        fn flat_split(arg: &[String]) -> Vec<String> {
-            arg.iter()
-                .flat_map(|s| s.split_whitespace())
-                .flat_map(|s| s.split(','))
-                .filter(|s| !s.is_empty())
-                .map(ToString::to_string)
-                .collect()
-        }
-        options.flag_features = flat_split(&options.flag_features);
-        options.flag_ignore = flat_split(&options.flag_ignore);
-        options.flag_exclude = flat_split(&options.flag_exclude);
-        options.flag_packages = flat_split(&options.flag_packages);
-        if options.flag_root_deps_only {
-            options.flag_depth = Some(1);
-        }
-        options
-    }
-
-    #[test]
-    fn default() {
-        let opts = options(&[]);
-        assert_eq!(
-            Options {
-                flag_format: Some("list".into()),
-                flag_color: Some("auto".into()),
-                ..Options::default()
-            },
-            opts
-        )
-    }
-
-    #[test]
-    fn root_only() {
-        let opts = options(&["--root-deps-only"]);
-        assert_eq!(
-            Options {
-                flag_format: Some("list".into()),
-                flag_color: Some("auto".into()),
-                flag_depth: Some(1),
-                flag_root_deps_only: true,
-                ..Options::default()
-            },
-            opts
-        )
-    }
-
-    #[test]
-    fn features() {
-        let opts1 = options(&["--features=one,two,three"]);
-        let opts2 = options(&["--features", "one,two,three"]);
-        let opts3 = options(&["--features", "one two three"]);
-        // Not supported
-        //let opts4 = options("--features one --features two --features three");
-        //let opts5 = options("--features one --features two,three");
-        let correct = Options {
-            flag_format: Some("list".into()),
-            flag_color: Some("auto".into()),
-            flag_features: vec!["one".into(), "two".into(), "three".into()],
-            ..Options::default()
-        };
-
-        assert_eq!(correct, opts1);
-        assert_eq!(correct, opts2);
-        assert_eq!(correct, opts3);
-    }
-
-    #[test]
-    fn exclude() {
-        let opts1 = options(&["--exclude=one,two,three"]);
-        let opts2 = options(&["--exclude", "one,two,three"]);
-        let opts3 = options(&["--exclude", "one two three"]);
-        // Not supported
-        //let opts4 = options("--exclude one two three");
-        //let opts5 = options("--exclude one --exclude two --exclude three");
-        //let opts6 = options("--exclude one --exclude two,three");
-        let correct = Options {
-            flag_format: Some("list".into()),
-            flag_color: Some("auto".into()),
-            flag_exclude: vec!["one".into(), "two".into(), "three".into()],
-            ..Options::default()
-        };
-
-        assert_eq!(correct, opts1);
-        assert_eq!(correct, opts2);
-        assert_eq!(correct, opts3);
-    }
-
-    #[test]
-    fn ignore() {
-        let opts1 = options(&["--ignore=one,two,three"]);
-        let opts2 = options(&["--ignore", "one,two,three"]);
-        let opts3 = options(&["--ignore", "one two three"]);
-        // Not supported
-        //let opts4 = options("--ignore one two three");
-        //let opts5 = options("--ignore one --ignore two --ignore three");
-        //let opts6 = options("--ignore one --ignore two,three");
-        let correct = Options {
-            flag_format: Some("list".into()),
-            flag_color: Some("auto".into()),
-            flag_ignore: vec!["one".into(), "two".into(), "three".into()],
-            ..Options::default()
-        };
-
-        assert_eq!(correct, opts1);
-        assert_eq!(correct, opts2);
-        assert_eq!(correct, opts3);
-    }
-
-    #[test]
-    fn verbose() {
-        let opts1 = options(&["--verbose", "--verbose", "--verbose"]);
-        let correct = Options {
-            flag_format: Some("list".into()),
-            flag_color: Some("auto".into()),
-            flag_verbose: 3,
-            ..Options::default()
-        };
-
-        assert_eq!(correct, opts1);
-    }
-
-    #[test]
-    fn packages() {
-        let opts1 = options(&["--packages", "one,two"]);
-        let opts2 = options(&["--packages", "one two"]);
-        // Not Supported
-        //let opts3 = options(&["--packages","one","--packages","two"]);
-        //let opts4 = options(&["--packages", "one", "two"]);
-        let correct = Options {
-            flag_format: Some("list".into()),
-            flag_color: Some("auto".into()),
-            flag_packages: vec!["one".into(), "two".into()],
-            ..Options::default()
-        };
-
-        assert_eq!(correct, opts1);
-        assert_eq!(correct, opts2);
-    }
-}