blob: 7391bb84dcab9fff6d99ae611d6e627d42d05c16 [file] [log] [blame]
use std::ffi::OsString;
use clap::{error::Result, ArgAction, Parser, Subcommand, ValueEnum};
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Default, strum::Display)]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum Format {
#[default]
List,
Json,
}
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Default, strum::Display)]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum Color {
#[default]
Auto,
Never,
Always,
}
#[derive(Parser, Debug)]
#[clap(bin_name = "cargo")]
struct Cargo {
#[command(subcommand)]
command: CargoCommand,
}
#[derive(Subcommand, Debug)]
enum CargoCommand {
Outdated(Options),
}
/// Options from CLI arguments
#[derive(Parser, Debug, PartialEq, Default)]
#[command(version)]
#[command(about = "Displays information about project dependency versions")]
pub struct Options {
/// Output formatting
#[arg(long, value_enum, ignore_case = true, default_value_t = Default::default())]
pub format: Format,
/// Output coloring
#[arg(long, value_enum, ignore_case = true, default_value_t = Default::default())]
pub color: Color,
/// Space-separated list of features
#[arg(long, use_value_delimiter = true)]
pub features: Vec<String>,
/// Dependencies to not print in the output (comma separated or one per '--ignore' argument)
#[arg(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)
#[arg(
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)
#[arg(short, long, value_name = "PATH")]
pub manifest_path: Option<String>,
/// Suppresses warnings
#[arg(short, long)]
pub quiet: bool,
/// Use verbose output
#[arg(short, long, action = ArgAction::Count)]
pub verbose: u64,
/// The exit code to return on new versions found
#[arg(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)
#[arg(short, long, value_name = "PKGS", use_value_delimiter = true)]
pub packages: Vec<String>,
/// Package to treat as the root package
#[arg(short, long)]
pub root: Option<String>,
/// How deep in the dependency chain to search (Defaults to all dependencies)
#[arg(short, long, value_name = "NUM")]
pub depth: Option<i32>,
/// Only check root dependencies (Equivalent to --depth=1)
#[arg(short = 'R', long)]
pub root_deps_only: bool,
/// Checks updates for all workspace members rather than only the root package
#[arg(short, long)]
pub workspace: bool,
/// Ignores channels for latest updates
#[arg(short, long)]
pub aggressive: bool,
/// Ignore relative dependencies external to workspace and check root dependencies only
#[arg(short = 'e', long = "ignore-external-rel")]
pub workspace_only: bool,
/// Run without accessing the network (useful for testing w/ local registries)
#[arg(short, long)]
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 }
}
pub fn parse() -> Options {
match try_parse_from(std::env::args_os()) {
Ok(opts) => opts,
Err(clap_err) => clap_err.exit(),
}
}
fn split_elem_by_ascii_whitespace(slice: &[String]) -> Vec<String> {
slice
.iter()
.flat_map(|x| x.split_ascii_whitespace())
.map(ToOwned::to_owned)
.collect()
}
fn try_parse_from(args: impl IntoIterator<Item = impl Into<OsString> + Clone>) -> Result<Options> {
let CargoCommand::Outdated(mut opts) = Cargo::try_parse_from(args)?.command;
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::*;
use pretty_assertions::assert_eq;
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);
try_parse_from(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 workspace_only() {
let opts = options(&["--ignore-external-rel"]);
assert_eq!(
Options {
workspace_only: true,
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,
);
}
#[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,
);
}
#[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,
);
}
#[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,
);
}
#[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,
);
}
#[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,
);
}
}