Merge #312

312: Update clap to v3.1 r=kbknapp a=CosmicHorrorDev



Co-authored-by: Lovecraftian Horror <LovecraftianHorror@pm.me>
Co-authored-by: Kevin K <kbknapp@gmail.com>
diff --git a/Cargo.lock b/Cargo.lock
index 3dc3f0d..cabc7e7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,15 +18,6 @@
 ]
 
 [[package]]
-name = "ansi_term"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
-dependencies = [
- "winapi",
-]
-
-[[package]]
 name = "anyhow"
 version = "1.0.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -114,7 +105,7 @@
  "bytesize",
  "cargo-platform",
  "cargo-util",
- "clap 3.2.23",
+ "clap",
  "crates-io",
  "crossbeam-utils",
  "curl",
@@ -171,9 +162,10 @@
 dependencies = [
  "anyhow",
  "cargo",
- "clap 2.34.0",
+ "clap",
  "env_logger 0.10.0",
  "git2-curl",
+ "pretty_assertions",
  "semver",
  "serde",
  "serde_derive",
@@ -232,32 +224,32 @@
 
 [[package]]
 name = "clap"
-version = "2.34.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
-dependencies = [
- "ansi_term",
- "atty",
- "bitflags",
- "strsim 0.8.0",
- "textwrap 0.11.0",
- "unicode-width",
- "vec_map",
-]
-
-[[package]]
-name = "clap"
 version = "3.2.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
 dependencies = [
  "atty",
  "bitflags",
+ "clap_derive",
  "clap_lex",
  "indexmap",
- "strsim 0.10.0",
+ "once_cell",
+ "strsim",
  "termcolor",
- "textwrap 0.16.0",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
@@ -358,6 +350,16 @@
 ]
 
 [[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "curl"
 version = "0.4.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -389,6 +391,12 @@
 ]
 
 [[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
 name = "either"
 version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -566,6 +574,12 @@
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
 name = "hermit-abi"
 version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -931,6 +945,15 @@
 checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
 
 [[package]]
+name = "output_vt100"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
 name = "pathdiff"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -949,6 +972,42 @@
 checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
 
 [[package]]
+name = "pretty_assertions"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
+dependencies = [
+ "ctor",
+ "diff",
+ "output_vt100",
+ "yansi",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
 name = "proc-macro2"
 version = "1.0.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1170,12 +1229,6 @@
 
 [[package]]
 name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
-[[package]]
-name = "strsim"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
@@ -1235,15 +1288,6 @@
 
 [[package]]
 name = "textwrap"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
-name = "textwrap"
 version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
@@ -1357,12 +1401,6 @@
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
 [[package]]
-name = "vec_map"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
-
-[[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1487,3 +1525,9 @@
 version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
+
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
diff --git a/Cargo.toml b/Cargo.toml
index 635899b..4825db1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,7 +39,7 @@
 tabwriter = "1.2.1"
 tempfile = "3"
 toml = "~0.5.0"
-clap = "2.33.3"
+clap = { version = "3.1.6", features = ["derive"] }
 
 [dependencies.termcolor]
 optional = true
@@ -52,3 +52,6 @@
 
 [profile.release]
 lto = true
+
+[dev-dependencies]
+pretty_assertions = "1.2.0"
diff --git a/src/cli.rs b/src/cli.rs
index 7842b15..cc22490 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,52 +1,106 @@
-use clap::{
-    arg_enum, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches,
-    SubCommand,
-};
+use std::{ffi::OsString, fmt};
 
-arg_enum! {
-    #[derive(Copy, Clone, Debug, PartialEq)]
-    pub enum Format {
-        List,
-        Json,
-    }
+use clap::{ArgEnum, Parser, Subcommand};
+
+#[derive(ArgEnum, 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
-    }
+#[derive(ArgEnum, Copy, Clone, Debug, PartialEq)]
+pub enum Color {
+    Auto,
+    Never,
+    Always,
 }
 
 impl Default for Color {
     fn default() -> Self { Color::Auto }
 }
 
+impl fmt::Display for Color {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(&format!("{:?}", self).to_ascii_lowercase())
+    }
+}
+
+#[derive(Parser, Debug)]
+#[clap(bin_name = "cargo")]
+struct Cargo {
+    #[clap(subcommand)]
+    command: CargoCommand,
+}
+
+#[derive(Subcommand, Debug)]
+enum CargoCommand {
+    Outdated(Options),
+}
+
 /// Options from CLI arguments
-#[derive(Debug, PartialEq, Default)]
+#[derive(Parser, Debug, PartialEq, Default)]
+#[clap(version)]
+#[clap(about = "Displays information about project dependency versions")]
 pub struct Options {
+    /// Output formatting
+    #[clap(long, arg_enum, ignore_case = true, default_value_t = Default::default())]
     pub format: Format,
+    /// Output coloring
+    #[clap(long, arg_enum, ignore_case = true, default_value_t = Default::default())]
     pub color: Color,
+    /// Space-separated list of features
+    #[clap(long, use_value_delimiter = true)]
     pub features: Vec<String>,
+    /// Dependencies to not print in the output (comma separated or one per '--ignore' argument)
+    #[clap(short, long, value_name = "DEPENDENCIES", use_value_delimiter = true)]
     pub ignore: Vec<String>,
+    /// Dependencies to exclude from building (comma separated or one per '--exclude' argument)
+    #[clap(
+        short = 'x',
+        long,
+        value_name = "DEPENDENCIES",
+        use_value_delimiter = true
+    )]
     pub exclude: Vec<String>,
+    /// Path to the Cargo.toml file to use (Default to Cargo.toml in project root)
+    #[clap(short, long, value_name = "PATH")]
     pub manifest_path: Option<String>,
+    /// Suppresses warnings
+    #[clap(short, long)]
     pub quiet: bool,
+    /// Use verbose output
+    #[clap(short, long, parse(from_occurrences))]
     pub verbose: u64,
+    /// The exit code to return on new versions found
+    #[clap(long, value_name = "NUM", default_value_t = Default::default())]
     pub exit_code: i32,
+    /// Packages to inspect for updates (comma separated or one per --packages' argument)
+    #[clap(short, long, value_name = "PKGS", use_value_delimiter = true)]
     pub packages: Vec<String>,
+    /// Package to treat as the root package
+    #[clap(short, long)]
     pub root: Option<String>,
+    /// How deep in the dependency chain to search (Defaults to all dependencies)
+    #[clap(short, long, value_name = "NUM")]
     pub depth: Option<i32>,
+    /// Only check root dependencies (Equivalent to --depth=1)
+    #[clap(short = 'R', long)]
     pub root_deps_only: bool,
+    /// Checks updates for all workspace members rather than only the root package
+    #[clap(short, long)]
     pub workspace: bool,
+    /// Ignores channels for latest updates
+    #[clap(short, long)]
     pub aggressive: bool,
+    /// Ignore relative dependencies external to workspace and check root dependencies only
+    #[clap(short = 'e', long = "ignore-external-rel")]
     pub workspace_only: bool,
+    /// Run without accessing the network (useful for testing w/ local registries)
+    #[clap(short, long)]
     pub offline: bool,
 }
 
@@ -62,225 +116,55 @@
     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_only: m.is_present("ignore-external-rel"),
-            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);
-        }
-
-        if m.is_present("ignore-external-rel") {
-            opts.depth = Some(1);
-            opts.root_deps_only = true;
-        }
-
-        opts
+pub fn parse() -> Options {
+    match try_parse_from(std::env::args_os()) {
+        Ok(opts) => opts,
+        Err(clap_err) => clap_err.exit(),
     }
 }
 
-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("aggressive")
-                        .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("ignore-external-rel")
-                        .short("e")
-                        .long("ignore-external-rel")
-                        .help("Ignore relative dependencies external to workspace and check root dependencies only."),
-                )
-                .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"))
-            )
+fn split_elem_by_ascii_whitespace(slice: &[String]) -> Vec<String> {
+    slice
+        .iter()
+        .flat_map(|x| x.split_ascii_whitespace())
+        .map(ToOwned::to_owned)
+        .collect()
 }
 
