Repository: taiki-e/cargo-hack (language: rust)

Issue:
Decide the behavior of --verbose flag
- First, we need to figure out what this flag is currently doing.
- This flag is not currently propagated to cargo, but it may be better to propagate it to cargo if specified multiple times.
- There should be other things that we would prefer to output if this flag is specified.



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f49d70ac..87e41d31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
 
 ## [Unreleased]
 
+- Fix handling of combined short flags. ([#143](https://github.com/taiki-e/cargo-hack/pull/143))
+
 ## [0.5.9] - 2021-12-29
 
 - Fix an error when using old cargo with a dependency graph containing 2021 edition crates. ([#138](https://github.com/taiki-e/cargo-hack/pull/138))
diff --git a/Cargo.toml b/Cargo.toml
index 3fefc318..935e2787 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ targets = ["x86_64-unknown-linux-gnu"]
 anyhow = "1.0.34"
 atty = "0.2.11"
 ctrlc = { version = "3.1.4", features = ["termination"] }
+lexopt = "0.2"
 serde_json = "1"
 termcolor = "1.1"
 toml = "0.5.2"
diff --git a/src/cli.rs b/src/cli.rs
index 4964d38a..2672f608 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,27 +1,29 @@
 use std::{
     env,
     ffi::{OsStr, OsString},
-    fmt,
-    iter::Peekable,
-    mem,
+    fmt, mem,
 };
 
-use anyhow::{bail, format_err, Error, Result};
+use anyhow::{bail, format_err, Result};
+use lexopt::{
+    Arg::{Long, Short, Value},
+    ValueExt,
+};
 
 use crate::{term, Feature, Rustup};
 
-pub(crate) struct Args<'a> {
-    pub(crate) leading_args: Vec<&'a str>,
-    pub(crate) trailing_args: &'a [String],
+pub(crate) struct Args {
+    pub(crate) leading_args: Vec<String>,
+    pub(crate) trailing_args: Vec<String>,
 
-    pub(crate) subcommand: Option<&'a str>,
+    pub(crate) subcommand: Option<String>,
 
     /// --manifest-path <PATH>
-    pub(crate) manifest_path: Option<&'a str>,
+    pub(crate) manifest_path: Option<String>,
     /// -p, --package <SPEC>...
-    pub(crate) package: Vec<&'a str>,
+    pub(crate) package: Vec<String>,
     /// --exclude <SPEC>...
-    pub(crate) exclude: Vec<&'a str>,
+    pub(crate) exclude: Vec<String>,
     /// --workspace, (--all)
     pub(crate) workspace: bool,
     /// --each-feature
@@ -43,13 +45,13 @@ pub(crate) struct Args<'a> {
     /// --keep-going
     pub(crate) keep_going: bool,
     /// --version-range
-    pub(crate) version_range: Option<&'a str>,
+    pub(crate) version_range: Option<String>,
     /// --version-step
-    pub(crate) version_step: Option<&'a str>,
+    pub(crate) version_step: Option<String>,
 
     // options for --each-feature and --feature-powerset
     /// --optional-deps [DEPS]...
-    pub(crate) optional_deps: Option<Vec<&'a str>>,
+    pub(crate) optional_deps: Option<Vec<String>>,
     /// --include-features
     pub(crate) include_features: Vec<Feature>,
     /// --include-deps-features
@@ -58,7 +60,7 @@ pub(crate) struct Args<'a> {
     // Note: These values are not always exactly the same as the input.
     // Error messages should not assume that these options have been specified.
     /// --exclude-features <FEATURES>..., --skip <FEATURES>...
-    pub(crate) exclude_features: Vec<&'a str>,
+    pub(crate) exclude_features: Vec<String>,
     /// --exclude-no-default-features
     pub(crate) exclude_no_default_features: bool,
     /// --exclude-all-features
@@ -72,7 +74,7 @@ pub(crate) struct Args<'a> {
 
     // options that will be propagated to cargo
     /// --features <FEATURES>...
-    pub(crate) features: Vec<&'a str>,
+    pub(crate) features: Vec<String>,
 
     // propagated to cargo (as a part of leading_args)
     /// --no-default-features
@@ -80,453 +82,501 @@ pub(crate) struct Args<'a> {
     // Note: specifying multiple `--target` flags requires unstable `-Z multitarget`,
     // so cargo-hack currently only supports a single `--target`.
     /// --target <TRIPLE>...
-    pub(crate) target: Option<&'a str>,
-}
-
-pub(crate) fn raw() -> Result<Vec<String>> {
-    let mut args = env::args_os();
-    let _ = args.next(); // executable name
-    handle_args(args)
-}
-
-fn handle_args(args: impl IntoIterator<Item = impl Into<OsString>>) -> Result<Vec<String>> {
-    // Adapted from https://github.com/rust-lang/rust/blob/3bc9dd0dd293ab82945e35888ed6d7ab802761ef/compiler/rustc_driver/src/lib.rs#L1365-L1375.
-    args.into_iter()
-        .enumerate()
-        .map(|(i, arg)| {
-            arg.into()
-                .into_string()
-                .map_err(|arg| format_err!("argument {} is not valid Unicode: {:?}", i + 1, arg))
-        })
-        .collect()
+    pub(crate) target: Option<String>,
 }
 
-pub(crate) fn parse_args<'a>(raw: &'a [String], cargo: &OsStr) -> Result<Args<'a>> {
-    let mut iter = raw.iter();
-    let args = &mut iter.by_ref().map(String::as_str).peekable();
-    match args.next() {
-        Some(a) if a == "hack" => {}
-        Some(a) => mini_usage(&format!("expected subcommand 'hack', found argument '{}'", a))?,
-        None => {
-            println!("{}", Help::short());
-            std::process::exit(1);
+impl Args {
+    pub(crate) fn parse(cargo: &OsStr) -> Result<Self> {
+        const SUBCMD: &str = "hack";
+
+        // rustc/cargo args must be valid Unicode
+        // https://github.com/rust-lang/rust/blob/3bc9dd0dd293ab82945e35888ed6d7ab802761ef/compiler/rustc_driver/src/lib.rs#L1365-L1375
+        fn handle_args(
+            args: impl IntoIterator<Item = impl Into<OsString>>,
+        ) -> impl Iterator<Item = Result<String>> {
+            args.into_iter().enumerate().map(|(i, arg)| {
+                arg.into().into_string().map_err(|arg| {
+                    format_err!("argument {} is not valid Unicode: {:?}", i + 1, arg)
+                })
+            })
         }
-    }
 
-    let mut leading = Vec::new();
-    let mut subcommand: Option<&'a str> = None;
-
-    let mut manifest_path = None;
-    let mut color = None;
-
-    let mut package = Vec::new();
-    let mut exclude = Vec::new();
-    let mut features = Vec::new();
-
-    let mut workspace = None;
-    let mut no_dev_deps = false;
-    let mut remove_dev_deps = false;
-    let mut each_feature = false;
-    let mut feature_powerset = false;
-    let mut ignore_private = false;
-    let mut ignore_unknown_features = false;
-    let mut clean_per_run = false;
-    let mut clean_per_version = false;
-    let mut keep_going = false;
-    let mut version_range = None;
-    let mut version_step = None;
-
-    let mut optional_deps = None;
-    let mut include_features = Vec::new();
-    let mut include_deps_features = false;
-
-    let mut exclude_features = Vec::new();
-    let mut exclude_no_default_features = false;
-    let mut exclude_all_features = false;
-
-    let mut group_features = Vec::new();
-    let mut depth = None;
-
-    let mut verbose = false;
-    let mut no_default_features = false;
-    let mut all_features = false;
-    let mut target = None;
-
-    let res = (|| -> Result<()> {
-        while let Some(arg) = args.next() {
-            // stop at `--`
-            // 1. `cargo hack check --no-dev-deps`
-            //   first:  `cargo hack check --no-dev-deps` (filtered and passed to `cargo`)
-            //   second: (empty)
-            // 2. `cargo hack test --each-feature -- --ignored`
-            //   first:  `cargo hack test --each-feature` (filtered and passed to `cargo`)
-            //   second: `--ignored` (passed directly to `cargo` with `--`)
+        let mut raw_args = handle_args(env::args_os());
+        raw_args.next(); // cargo
+        match raw_args.next().transpose()? {
+            Some(a) if a == SUBCMD => {}
+            Some(a) => bail!("expected subcommand '{}', found argument '{}'", SUBCMD, a),
+            None => bail!("expected subcommand '{}'", SUBCMD),
+        }
+        let mut args = vec![];
+        for arg in &mut raw_args {
+            let arg = arg?;
             if arg == "--" {
                 break;
             }
-
-            if !arg.starts_with('-') {
-                subcommand.get_or_insert(arg);
-                leading.push(arg);
-                continue;
-            }
+            args.push(arg);
+        }
+        let rest = raw_args.collect::<Result<Vec<_>>>()?;
+
+        let mut cargo_args = Vec::new();
+        let mut subcommand: Option<String> = None;
+
+        let mut manifest_path = None;
+        let mut color = None;
+
+        let mut package = Vec::new();
+        let mut exclude = Vec::new();
+        let mut features = Vec::new();
+
+        let mut workspace = false;
+        let mut no_dev_deps = false;
+        let mut remove_dev_deps = false;
+        let mut each_feature = false;
+        let mut feature_powerset = false;
+        let mut ignore_private = false;
+        let mut ignore_unknown_features = false;
+        let mut clean_per_run = false;
+        let mut clean_per_version = false;
+        let mut keep_going = false;
+        let mut version_range = None;
+        let mut version_step = None;
+
+        let mut optional_deps = None;
+        let mut include_features = Vec::new();
+        let mut include_deps_features = false;
+
+        let mut exclude_features = Vec::new();
+        let mut exclude_no_default_features = false;
+        let mut exclude_all_features = false;
+
+        let mut group_features: Vec<String> = Vec::new();
+        let mut depth = None;
+
+        let mut verbose = 0;
+        let mut no_default_features = false;
+        let mut all_features = false;
+        let mut target = None;
+
+        let mut parser = lexopt::Parser::from_args(args);
+        let mut next_flag: Option<OwnedFlag> = None;
+        loop {
+            let arg = next_flag.take();
+            let arg = match &arg {
+                Some(flag) => flag.as_arg(),
+                None => match parser.next()? {
+                    Some(arg) => arg,
+                    None => break,
+                },
+            };
 
             macro_rules! parse_opt {
-                ($opt:ident, $propagate:expr, $pat:expr $(,)?) => {
-                    if let Some(val) = parse_opt(arg, args, subcommand, $pat, true)? {
-                        let val = val.unwrap();
-                        if $opt.is_some() {
-                            multi_arg($pat, subcommand)?;
-                        }
-                        $opt = Some(val);
-                        if $propagate {
-                            leading.push($pat);
-                            leading.push(val);
-                        }
-                        continue;
+                ($opt:ident, $propagate:expr $(,)?) => {{
+                    if $opt.is_some() {
+                        multi_arg(&arg, subcommand.as_deref())?;
                     }
-                };
+                    if $propagate {
+                        cargo_args.push(format_flag(&arg));
+                    }
+                    let val = parser.value()?.parse::<String>()?;
+                    if $propagate {
+                        cargo_args.push(val.clone());
+                    }
+                    $opt = Some(val);
+                }};
             }
 
             macro_rules! parse_multi_opt {
-                ($v:ident, $allow_split:expr, $pat:expr $(,)?) => {
-                    if let Some(val) = parse_opt(arg, args, subcommand, $pat, true)? {
-                        let val = val.unwrap();
-                        if $allow_split {
-                            if val.contains(',') {
-                                $v.extend(val.split(','));
-                            } else {
-                                $v.extend(val.split(' '));
-                            }
-                        } else {
-                            $v.push(val);
-                        }
-                        continue;
+                ($v:ident $(,)?) => {{
+                    let val = parser.value()?;
+                    let mut val = val.to_str().unwrap();
+                    if val.starts_with('\'') && val.ends_with('\'')
+                        || val.starts_with('"') && val.ends_with('"')
+                    {
+                        val = &val[1..val.len() - 1];
                     }
-                };
+                    if val.contains(',') {
+                        $v.extend(val.split(',').map(str::to_owned));
+                    } else {
+                        $v.extend(val.split(' ').map(str::to_owned));
+                    }
+                }};
             }
 
             macro_rules! parse_flag {
-                ($flag:ident) => {
+                ($flag:ident $(,)?) => {
                     if mem::replace(&mut $flag, true) {
-                        multi_arg(&arg, subcommand)?;
-                    } else {
-                        continue;
+                        multi_arg(&arg, subcommand.as_deref())?;
                     }
                 };
             }
 
-            parse_opt!(manifest_path, false, "--manifest-path");
-            parse_opt!(depth, false, "--depth");
-            parse_opt!(color, true, "--color");
-            parse_opt!(version_range, false, "--version-range");
-            parse_opt!(version_step, false, "--version-step");
-            parse_opt!(target, true, "--target");
-
-            parse_multi_opt!(package, false, "--package");
-            parse_multi_opt!(package, false, "-p");
-            parse_multi_opt!(exclude, false, "--exclude");
-            parse_multi_opt!(features, true, "--features");
-            parse_multi_opt!(exclude_features, true, "--skip");
-            parse_multi_opt!(exclude_features, true, "--exclude-features",);
-            parse_multi_opt!(include_features, true, "--include-features",);
-            parse_multi_opt!(group_features, false, "--group-features",);
-
-            if let Some(val) = parse_opt(arg, args, subcommand, "--optional-deps", false)? {
-                if optional_deps.is_some() {
-                    multi_arg(arg, subcommand)?;
-                }
-                let optional_deps = optional_deps.get_or_insert_with(Vec::new);
-                if let Some(val) = val {
+            match arg {
+                Long("manifest-path") => parse_opt!(manifest_path, false),
+                Long("depth") => parse_opt!(depth, false),
+                Long("color") => parse_opt!(color, true),
+                Long("version-range") => parse_opt!(version_range, false),
+                Long("version-step") => parse_opt!(version_step, false),
+                Long("target") => parse_opt!(target, true),
+
+                Short('p') | Long("package") => package.push(parser.value()?.parse()?),
+                Long("exclude") => exclude.push(parser.value()?.parse()?),
+                Long("group-features") => group_features.push(parser.value()?.parse()?),
+
+                Long("features") => parse_multi_opt!(features),
+                Long("skip") | Long("exclude-features") => parse_multi_opt!(exclude_features),
+                Long("include-features") => parse_multi_opt!(include_features),
+
+                Long("optional-deps") => {
+                    if optional_deps.is_some() {
+                        multi_arg(&arg, subcommand.as_deref())?;
+                    }
+                    let optional_deps = optional_deps.get_or_insert_with(Vec::new);
+                    let val = match parser.optional_value() {
+                        Some(val) => val,
+                        None => match parser.next()? {
+                            Some(Value(val)) => val,
+                            Some(arg) => {
+                                next_flag = Some(arg.into());
+                                continue;
+                            }
+                            None => break,
+                        },
+                    };
+                    let mut val = val.to_str().unwrap();
+                    if val.starts_with('\'') && val.ends_with('\'')
+                        || val.starts_with('"') && val.ends_with('"')
+                    {
+                        val = &val[1..val.len() - 1];
+                    }
                     if val.contains(',') {
-                        optional_deps.extend(val.split(','));
+                        optional_deps.extend(val.split(',').map(str::to_owned));
                     } else {
-                        optional_deps.extend(val.split(' '));
+                        optional_deps.extend(val.split(' ').map(str::to_owned));
                     }
                 }
-                continue;
-            }
 
-            match &*arg {
-                "--workspace" | "--all" => {
-                    if let Some(arg) = workspace.replace(arg) {
-                        multi_arg(arg, subcommand)?;
-                    }
-                    continue;
-                }
-                "--no-dev-deps" => parse_flag!(no_dev_deps),
-                "--remove-dev-deps" => parse_flag!(remove_dev_deps),
-                "--each-feature" => parse_flag!(each_feature),
-                "--feature-powerset" => parse_flag!(feature_powerset),
-                "--ignore-private" => parse_flag!(ignore_private),
-                "--exclude-no-default-features" => parse_flag!(exclude_no_default_features),
-                "--exclude-all-features" => parse_flag!(exclude_all_features),
-                "--include-deps-features" => parse_flag!(include_deps_features),
-                "--clean-per-run" => parse_flag!(clean_per_run),
-                "--clean-per-version" => parse_flag!(clean_per_version),
-                "--keep-going" => parse_flag!(keep_going),
-                "--ignore-unknown-features" => parse_flag!(ignore_unknown_features),
-                // allow multiple uses
-                "--verbose" | "-v" | "-vv" => {
-                    verbose = true;
-                    continue;
-                }
+                Long("workspace") | Long("all") => parse_flag!(workspace),
+                Long("no-dev-deps") => parse_flag!(no_dev_deps),
+                Long("remove-dev-deps") => parse_flag!(remove_dev_deps),
+                Long("each-feature") => parse_flag!(each_feature),
+                Long("feature-powerset") => parse_flag!(feature_powerset),
+                Long("ignore-private") => parse_flag!(ignore_private),
+                Long("exclude-no-default-features") => parse_flag!(exclude_no_default_features),
+                Long("exclude-all-features") => parse_flag!(exclude_all_features),
+                Long("include-deps-features") => parse_flag!(include_deps_features),
+                Long("clean-per-run") => parse_flag!(clean_per_run),
+                Long("clean-per-version") => parse_flag!(clean_per_version),
+                Long("keep-going") => parse_flag!(keep_going),
+                Long("ignore-unknown-features") => parse_flag!(ignore_unknown_features),
+                Short('v') | Long("verbose") => verbose += 1,
 
                 // detect similar arg
-                "--each-features" => similar_arg(arg, subcommand, "--each-feature", None)?,
-                "--features-powerset" => similar_arg(arg, subcommand, "--feature-powerset", None)?,
+                Long("each-features") => {
+                    similar_arg(&arg, subcommand.as_deref(), "--each-feature", None)?
+                }
+                Long("features-powerset") => {
+                    similar_arg(&arg, subcommand.as_deref(), "--feature-powerset", None)?
+                }
 
                 // propagated
-                "--no-default-features" => no_default_features = true,
-                "--all-features" => all_features = true,
-                _ => {}
-            }
+                Long("no-default-features") => {
+                    no_default_features = true;
+                    cargo_args.push("--no-default-features".to_owned());
+                }
+                Long("all-features") => {
+                    all_features = true;
+                    cargo_args.push("--all-features".to_owned());
+                }
 
-            removed_flags(arg)?;
+                Short('h') if subcommand.is_none() => {
+                    println!("{}", Help::short());
+                    std::process::exit(0);
+                }
+                Long("help") if subcommand.is_none() => {
+                    println!("{}", Help::long());
+                    std::process::exit(0);
+                }
+                Short('V') | Long("version") if subcommand.is_none() => {
+                    println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
+                    std::process::exit(0);
+                }
 
-            leading.push(arg);
+                // passthrough
+                Long(flag) => {
+                    removed_flags(flag)?;
+                    let flag = format!("--{}", flag);
+                    if let Some(val) = parser.optional_value() {
+                        cargo_args.push(format!("{}={}", flag, val.parse::<String>()?));
+                    } else {
+                        cargo_args.push(flag);
+                    }
+                }
+                Short(flag) => {
+                    if matches!(flag, 'q' | 'r') {
+                        // To handle combined short flags properly, handle known
+                        // short flags without value as special cases.
+                        cargo_args.push(format!("-{}", flag));
+                    } else if let Some(val) = parser.optional_value() {
+                        cargo_args.push(format!("-{}{}", flag, val.parse::<String>()?));
+                    } else {
+                        cargo_args.push(format!("-{}", flag));
+                    }
+                }
+                Value(val) => {
+                    let val = val.parse::<String>()?;
+                    if subcommand.is_none() {
+                        subcommand = Some(val.clone());
+                    }
+                    cargo_args.push(val);
+                }
+            }
         }
 
-        Ok(())
-    })();
-
-    term::set_coloring(color)?;
-
-    res?;
+        term::set_coloring(color.as_deref())?;
 
-    if !exclude.is_empty() && workspace.is_none() {
-        // TODO: This is the same behavior as cargo, but should we allow it to be used
-        // in the root of a virtual workspace as well?
-        requires("--exclude", &["--workspace"])?;
-    }
-    if ignore_unknown_features {
-        if features.is_empty() && include_features.is_empty() && group_features.is_empty() {
-            requires("--ignore-unknown-features", &[
-                "--features",
-                "--include-features",
-                "--group-features",
-            ])?;
-        }
-        if !include_features.is_empty() {
-            // TODO: implement
-            warn!(
-                "--ignore-unknown-features for --include-features is not fully implemented and may not work as intended"
-            );
-        }
-        if !group_features.is_empty() {
-            // TODO: implement
-            warn!(
-                "--ignore-unknown-features for --group-features is not fully implemented and may not work as intended"
-            );
+        if !exclude.is_empty() && !workspace {
+            // TODO: This is the same behavior as cargo, but should we allow it to be used
+            // in the root of a virtual workspace as well?
+            requires("--exclude", &["--workspace"])?;
         }
-    }
-    if !each_feature && !feature_powerset {
-        if optional_deps.is_some() {
-            requires("--optional-deps", &["--each-feature", "--feature-powerset"])?;
-        } else if !exclude_features.is_empty() {
-            requires("--exclude-features (--skip)", &["--each-feature", "--feature-powerset"])?;
-        } else if exclude_no_default_features {
-            requires("--exclude-no-default-features", &["--each-feature", "--feature-powerset"])?;
-        } else if exclude_all_features {
-            requires("--exclude-all-features", &["--each-feature", "--feature-powerset"])?;
-        } else if !include_features.is_empty() {
-            requires("--include-features", &["--each-feature", "--feature-powerset"])?;
-        } else if include_deps_features {
-            requires("--include-deps-features", &["--each-feature", "--feature-powerset"])?;
+        if ignore_unknown_features {
+            if features.is_empty() && include_features.is_empty() && group_features.is_empty() {
+                requires("--ignore-unknown-features", &[
+                    "--features",
+                    "--include-features",
+                    "--group-features",
+                ])?;
+            }
+            if !include_features.is_empty() {
+                let _guard = term::warn::scoped(false);
+                // TODO: implement
+                warn!(
+                    "--ignore-unknown-features for --include-features is not fully implemented and may not work as intended"
+                );
+            }
+            if !group_features.is_empty() {
+                let _guard = term::warn::scoped(false);
+                // TODO: implement
+                warn!(
+                    "--ignore-unknown-features for --group-features is not fully implemented and may not work as intended"
+                );
+            }
         }
-    }
-    if !feature_powerset {
-        if depth.is_some() {
-            requires("--depth", &["--feature-powerset"])?;
-        } else if !group_features.is_empty() {
-            requires("--group-features", &["--feature-powerset"])?;
+        if !each_feature && !feature_powerset {
+            if optional_deps.is_some() {
+                requires("--optional-deps", &["--each-feature", "--feature-powerset"])?;
+            } else if !exclude_features.is_empty() {
+                requires("--exclude-features (--skip)", &["--each-feature", "--feature-powerset"])?;
+            } else if exclude_no_default_features {
+                requires("--exclude-no-default-features", &[
+                    "--each-feature",
+                    "--feature-powerset",
+                ])?;
+            } else if exclude_all_features {
+                requires("--exclude-all-features", &["--each-feature", "--feature-powerset"])?;
+            } else if !include_features.is_empty() {
+                requires("--include-features", &["--each-feature", "--feature-powerset"])?;
+            } else if include_deps_features {
+                requires("--include-deps-features", &["--each-feature", "--feature-powerset"])?;
+            }
         }
-    }
-    if version_range.is_none() {
-        if version_step.is_some() {
-            requires("--version-step", &["--version-range"])?;
+        if !feature_powerset {
+            if depth.is_some() {
+                requires("--depth", &["--feature-powerset"])?;
+            } else if !group_features.is_empty() {
+                requires("--group-features", &["--feature-powerset"])?;
+            }
         }
-        if clean_per_version {
-            requires("--clean-per-version", &["--version-range"])?;
+        if version_range.is_none() {
+            if version_step.is_some() {
+                requires("--version-step", &["--version-range"])?;
+            }
+            if clean_per_version {
+                requires("--clean-per-version", &["--version-range"])?;
+            }
         }
-    }
 
-    let depth = depth.map(str::parse::<usize>).transpose()?;
-    let group_features =
-        group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| {
-            let g = if g.contains(',') {
-                g.split(',')
-            } else if g.contains(' ') {
-                g.split(' ')
-            } else {
-                bail!(
-                    "--group-features requires a list of two or more features separated by space \
-                     or comma"
-                );
-            };
-            v.push(Feature::group(g));
-            Ok(v)
-        })?;
-
-    if let Some(subcommand) = subcommand {
-        match subcommand {
-            "test" | "bench" => {
-                if remove_dev_deps {
+        let depth = depth.as_deref().map(str::parse::<usize>).transpose()?;
+        let group_features =
+            group_features.iter().try_fold(Vec::with_capacity(group_features.len()), |mut v, g| {
+                let g = if g.contains(',') {
+                    g.split(',')
+                } else if g.contains(' ') {
+                    g.split(' ')
+                } else {
                     bail!(
-                        "--remove-dev-deps may not be used together with {} subcommand",
-                        subcommand
+                        "--group-features requires a list of two or more features separated by space \
+                         or comma"
                     );
-                } else if no_dev_deps {
-                    bail!("--no-dev-deps may not be used together with {} subcommand", subcommand);
+                };
+                v.push(Feature::group(g));
+                Ok(v)
+            })?;
+
+        if let Some(subcommand) = subcommand.as_deref() {
+            match subcommand {
+                "test" | "bench" => {
+                    if remove_dev_deps {
+                        bail!(
+                            "--remove-dev-deps may not be used together with {} subcommand",
+                            subcommand
+                        );
+                    } else if no_dev_deps {
+                        bail!(
+                            "--no-dev-deps may not be used together with {} subcommand",
+                            subcommand
+                        );
+                    }
                 }
+                // cargo-hack may not be used together with subcommands that do not have the --manifest-path flag.
+                "install" => {
+                    bail!("cargo-hack may not be used together with {} subcommand", subcommand)
+                }
+                _ => {}
             }
-            // cargo-hack may not be used together with subcommands that do not have the --manifest-path flag.
-            "install" => {
-                bail!("cargo-hack may not be used together with {} subcommand", subcommand)
-            }
-            _ => {}
         }
-    }
 
-    if let Some(pos) = leading.iter().position(|a| match &**a {
-        "--example" | "--examples" | "--test" | "--tests" | "--bench" | "--benches"
-        | "--all-targets" => true,
-        _ => a.starts_with("--example=") || a.starts_with("--test=") || a.starts_with("--bench="),
-    }) {
-        if remove_dev_deps {
-            conflicts("--remove-dev-deps", leading[pos])?;
-        } else if no_dev_deps {
-            conflicts("--no-dev-deps", leading[pos])?;
+        if let Some(pos) = cargo_args.iter().position(|a| match &**a {
+            "--example" | "--examples" | "--test" | "--tests" | "--bench" | "--benches"
+            | "--all-targets" => true,
+            _ => {
+                a.starts_with("--example=") || a.starts_with("--test=") || a.starts_with("--bench=")
+            }
+        }) {
+            if remove_dev_deps {
+                conflicts("--remove-dev-deps", &cargo_args[pos])?;
+            } else if no_dev_deps {
+                conflicts("--no-dev-deps", &cargo_args[pos])?;
+            }
         }
-    }
 
-    if !include_features.is_empty() {
-        if optional_deps.is_some() {
-            conflicts("--include-features", "--optional-deps")?;
-        } else if include_deps_features {
-            conflicts("--include-features", "--include-deps-features")?;
+        if !include_features.is_empty() {
+            if optional_deps.is_some() {
+                conflicts("--include-features", "--optional-deps")?;
+            } else if include_deps_features {
+                conflicts("--include-features", "--include-deps-features")?;
+            }
         }
-    }
 
-    if no_dev_deps && remove_dev_deps {
-        conflicts("--no-dev-deps", "--remove-dev-deps")?;
-    }
-    if each_feature && feature_powerset {
-        conflicts("--each-feature", "--feature-powerset")?;
-    }
-    if all_features {
-        if each_feature {
-            conflicts("--all-features", "--each-feature")?;
-        } else if feature_powerset {
-            conflicts("--all-features", "--feature-powerset")?;
+        if no_dev_deps && remove_dev_deps {
+            conflicts("--no-dev-deps", "--remove-dev-deps")?;
         }
-    }
-    if no_default_features {
-        if each_feature {
-            conflicts("--no-default-features", "--each-feature")?;
-        } else if feature_powerset {
-            conflicts("--no-default-features", "--feature-powerset")?;
+        if each_feature && feature_powerset {
+            conflicts("--each-feature", "--feature-powerset")?;
         }
-    }
-
-    for f in &exclude_features {
-        if features.contains(f) {
-            bail!("feature `{}` specified by both --exclude-features and --features", f);
+        if all_features {
+            if each_feature {
+                conflicts("--all-features", "--each-feature")?;
+            } else if feature_powerset {
+                conflicts("--all-features", "--feature-powerset")?;
+            }
         }
-        if optional_deps.as_ref().map_or(false, |d| d.contains(f)) {
-            bail!("feature `{}` specified by both --exclude-features and --optional-deps", f);
+        if no_default_features {
+            if each_feature {
+                conflicts("--no-default-features", "--each-feature")?;
+            } else if feature_powerset {
+                conflicts("--no-default-features", "--feature-powerset")?;
+            }
         }
-        if group_features.iter().any(|v| v.matches(f)) {
-            bail!("feature `{}` specified by both --exclude-features and --group-features", f);
+
+        for f in &exclude_features {
+            if features.contains(f) {
+                bail!("feature `{}` specified by both --exclude-features and --features", f);
+            }
+            if optional_deps.as_ref().map_or(false, |d| d.contains(f)) {
+                bail!("feature `{}` specified by both --exclude-features and --optional-deps", f);
+            }
+            if group_features.iter().any(|v| v.matches(f)) {
+                bail!("feature `{}` specified by both --exclude-features and --group-features", f);
+            }
+            if include_features.contains(f) {
+                bail!(
+                    "feature `{}` specified by both --exclude-features and --include-features",
+                    f
+                );
+            }
         }
-        if include_features.contains(f) {
-            bail!("feature `{}` specified by both --exclude-features and --include-features", f);
+
+        if subcommand.is_none() {
+            if cargo_args.iter().any(|a| a == "--list") {
+                cmd!(cargo, "--list").run()?;
+                std::process::exit(0);
+            } else if !remove_dev_deps {
+                // TODO: improve this
+                mini_usage("no subcommand or valid flag specified")?;
+            }
         }
-    }
 
-    if subcommand.is_none() {
-        if leading.contains(&"-h") {
-            println!("{}", Help::short());
-            std::process::exit(0);
-        } else if leading.contains(&"--help") {
-            println!("{}", Help::long());
-            std::process::exit(0);
-        } else if leading.iter().any(|&a| a == "--version" || a == "-V" || a == "-vV" || a == "-Vv")
-        {
-            print_version();
-            std::process::exit(0);
-        } else if leading.contains(&"--list") {
-            cmd!(cargo, "--list").run()?;
-            std::process::exit(0);
-        } else if !remove_dev_deps {
-            // TODO: improve this
-            mini_usage("no subcommand or valid flag specified")?;
+        if version_range.is_some() {
+            let rustup = Rustup::new();
+            if rustup.version < 23 {
+                bail!("--version-range requires rustup 1.23 or later");
+            }
         }
-    }
 
-    let rustup = Rustup::new();
+        if no_dev_deps {
+            info!(
+                "--no-dev-deps removes dev-dependencies from real `Cargo.toml` while cargo-hack is running and restores it when finished"
+            );
+        }
 
-    if rustup.version < 23 && version_range.is_some() {
-        bail!("--version-range requires rustup 1.23 or later");
-    }
+        // https://github.com/taiki-e/cargo-hack/issues/42
+        // https://github.com/rust-lang/cargo/pull/8799
+        let namespaced_features = has_z_flag(&cargo_args, "namespaced-features");
+        exclude_no_default_features |= !include_features.is_empty();
+        exclude_all_features |= !include_features.is_empty()
+            || !exclude_features.is_empty()
+            || (feature_powerset && !namespaced_features && depth.is_none());
+        exclude_features.extend_from_slice(&features);
+
+        term::verbose::set(verbose != 0);
+        // If `-vv` is passed, propagate `-v` to cargo.
+        if verbose > 1 {
+            cargo_args.push(format!("-{}", "v".repeat(verbose - 1)))
+        }
 
-    if no_dev_deps {
-        info!(
-            "--no-dev-deps removes dev-dependencies from real `Cargo.toml` while cargo-hack is running and restores it when finished"
-        );
+        Ok(Args {
+            leading_args: cargo_args,
+            trailing_args: rest,
+
+            subcommand,
+
+            manifest_path,
+            package,
+            exclude,
+            workspace,
+            each_feature,
+            feature_powerset,
+            no_dev_deps,
+            remove_dev_deps,
+            ignore_private,
+            ignore_unknown_features,
+            optional_deps,
+            clean_per_run,
+            clean_per_version,
+            keep_going,
+            include_features: include_features.into_iter().map(Into::into).collect(),
+            include_deps_features,
+            version_range,
+            version_step,
+
+            depth,
+            group_features,
+
+            exclude_features,
+            exclude_no_default_features,
+            exclude_all_features,
+
+            features,
+
+            no_default_features,
+            target,
+        })
     }
-
-    // https://github.com/taiki-e/cargo-hack/issues/42
-    // https://github.com/rust-lang/cargo/pull/8799
-    let namespaced_features = has_z_flag(&leading, "namespaced-features");
-    exclude_no_default_features |= !include_features.is_empty();
-    exclude_all_features |= !include_features.is_empty()
-        || !exclude_features.is_empty()
-        || (feature_powerset && !namespaced_features && depth.is_none());
-    exclude_features.extend_from_slice(&features);
-
-    term::set_verbose(verbose);
-    Ok(Args {
-        leading_args: leading,
-        trailing_args: iter.as_slice(),
-
-        subcommand,
-
-        manifest_path,
-        package,
-        exclude,
-        workspace: workspace.is_some(),
-        each_feature,
-        feature_powerset,
-        no_dev_deps,
-        remove_dev_deps,
-        ignore_private,
-        ignore_unknown_features,
-        optional_deps,
-        clean_per_run,
-        clean_per_version,
-        keep_going,
-        include_features: include_features.into_iter().map(Into::into).collect(),
-        include_deps_features,
-        version_range,
-        version_step,
-
-        depth,
-        group_features,
-
-        exclude_features,
-        exclude_no_default_features,
-        exclude_all_features,
-
-        features,
-
-        no_default_features,
-        target,
-    })
 }
 
-fn has_z_flag(args: &[&str], name: &str) -> bool {
-    let mut iter = args.iter().copied();
+fn has_z_flag(args: &[String], name: &str) -> bool {
+    let mut iter = args.iter().map(String::as_str);
     while let Some(mut arg) = iter.next() {
         if arg == "-Z" {
             arg = iter.next().unwrap();
@@ -544,38 +594,6 @@ fn has_z_flag(args: &[&str], name: &str) -> bool {
     false
 }
 
-fn parse_opt<'a>(
-    arg: &'a str,
-    args: &mut Peekable<impl Iterator<Item = &'a str>>,
-    subcommand: Option<&str>,
-    pat: &str,
-    require_value: bool,
-) -> Result<Option<Option<&'a str>>> {
-    if let Some(rem) = arg.strip_prefix(pat) {
-        if rem.is_empty() {
-            if require_value {
-                return Ok(Some(Some(args.next().ok_or_else(|| req_arg(pat, subcommand))?)));
-            }
-            if args.peek().map_or(true, |s| s.starts_with('-')) {
-                Ok(Some(None))
-            } else {
-                Ok(Some(args.next()))
-            }
-        } else if let Some(mut val) = rem.strip_prefix('=') {
-            if val.starts_with('\'') && val.ends_with('\'')
-                || val.starts_with('"') && val.ends_with('"')
-            {
-                val = &val[1..val.len() - 1];
-            }
-            Ok(Some(Some(val)))
-        } else {
-            Ok(None)
-        }
-    } else {
-        Ok(None)
-    }
-}
-
 // (short flag, long flag, value name, short descriptions, additional descriptions)
 type HelpText<'a> = (&'a str, &'a str, &'a str, &'a str, &'a [&'a str]);
 
@@ -809,19 +827,15 @@ Some common cargo commands are (see all commands with --list):
     }
 }
 
-fn print_version() {
-    println!("{0} {1}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
-}
-
 // Note: When adding a flag here, update the test with the same name in `tests/test.rs` file.
 
 fn removed_flags(flag: &str) -> Result<()> {
     let alt = match flag {
-        "--ignore-non-exist-features" => "--ignore-unknown-features",
-        "--skip-no-default-features" => "--exclude-no-default-features",
+        "ignore-non-exist-features" => "--ignore-unknown-features",
+        "skip-no-default-features" => "--exclude-no-default-features",
         _ => return Ok(()),
     };
-    bail!("{} was removed, use {} instead", flag, alt)
+    bail!("--{} was removed, use {} instead", flag, alt)
 }
 
 fn mini_usage(msg: &str) -> Result<()> {
@@ -841,24 +855,40 @@ fn get_help(flag: &str) -> Option<&HelpText<'_>> {
     HELP.iter().find(|&(s, l, ..)| *s == flag || *l == flag)
 }
 
-fn req_arg(flag: &str, subcommand: Option<&str>) -> Error {
-    let arg = get_help(flag).map_or_else(|| flag.to_string(), |arg| format!("{} {}", arg.1, arg.2));
-    format_err!(
-        "\
-The argument '{}' requires a value but none was supplied
+enum OwnedFlag {
+    Long(String),
+    Short(char),
+}
 
-USAGE:
-    cargo hack{} {}
+impl OwnedFlag {
+    fn as_arg(&self) -> lexopt::Arg<'_> {
+        match self {
+            Self::Long(flag) => Long(flag),
+            &Self::Short(flag) => Short(flag),
+        }
+    }
+}
 
-For more information try --help
-",
-        flag,
-        subcommand.map_or_else(String::new, |subcommand| String::from(" ") + subcommand),
-        arg,
-    )
+impl From<lexopt::Arg<'_>> for OwnedFlag {
+    fn from(arg: lexopt::Arg<'_>) -> Self {
+        match arg {
+            Long(flag) => Self::Long(flag.to_owned()),
+            Short(flag) => Self::Short(flag),
+            Value(_) => unreachable!(),
+        }
+    }
 }
 
-fn multi_arg(flag: &str, subcommand: Option<&str>) -> Result<()> {
+fn format_flag(flag: &lexopt::Arg<'_>) -> String {
+    match flag {
+        Long(flag) => format!("--{}", flag),
+        Short(flag) => format!("-{}", flag),
+        Value(_) => unreachable!(),
+    }
+}
+
+fn multi_arg(flag: &lexopt::Arg<'_>, subcommand: Option<&str>) -> Result<()> {
+    let flag = &format_flag(flag);
     let arg = get_help(flag).map_or_else(|| flag.to_string(), |arg| format!("{} {}", arg.1, arg.2));
     bail!(
         "\
@@ -876,11 +906,12 @@ For more information try --help
 }
 
 fn similar_arg(
-    arg: &str,
+    flag: &lexopt::Arg<'_>,
     subcommand: Option<&str>,
     expected: &str,
     value: Option<&str>,
 ) -> Result<()> {
+    let flag = &format_flag(flag);
     bail!(
         "\
 Found argument '{0}' which wasn't expected, or isn't valid in this context
@@ -891,7 +922,7 @@ USAGE:
 
 For more information try --help
 ",
-        arg,
+        flag,
         subcommand.map_or_else(String::new, |subcommand| String::from(" ") + subcommand),
         expected,
         value.unwrap_or_default()
@@ -937,20 +968,6 @@ mod tests {
     use super::Help;
     use crate::fs;
 
-    // See handle_args function for more.
-    #[cfg(unix)]
-    #[test]
-    fn non_utf8_arg() {
-        use std::{ffi::OsStr, os::unix::prelude::OsStrExt};
-        // `cargo hack -- $'fo\x80o'`
-        super::handle_args(&[
-            "hack".as_ref(),
-            "--".as_ref(),
-            OsStr::from_bytes(&[b'f', b'o', 0x80, b'o']),
-        ])
-        .unwrap_err();
-    }
-
     #[track_caller]
     fn assert_diff(expected_path: impl AsRef<Path>, actual: impl AsRef<str>) {
         let actual = actual.as_ref();
@@ -1022,7 +1039,7 @@ mod tests {
             }
         }
         if start && end {
-            fs::write(path, out)?;
+            assert_diff(path, out);
         } else if start {
             panic!("missing `<!-- readme-long-help:end -->` comment in README.md");
         } else {
diff --git a/src/context.rs b/src/context.rs
index ee21ad22..99f9f562 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -10,15 +10,15 @@ use std::{
 use anyhow::{bail, Result};
 
 use crate::{
-    cli::{self, Args},
+    cli::Args,
     features::Features,
     manifest::Manifest,
     metadata::{Metadata, Package, PackageId},
     restore, rustup, term, ProcessBuilder,
 };
 
-pub(crate) struct Context<'a> {
-    args: Args<'a>,
+pub(crate) struct Context {
+    args: Args,
     metadata: Metadata,
     manifests: HashMap<PackageId, Manifest>,
     pkg_features: HashMap<PackageId, Features>,
@@ -28,11 +28,11 @@ pub(crate) struct Context<'a> {
     pub(crate) version_range: Option<Vec<String>>,
 }
 
-impl<'a> Context<'a> {
-    pub(crate) fn new(args: &'a [String]) -> Result<Self> {
+impl Context {
+    pub(crate) fn new() -> Result<Self> {
         let cargo = env::var_os("CARGO_HACK_CARGO_SRC")
             .unwrap_or_else(|| env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")));
-        let args = cli::parse_args(args, &cargo)?;
+        let args = Args::parse(&cargo)?;
         assert!(
             args.subcommand.is_some() || args.remove_dev_deps,
             "no subcommand or valid flag specified"
@@ -48,7 +48,8 @@ impl<'a> Context<'a> {
 
         let version_range = args
             .version_range
-            .map(|range| rustup::version_range(range, args.version_step, &metadata))
+            .as_ref()
+            .map(|range| rustup::version_range(range, args.version_step.as_deref(), &metadata))
             .transpose()?;
 
         let mut pkg_features = HashMap::with_capacity(metadata.workspace_members.len());
@@ -142,8 +143,8 @@ impl<'a> Context<'a> {
     }
 }
 
-impl<'a> ops::Deref for Context<'a> {
-    type Target = Args<'a>;
+impl ops::Deref for Context {
+    type Target = Args;
 
     fn deref(&self) -> &Self::Target {
         &self.args
diff --git a/src/features.rs b/src/features.rs
index 8f744332..98ab0fab 100644
--- a/src/features.rs
+++ b/src/features.rs
@@ -123,6 +123,12 @@ impl PartialEq<str> for Feature {
     }
 }
 
+impl PartialEq<String> for Feature {
+    fn eq(&self, other: &String) -> bool {
+        self.name() == other
+    }
+}
+
 impl<S: Into<String>> From<S> for Feature {
     fn from(name: S) -> Self {
         Self::Normal { name: name.into() }
diff --git a/src/main.rs b/src/main.rs
index 2a9ddca2..a2b77f70 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -27,6 +27,7 @@ mod version;
 
 use std::{
     collections::BTreeMap,
+    env,
     fmt::{self, Write},
 };
 
@@ -41,19 +42,20 @@ fn main() {
     if let Err(e) = try_main() {
         error!("{:#}", e);
     }
-    if term::has_error() {
+    if term::error()
+        || term::warn() && env::var_os("CARGO_HACK_DENY_WARNINGS").filter(|v| v == "true").is_some()
+    {
         std::process::exit(1)
     }
 }
 
 fn try_main() -> Result<()> {
-    let args = &cli::raw()?;
-    let cx = &Context::new(args)?;
+    let cx = &Context::new()?;
 
     exec_on_workspace(cx)
 }
 
-fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
+fn exec_on_workspace(cx: &Context) -> Result<()> {
     // TODO: Ideally, we should do this, but for now, we allow it as cargo-hack
     // may mistakenly interpret the specified valid feature flag as unknown.
     // if cx.ignore_unknown_features && !cx.workspace && !cx.current_manifest().is_virtual() {
@@ -72,7 +74,7 @@ fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
             // First, generate the lockfile using the oldest cargo specified.
             // https://github.com/taiki-e/cargo-hack/issues/105
             let toolchain = &range[0];
-            rustup::install_toolchain(toolchain, cx.target, true)?;
+            rustup::install_toolchain(toolchain, cx.target.as_deref(), true)?;
             let mut line = line.clone();
             line.leading_arg(toolchain);
             line.arg("generate-lockfile");
@@ -91,7 +93,7 @@ fn exec_on_workspace(cx: &Context<'_>) -> Result<()> {
 
         range.iter().enumerate().try_for_each(|(i, toolchain)| {
             if i != 0 {
-                rustup::install_toolchain(toolchain, cx.target, true)?;
+                rustup::install_toolchain(toolchain, cx.target.as_deref(), true)?;
             }
 
             if cx.clean_per_version {
@@ -134,7 +136,7 @@ enum Kind<'a> {
     Powerset { features: Vec<Vec<&'a Feature>> },
 }
 
-fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progress) -> Kind<'a> {
+fn determine_kind<'a>(cx: &'a Context, id: &PackageId, progress: &mut Progress) -> Kind<'a> {
     if cx.ignore_private && cx.is_private(id) {
         info!("skipped running on private package `{}`", cx.name_verbose(id));
         return Kind::SkipAsPrivate;
@@ -149,7 +151,7 @@ fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progre
 
     let package = cx.packages(id);
     let filter = |&f: &&Feature| {
-        !cx.exclude_features.iter().any(|s| f == *s)
+        !cx.exclude_features.iter().any(|s| f == s)
             && !cx.group_features.iter().any(|g| g.matches(f.name()))
     };
     let features = if cx.include_features.is_empty() {
@@ -164,17 +166,21 @@ fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progre
         let mut features: Vec<_> = feature_list.normal().iter().filter(filter).collect();
 
         if let Some(opt_deps) = &cx.optional_deps {
-            for &d in opt_deps {
-                if !feature_list.optional_deps().iter().any(|f| f == d) {
-                    warn!(
-                        "specified optional dependency `{}` not found in package `{}`",
-                        d, package.name
-                    );
+            if opt_deps.len() == 1 && opt_deps[0].is_empty() {
+                // --optional-deps=
+            } else {
+                for d in opt_deps {
+                    if !feature_list.optional_deps().iter().any(|f| f == d) {
+                        warn!(
+                            "specified optional dependency `{}` not found in package `{}`",
+                            d, package.name
+                        );
+                    }
                 }
             }
 
             features.extend(feature_list.optional_deps().iter().filter(|f| {
-                filter(f) && (opt_deps.is_empty() || opt_deps.iter().any(|x| *f == *x))
+                filter(f) && (opt_deps.is_empty() || opt_deps.iter().any(|x| *f == x))
             }));
         }
 
@@ -224,7 +230,7 @@ fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progre
 }
 
 fn determine_package_list<'a>(
-    cx: &'a Context<'_>,
+    cx: &'a Context,
     progress: &mut Progress,
 ) -> Result<Vec<(&'a PackageId, Kind<'a>)>> {
     Ok(if cx.workspace {
@@ -239,20 +245,20 @@ fn determine_package_list<'a>(
         }
 
         cx.workspace_members()
-            .filter(|id| !cx.exclude.contains(&&*cx.packages(id).name))
+            .filter(|id| !cx.exclude.contains(&cx.packages(id).name))
             .map(|id| (id, determine_kind(cx, id, progress)))
             .collect()
     } else if !cx.package.is_empty() {
         if let Some(spec) = cx
             .package
             .iter()
-            .find(|&&spec| !cx.workspace_members().any(|id| cx.packages(id).name == spec))
+            .find(|&spec| !cx.workspace_members().any(|id| cx.packages(id).name == *spec))
         {
             bail!("package ID specification `{}` matched no packages", spec)
         }
 
         cx.workspace_members()
-            .filter(|id| cx.package.contains(&&*cx.packages(id).name))
+            .filter(|id| cx.package.contains(&cx.packages(id).name))
             .map(|id| (id, determine_kind(cx, id, progress)))
             .collect()
     } else if cx.current_package().is_none() {
@@ -267,7 +273,7 @@ fn determine_package_list<'a>(
 }
 
 fn exec_on_package(
-    cx: &Context<'_>,
+    cx: &Context,
     id: &PackageId,
     kind: &Kind<'_>,
     line: &ProcessBuilder<'_>,
@@ -301,7 +307,7 @@ fn exec_on_package(
 }
 
 fn exec_actual(
-    cx: &Context<'_>,
+    cx: &Context,
     id: &PackageId,
     kind: &Kind<'_>,
     line: &mut ProcessBuilder<'_>,
@@ -361,7 +367,7 @@ fn exec_actual(
 }
 
 fn exec_cargo_with_features(
-    cx: &Context<'_>,
+    cx: &Context,
     id: &PackageId,
     line: &ProcessBuilder<'_>,
     progress: &mut Progress,
@@ -395,7 +401,7 @@ impl fmt::Display for KeepGoing {
 }
 
 fn exec_cargo(
-    cx: &Context<'_>,
+    cx: &Context,
     id: &PackageId,
     line: &mut ProcessBuilder<'_>,
     progress: &mut Progress,
@@ -419,7 +425,7 @@ fn exec_cargo(
 }
 
 fn exec_cargo_inner(
-    cx: &Context<'_>,
+    cx: &Context,
     id: &PackageId,
     line: &mut ProcessBuilder<'_>,
     progress: &mut Progress,
@@ -446,7 +452,7 @@ fn exec_cargo_inner(
     line.run()
 }
 
-fn cargo_clean(cx: &Context<'_>, id: Option<&PackageId>) -> Result<()> {
+fn cargo_clean(cx: &Context, id: Option<&PackageId>) -> Result<()> {
     let mut line = cx.cargo();
     line.arg("clean");
     if let Some(id) = id {
diff --git a/src/metadata.rs b/src/metadata.rs
index 2c6b99b5..bbc8efa6 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -46,7 +46,7 @@ pub(crate) struct Metadata {
 }
 
 impl Metadata {
-    pub(crate) fn new(args: &Args<'_>, cargo: &OsStr, restore: &restore::Manager) -> Result<Self> {
+    pub(crate) fn new(args: &Args, cargo: &OsStr, restore: &restore::Manager) -> Result<Self> {
         // If failed to determine cargo version, assign 0 to skip all version-dependent decisions.
         let mut cargo_version = cargo::minor_version(cmd!(cargo))
             .map_err(|e| warn!("unable to determine cargo version: {:#}", e))
@@ -72,7 +72,7 @@ impl Metadata {
                 }
                 cmd.run_with_output()?;
             }
-            let guard = term::scoped_verbose(false);
+            let guard = term::verbose::scoped(false);
             let mut handle = restore.set(&fs::read_to_string(&lockfile)?, lockfile);
 
             // Try with stable cargo because if workspace member has
diff --git a/src/process.rs b/src/process.rs
index 6bba4428..bcb417e0 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -32,7 +32,7 @@ pub(crate) struct ProcessBuilder<'a> {
     /// The program to execute.
     program: Rc<OsStr>,
     /// A list of arguments to pass to the program (until '--').
-    propagated_leading_args: &'a [&'a str],
+    propagated_leading_args: &'a [String],
     /// A list of arguments to pass to the program (after '--').
     trailing_args: &'a [String],
 
@@ -80,9 +80,9 @@ impl<'a> ProcessBuilder<'a> {
         self
     }
 
-    pub(crate) fn apply_context(&mut self, cx: &'a Context<'_>) -> &mut Self {
+    pub(crate) fn apply_context(&mut self, cx: &'a Context) -> &mut Self {
         self.propagated_leading_args = &cx.leading_args;
-        self.trailing_args = cx.trailing_args;
+        self.trailing_args = &cx.trailing_args;
         self
     }
 
@@ -93,9 +93,9 @@ impl<'a> ProcessBuilder<'a> {
         }
     }
 
-    pub(crate) fn append_features_from_args(&mut self, cx: &Context<'_>, id: &PackageId) {
+    pub(crate) fn append_features_from_args(&mut self, cx: &Context, id: &PackageId) {
         if cx.ignore_unknown_features {
-            self.append_features(cx.features.iter().filter(|&&f| {
+            self.append_features(cx.features.iter().filter(|&f| {
                 if cx.pkg_features(id).contains(f) {
                     true
                 } else {
diff --git a/src/term.rs b/src/term.rs
index 94228107..3c9ff801 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -51,7 +51,6 @@ pub(crate) fn set_coloring(color: Option<&str>) -> Result<()> {
     if coloring == Coloring::Auto && !atty::is(atty::Stream::Stderr) {
         coloring = Coloring::Never;
     }
-    // Relaxed is fine because only the argument parsing step updates this value.
     COLORING.store(coloring as _, Ordering::Relaxed);
     Ok(())
 }
@@ -64,36 +63,35 @@ fn coloring() -> ColorChoice {
     }
 }
 
-static HAS_ERROR: AtomicBool = AtomicBool::new(false);
-pub(crate) fn set_error() {
-    HAS_ERROR.store(true, Ordering::SeqCst)
-}
-pub(crate) fn has_error() -> bool {
-    HAS_ERROR.load(Ordering::SeqCst)
-}
-
-static VERBOSE: AtomicBool = AtomicBool::new(false);
-pub(crate) struct VerboseGuard {
-    prev: bool,
-}
-impl Drop for VerboseGuard {
-    fn drop(&mut self) {
-        set_verbose(self.prev);
-    }
-}
-pub(crate) fn set_verbose(verbose: bool) {
-    // TODO: CARGO_TERM_VERBOSE
-    // https://doc.rust-lang.org/nightly/cargo/reference/config.html#termverbose
-    VERBOSE.store(verbose, Ordering::Relaxed)
-}
-pub(crate) fn scoped_verbose(verbose: bool) -> VerboseGuard {
-    // TODO: CARGO_TERM_VERBOSE
-    // https://doc.rust-lang.org/nightly/cargo/reference/config.html#termverbose
-    VerboseGuard { prev: VERBOSE.swap(verbose, Ordering::Relaxed) }
-}
-pub(crate) fn verbose() -> bool {
-    VERBOSE.load(Ordering::Relaxed)
+macro_rules! global_flag {
+    ($name:ident: $value:ty = $ty:ident::new($($default:expr)?)) => {
+        pub(crate) mod $name {
+            use super::*;
+            pub(super) static VALUE: $ty = $ty::new($($default)?);
+            pub(crate) fn set(value: $value) {
+                VALUE.store(value, Ordering::Relaxed);
+            }
+            pub(crate) struct Guard {
+                prev: $value,
+            }
+            impl Drop for Guard {
+                fn drop(&mut self) {
+                    set(self.prev);
+                }
+            }
+            #[allow(dead_code)]
+            pub(crate) fn scoped(value: $value) -> Guard {
+                Guard { prev: VALUE.swap(value, Ordering::Relaxed) }
+            }
+        }
+        pub(crate) fn $name() -> $value {
+            $name::VALUE.load(Ordering::Relaxed)
+        }
+    };
 }
+global_flag!(verbose: bool = AtomicBool::new(false));
+global_flag!(error: bool = AtomicBool::new(false));
+global_flag!(warn: bool = AtomicBool::new(false));
 
 pub(crate) fn print_status(status: &str, color: Option<Color>) -> StandardStream {
     let mut stream = StandardStream::stderr(coloring());
@@ -109,7 +107,7 @@ pub(crate) fn print_status(status: &str, color: Option<Color>) -> StandardStream
 macro_rules! error {
     ($($msg:expr),* $(,)?) => {{
         use std::io::Write;
-        crate::term::set_error();
+        crate::term::error::set(true);
         let mut stream = crate::term::print_status("error", Some(termcolor::Color::Red));
         let _ = writeln!(stream, $($msg),*);
     }};
@@ -118,6 +116,7 @@ macro_rules! error {
 macro_rules! warn {
     ($($msg:expr),* $(,)?) => {{
         use std::io::Write;
+        crate::term::warn::set(true);
         let mut stream = crate::term::print_status("warning", Some(termcolor::Color::Yellow));
         let _ = writeln!(stream, $($msg),*);
     }};


Test patch:
diff --git a/tests/auxiliary/mod.rs b/tests/auxiliary/mod.rs
index 132417fb..d3fb8a36 100644
--- a/tests/auxiliary/mod.rs
+++ b/tests/auxiliary/mod.rs
@@ -1,4 +1,3 @@
-pub use std::path::MAIN_SEPARATOR as SEPARATOR;
 use std::{
     env,
     ffi::OsStr,
@@ -18,6 +17,7 @@ static FIXTURES_PATH: Lazy<PathBuf> =
 
 pub fn cargo_bin_exe() -> Command {
     let mut cmd = Command::new(env!("CARGO_BIN_EXE_cargo-hack"));
+    cmd.env("CARGO_HACK_DENY_WARNINGS", "true");
     cmd.env_remove("RUSTFLAGS");
     cmd.env_remove("CARGO_TERM_COLOR");
     cmd
diff --git a/tests/fixtures/optional_deps/member2/src/lib.rs b/tests/fixtures/optional_deps/member2/src/lib.rs
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/fixtures/optional_deps/member3/src/lib.rs b/tests/fixtures/optional_deps/member3/src/lib.rs
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/fixtures/optional_deps/real/src/lib.rs b/tests/fixtures/optional_deps/real/src/lib.rs
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test.rs b/tests/test.rs
index eb7ffa02..a9c67fda 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -2,12 +2,13 @@
 
 mod auxiliary;
 
-use std::env;
-
-use auxiliary::{
-    cargo_bin_exe, cargo_hack, has_stable_toolchain, target_triple, CommandExt, SEPARATOR,
+use std::{
+    env::{self, consts::EXE_SUFFIX},
+    path::MAIN_SEPARATOR,
 };
 
+use auxiliary::{cargo_bin_exe, cargo_hack, has_stable_toolchain, target_triple, CommandExt};
+
 #[test]
 fn failures() {
     cargo_bin_exe().assert_failure("real");
@@ -233,7 +234,7 @@ fn exclude() {
         .stderr_contains("running `cargo check` on member2");
 
     // not_found is warning
-    cargo_hack(["check", "--all", "--exclude", "foo"]).assert_success("virtual").stderr_contains(
+    cargo_hack(["check", "--all", "--exclude", "foo"]).assert_failure("virtual").stderr_contains(
         "
         excluded package(s) `foo` not found in workspace
         running `cargo check` on member1
@@ -802,7 +803,7 @@ fn exclude_features_failure() {
         .stderr_contains("feature `a` specified by both --exclude-features and --include-features");
 
     cargo_hack(["check", "--each-feature", "--exclude-features=z"])
-        .assert_success("real") // warn
+        .assert_failure("real") // warn
         .stderr_contains("specified feature `z` not found in package `real`");
 }
 
@@ -1185,17 +1186,52 @@ fn list_separator() {
         .stderr_contains("running `cargo run --features real,renemed` on optional_deps");
 }
 
+#[test]
+fn short_flag() {
+    cargo_hack(["check", "-vvpmember1"]) // same as -v -v -p member1
+        .assert_success("virtual")
+        .stderr_contains(format!(
+            "
+            cargo{1} check -v --manifest-path member1{0}Cargo.toml` (1/1)
+            ",
+            MAIN_SEPARATOR, EXE_SUFFIX
+        ))
+        .stderr_not_contains("member2");
+
+    cargo_hack(["check", "-qpmember1"]) // same as -q -p member1
+        .assert_success("virtual")
+        .stderr_contains("`cargo check -q` on member1 (1/1)")
+        .stderr_not_contains("member2");
+}
+
 #[test]
 fn verbose() {
     cargo_hack(["check", "--verbose"]).assert_success("virtual").stderr_contains(format!(
         "
-        cargo{1} check --manifest-path member1{0}Cargo.toml`
-        cargo{1} check --manifest-path member2{0}Cargo.toml`
-        cargo{1} check --manifest-path dir{0}not_find_manifest{0}Cargo.toml`
+        cargo{1} check --manifest-path member1{0}Cargo.toml` (1/3)
+        cargo{1} check --manifest-path member2{0}Cargo.toml` (2/3)
+        cargo{1} check --manifest-path dir{0}not_find_manifest{0}Cargo.toml` (3/3)
         ",
-        SEPARATOR,
-        env::consts::EXE_SUFFIX
+        MAIN_SEPARATOR, EXE_SUFFIX
     ));
+
+    // If `-vv` is passed, propagate `-v` to cargo.
+    cargo_hack(["check", "-vv", "-p", "member1"]).assert_success("virtual").stderr_contains(
+        format!(
+            "
+            cargo{1} check -v --manifest-path member1{0}Cargo.toml` (1/1)
+            ",
+            MAIN_SEPARATOR, EXE_SUFFIX
+        ),
+    );
+    cargo_hack(["check", "-vvv", "-p", "member1"]).assert_success("virtual").stderr_contains(
+        format!(
+            "
+            cargo{1} check -vv --manifest-path member1{0}Cargo.toml` (1/1)
+            ",
+            MAIN_SEPARATOR, EXE_SUFFIX
+        ),
+    );
 }
 
 #[test]
@@ -1314,7 +1350,7 @@ fn version_range_failure() {
 
     // patch version
     cargo_hack(["check", "--version-range", "1.45.2.."])
-        .assert_success("real") // warn
+        .assert_failure("real") // warn
         .stderr_contains(
             "
             --version-range always selects the latest patch release per minor release, \
@@ -1351,6 +1387,6 @@ fn keep_going() {
             cargo{0} check --manifest-path Cargo.toml --no-default-features`
             cargo{0} check --manifest-path Cargo.toml --no-default-features --features a`
             ",
-            env::consts::EXE_SUFFIX
+            EXE_SUFFIX
         ));
 }