-pub fn parse() -> Options {
-    let matches = build().get_matches();
+fn try_parse_from(
+    args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
+) -> clap::Result<Options> {
+    let CargoCommand::Outdated(mut opts) = Cargo::try_parse_from(args)?.command;
 
-    Options::from(matches.subcommand_matches("outdated").unwrap())
+    opts.exclude = split_elem_by_ascii_whitespace(&opts.exclude);
+    opts.features = split_elem_by_ascii_whitespace(&opts.features);
+    opts.ignore = split_elem_by_ascii_whitespace(&opts.ignore);
+    opts.packages = split_elem_by_ascii_whitespace(&opts.packages);
+
+    if opts.root_deps_only {
+        opts.depth = Some(1);
+    }
+
+    if opts.workspace_only {
+        opts.depth = Some(1);
+        opts.root_deps_only = true;
+    }
+
+    Ok(opts)
 }
 
 #[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())
-    }
+    use pretty_assertions::assert_eq;
 
-    fn options_fail(args: &[&str]) -> clap::Result<ArgMatches<'static>> {
+    fn options(args: &[&str]) -> Options { options_fail(args).unwrap() }
+
+    fn options_fail(args: &[&str]) -> clap::Result<Options> {
         let mut argv = vec!["cargo", "outdated"];
         argv.extend(args);
-        build().get_matches_from_safe(argv)
+        try_parse_from(argv)
     }
 
     #[test]
@@ -348,10 +232,8 @@
         let res = options_fail(&["--features", "one", "two"]);
         assert!(res.is_err());
         assert_eq!(
-            res.as_ref().unwrap_err().kind,
+            res.as_ref().unwrap_err().kind(),
             clap::ErrorKind::UnknownArgument,
-            "{:?}",
-            res.as_ref().unwrap_err().kind
         );
     }
 
@@ -379,10 +261,8 @@
         let res = options_fail(&["--exclude", "one", "two"]);
         assert!(res.is_err());
         assert_eq!(
-            res.as_ref().unwrap_err().kind,
+            res.as_ref().unwrap_err().kind(),
             clap::ErrorKind::UnknownArgument,
-            "{:?}",
-            res.as_ref().unwrap_err().kind
         );
     }
 
@@ -410,10 +290,8 @@
         let res = options_fail(&["--ignore", "one", "two"]);
         assert!(res.is_err());
         assert_eq!(
-            res.as_ref().unwrap_err().kind,
+            res.as_ref().unwrap_err().kind(),
             clap::ErrorKind::UnknownArgument,
-            "{:?}",
-            res.as_ref().unwrap_err().kind
         );
     }
 
@@ -448,10 +326,8 @@
         let res = options_fail(&["--packages", "one", "two"]);
         assert!(res.is_err());
         assert_eq!(
-            res.as_ref().unwrap_err().kind,
+            res.as_ref().unwrap_err().kind(),
             clap::ErrorKind::UnknownArgument,
-            "{:?}",
-            res.as_ref().unwrap_err().kind
         );
     }
 
@@ -471,10 +347,8 @@
         let res = options_fail(&["--format", "foobar"]);
         assert!(res.is_err());
         assert_eq!(
-            res.as_ref().unwrap_err().kind,
+            res.as_ref().unwrap_err().kind(),
             clap::ErrorKind::InvalidValue,
-            "{:?}",
-            res.as_ref().unwrap_err().kind
         );
     }
 
@@ -494,10 +368,8 @@
         let res = options_fail(&["--color", "foobar"]);
         assert!(res.is_err());
         assert_eq!(
-            res.as_ref().unwrap_err().kind,
+            res.as_ref().unwrap_err().kind(),
             clap::ErrorKind::InvalidValue,
-            "{:?}",
-            res.as_ref().unwrap_err().kind
         );
     }
 }