use std::{
env,
ffi::{OsStr, OsString},
fmt, mem,
};
use anyhow::{bail, format_err, Result};
use lexopt::{
Arg::{Long, Short, Value},
ValueExt,
};
use crate::{term, Feature, Rustup};
pub(crate) struct Args {
pub(crate) leading_args: Vec<String>,
pub(crate) trailing_args: Vec<String>,
pub(crate) subcommand: Option<String>,
pub(crate) manifest_path: Option<String>,
pub(crate) package: Vec<String>,
pub(crate) exclude: Vec<String>,
pub(crate) workspace: bool,
pub(crate) each_feature: bool,
pub(crate) feature_powerset: bool,
pub(crate) no_dev_deps: bool,
pub(crate) remove_dev_deps: bool,
pub(crate) ignore_private: bool,
pub(crate) ignore_unknown_features: bool,
pub(crate) clean_per_run: bool,
pub(crate) clean_per_version: bool,
pub(crate) keep_going: bool,
pub(crate) version_range: Option<String>,
pub(crate) version_step: Option<String>,
pub(crate) optional_deps: Option<Vec<String>>,
pub(crate) include_features: Vec<Feature>,
pub(crate) include_deps_features: bool,
pub(crate) exclude_features: Vec<String>,
pub(crate) exclude_no_default_features: bool,
pub(crate) exclude_all_features: bool,
pub(crate) depth: Option<usize>,
pub(crate) group_features: Vec<Feature>,
pub(crate) features: Vec<String>,
pub(crate) no_default_features: bool,
pub(crate) target: Option<String>,
}
impl Args {
pub(crate) fn parse(cargo: &OsStr) -> Result<Self> {
const SUBCMD: &str = "hack";
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 raw_args = handle_args(env::args_os());
raw_args.next(); 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;
}
args.push(arg);
}
let rest = raw_args.collect::<Result<Vec<_>>>()?;
let mut cargo_args = vec![];
let mut subcommand: Option<String> = None;
let mut manifest_path = None;
let mut color = None;
let mut package = vec![];
let mut exclude = vec![];
let mut features = vec![];
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![];
let mut include_deps_features = false;
let mut exclude_features = vec![];
let mut exclude_no_default_features = false;
let mut exclude_all_features = false;
let mut group_features: Vec<String> = vec![];
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 $(,)?) => {{
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 $(,)?) => {{
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 $(,)?) => {
if mem::replace(&mut $flag, true) {
multi_arg(&arg, subcommand.as_deref())?;
}
};
}
match arg {
Long("color") => parse_opt!(color, true),
Long("target") => parse_opt!(target, true),
Long("manifest-path") => parse_opt!(manifest_path, false),
Long("depth") => parse_opt!(depth, false),
Long("version-range") => parse_opt!(version_range, false),
Long("version-step") => parse_opt!(version_step, false),
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()?),
Short('F') | Long("features") => parse_multi_opt!(features),
Long("skip" | "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(',').map(str::to_owned));
} else {
optional_deps.extend(val.split(' ').map(str::to_owned));
}
}
Long("workspace" | "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,
Long("each-features") => {
similar_arg(&arg, subcommand.as_deref(), "--each-feature", None)?;
}
Long("features-powerset") => {
similar_arg(&arg, subcommand.as_deref(), "--feature-powerset", None)?;
}
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());
}
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);
}
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') {
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);
}
}
}
term::set_coloring(color.as_deref())?;
if !exclude.is_empty() && !workspace {
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() {
let _guard = term::warn::scoped(false);
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);
warn!(
"--ignore-unknown-features for --group-features is not fully implemented and may not work as intended"
);
}
}
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 !feature_powerset {
if depth.is_some() {
requires("--depth", &["--feature-powerset"])?;
} else if !group_features.is_empty() {
requires("--group-features", &["--feature-powerset"])?;
}
}
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.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!(
"--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.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
);
}
}
"install" => {
bail!("cargo-hack may not be used together with {} subcommand", subcommand)
}
_ => {}
}
}
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 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_default_features {
if each_feature {
conflicts("--no-default-features", "--each-feature")?;
} else if feature_powerset {
conflicts("--no-default-features", "--feature-powerset")?;
}
}
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 subcommand.is_none() {
if cargo_args.iter().any(|a| a == "--list") {
cmd!(cargo, "--list").run()?;
std::process::exit(0);
} else if !remove_dev_deps {
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");
}
}
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"
);
}
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 verbose > 1 {
cargo_args.push(format!("-{}", "v".repeat(verbose - 1)));
}
Ok(Self {
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,
})
}
}
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();
} else if let Some(a) = arg.strip_prefix("-Z") {
arg = a;
} else {
continue;
}
if let Some(rest) = arg.strip_prefix(name) {
if rest.is_empty() || rest.starts_with('=') {
return true;
}
}
}
false
}
type HelpText<'a> = (&'a str, &'a str, &'a str, &'a str, &'a [&'a str]);
const HELP: &[HelpText<'_>] = &[
("-p", "--package", "<SPEC>...", "Package(s) to check", &[]),
("", "--all", "", "Alias for --workspace", &[]),
("", "--workspace", "", "Perform command for all packages in the workspace", &[]),
("", "--exclude", "<SPEC>...", "Exclude packages from the check", &[
"This flag can only be used together with --workspace",
]),
("", "--manifest-path", "<PATH>", "Path to Cargo.toml", &[]),
("-F", "--features", "<FEATURES>...", "Space-separated list of features to activate", &[]),
("", "--each-feature", "", "Perform for each feature of the package", &[
"This also includes runs with just --no-default-features flag, and default features.",
"When this flag is not used together with --exclude-features (--skip) and \
--include-features and there are multiple features, this also includes runs with \
just --all-features flag."
]),
("", "--feature-powerset", "", "Perform for the feature powerset of the package", &[
"This also includes runs with just --no-default-features flag, and default features.",
"When this flag is used together with --depth or namespaced features \
(-Z namespaced-features) and not used together with --exclude-features (--skip) and \
--include-features and there are multiple features, this also includes runs with just \
--all-features flag."
]),
("", "--optional-deps", "[DEPS]...", "Use optional dependencies as features", &[
"If DEPS are not specified, all optional dependencies are considered as features.",
"This flag can only be used together with either --each-feature flag or --feature-powerset \
flag.",
]),
("", "--skip", "<FEATURES>...", "Alias for --exclude-features", &[]),
("", "--exclude-features", "<FEATURES>...", "Space-separated list of features to exclude", &[
"To exclude run of default feature, using value `--exclude-features default`.",
"To exclude run of just --no-default-features flag, using --exclude-no-default-features \
flag.",
"To exclude run of just --all-features flag, using --exclude-all-features flag.",
"This flag can only be used together with either --each-feature flag or --feature-powerset \
flag.",
]),
("", "--exclude-no-default-features", "", "Exclude run of just --no-default-features flag", &[
"This flag can only be used together with either --each-feature flag or --feature-powerset \
flag.",
]),
("", "--exclude-all-features", "", "Exclude run of just --all-features flag", &[
"This flag can only be used together with either --each-feature flag or --feature-powerset \
flag.",
]),
(
"",
"--depth",
"<NUM>",
"Specify a max number of simultaneous feature flags of --feature-powerset",
&[
"If NUM is set to 1, --feature-powerset is equivalent to --each-feature.",
"This flag can only be used together with --feature-powerset flag.",
],
),
("", "--group-features", "<FEATURES>...", "Space-separated list of features to group", &[
"To specify multiple groups, use this option multiple times: `--group-features a,b \
--group-features c,d`",
"This flag can only be used together with --feature-powerset flag.",
]),
(
"",
"--include-features",
"<FEATURES>...",
"Include only the specified features in the feature combinations instead of package \
features",
&[
"This flag can only be used together with either --each-feature flag or \
--feature-powerset flag.",
],
),
("", "--no-dev-deps", "", "Perform without dev-dependencies", &[
"Note that this flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is \
running and restores it when finished.",
]),
(
"",
"--remove-dev-deps",
"",
"Equivalent to --no-dev-deps flag except for does not restore the original `Cargo.toml` \
after performed",
&[],
),
("", "--ignore-private", "", "Skip to perform on `publish = false` packages", &[]),
(
"",
"--ignore-unknown-features",
"",
"Skip passing --features flag to `cargo` if that feature does not exist in the package",
&["This flag can only be used together with either --features or --include-features."],
),
(
"",
"--version-range",
"<START>..[END]",
"Perform commands on a specified (inclusive) range of Rust versions",
&[
"If the given range is unclosed, the latest stable compiler is treated as the upper \
bound.",
"Note that ranges are always inclusive ranges.",
],
),
(
"",
"--version-step",
"<NUM>",
"Specify the version interval of --version-range (default to `1`)",
&["This flag can only be used together with --version-range flag."],
),
("", "--clean-per-run", "", "Remove artifacts for that package before running the command", &[
"If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts will \
be removed before each run.",
"Note that dependencies artifacts will be preserved.",
]),
("", "--clean-per-version", "", "Remove artifacts per Rust version", &[
"Note that dependencies artifacts will also be removed.",
"This flag can only be used together with --version-range flag.",
]),
("", "--keep-going", "", "Keep going on failure", &[]),
("-v", "--verbose", "", "Use verbose output", &[]),
("", "--color", "<WHEN>", "Coloring: auto, always, never", &[
"This flag will be propagated to cargo.",
]),
("-h", "--help", "", "Prints help information", &[]),
("-V", "--version", "", "Prints version information", &[]),
];
struct Help {
long: bool,
term_size: usize,
print_version: bool,
}
const MAX_TERM_WIDTH: usize = 100;
impl Help {
fn long() -> Self {
Self { long: true, term_size: MAX_TERM_WIDTH, print_version: true }
}
fn short() -> Self {
Self { long: false, term_size: MAX_TERM_WIDTH, print_version: true }
}
}
impl fmt::Display for Help {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn write(
f: &mut fmt::Formatter<'_>,
indent: usize,
require_first_indent: bool,
term_size: usize,
desc: &str,
) -> fmt::Result {
if require_first_indent {
(0..indent).try_for_each(|_| write!(f, " "))?;
}
let mut written = 0;
let size = term_size - indent;
for s in desc.split(' ') {
if written + s.len() + 1 >= size {
writeln!(f)?;
(0..indent).try_for_each(|_| write!(f, " "))?;
write!(f, "{}", s)?;
written = s.len();
} else if written == 0 {
write!(f, "{}", s)?;
written += s.len();
} else {
write!(f, " {}", s)?;
written += s.len() + 1;
}
}
Ok(())
}
writeln!(
f,
"\
{0}{1}\n{2}
USAGE:
cargo hack [OPTIONS] [SUBCOMMAND]\n
Use -h for short descriptions and --help for more details.\n
OPTIONS:",
env!("CARGO_PKG_NAME"),
if self.print_version { concat!(" ", env!("CARGO_PKG_VERSION")) } else { "" },
env!("CARGO_PKG_DESCRIPTION")
)?;
for &(short, long, value_name, desc, additional) in HELP {
write!(f, " {:2}{} ", short, if short.is_empty() { " " } else { "," })?;
if self.long {
if value_name.is_empty() {
writeln!(f, "{}", long)?;
} else {
writeln!(f, "{} {}", long, value_name)?;
}
write(f, 12, true, self.term_size, desc)?;
writeln!(f, ".\n")?;
for desc in additional {
write(f, 12, true, self.term_size, desc)?;
writeln!(f, "\n")?;
}
} else {
if value_name.is_empty() {
write!(f, "{:32} ", long)?;
} else {
let long = format!("{} {}", long, value_name);
write!(f, "{:32} ", long)?;
}
write(f, 41, false, self.term_size, desc)?;
writeln!(f)?;
}
}
if !self.long {
writeln!(f)?;
}
writeln!(
f,
"\
Some common cargo commands are (see all commands with --list):
build Compile the current package
check Analyze the current package and report errors, but don't build object files
run Run a binary or example of the local package
test Run the tests"
)
}
}
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",
_ => return Ok(()),
};
bail!("--{} was removed, use {} instead", flag, alt)
}
fn mini_usage(msg: &str) -> Result<()> {
bail!(
"\
{}
USAGE:
cargo hack [OPTIONS] [SUBCOMMAND]
For more information try --help",
msg,
)
}
fn get_help(flag: &str) -> Option<&HelpText<'_>> {
HELP.iter().find(|&(s, l, ..)| *s == flag || *l == flag)
}
enum OwnedFlag {
Long(String),
Short(char),
}
impl OwnedFlag {
fn as_arg(&self) -> lexopt::Arg<'_> {
match self {
Self::Long(flag) => Long(flag),
&Self::Short(flag) => Short(flag),
}
}
}
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 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!(
"\
The argument '{}' was provided more than once, but cannot be used multiple times
USAGE:
cargo hack{} {}
For more information try --help
",
flag,
subcommand.map_or_else(String::new, |subcommand| String::from(" ") + subcommand),
arg,
)
}
fn similar_arg(
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
Did you mean {2}?
USAGE:
cargo{1} {2} {3}
For more information try --help
",
flag,
subcommand.map_or_else(String::new, |subcommand| String::from(" ") + subcommand),
expected,
value.unwrap_or_default()
)
}
fn requires(flag: &str, requires: &[&str]) -> Result<()> {
let with = match requires.len() {
0 => unreachable!(),
1 => requires[0].to_string(),
2 => format!("either {} or {}", requires[0], requires[1]),
_ => {
let mut with = String::new();
for f in requires.iter().take(requires.len() - 1) {
with += f;
with += ", ";
}
with += "or ";
with += requires.last().unwrap();
with
}
};
bail!("{} can only be used together with {}", flag, with);
}
fn conflicts(a: &str, b: &str) -> Result<()> {
bail!("{} may not be used together with {}", a, b);
}
#[cfg(test)]
mod tests {
use std::{
env,
io::Write,
panic,
path::Path,
process::{Command, Stdio},
};
use anyhow::Result;
use super::Help;
use crate::fs;
#[track_caller]
fn assert_diff(expected_path: impl AsRef<Path>, actual: impl AsRef<str>) {
let actual = actual.as_ref();
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let manifest_dir =
manifest_dir.strip_prefix(env::current_dir().unwrap()).unwrap_or(manifest_dir);
let expected_path = &manifest_dir.join(expected_path);
if !expected_path.is_file() {
fs::write(expected_path, "").unwrap();
}
let expected = fs::read_to_string(expected_path).unwrap();
if expected != actual {
if env::var_os("CI").is_some() {
let mut child = Command::new("git")
.args(["--no-pager", "diff", "--no-index", "--"])
.arg(expected_path)
.arg("-")
.stdin(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(actual.as_bytes()).unwrap();
assert!(!child.wait().unwrap().success());
panic!("assertion failed; please run test locally and commit resulting changes, or apply above diff as patch");
} else {
fs::write(expected_path, actual).unwrap();
}
}
}
#[test]
fn long_help() {
let actual = Help { print_version: false, ..Help::long() }.to_string();
assert_diff("tests/long-help.txt", actual);
}
#[test]
fn short_help() {
let actual = Help { print_version: false, ..Help::short() }.to_string();
assert_diff("tests/short-help.txt", actual);
}
#[test]
fn update_readme() -> Result<()> {
let new = Help { print_version: false, ..Help::long() }.to_string();
let path = &Path::new(env!("CARGO_MANIFEST_DIR")).join("README.md");
let base = fs::read_to_string(path)?;
let mut out = String::with_capacity(base.capacity());
let mut lines = base.lines();
let mut start = false;
let mut end = false;
while let Some(line) = lines.next() {
out.push_str(line);
out.push('\n');
if line == "<!-- readme-long-help:start -->" {
start = true;
out.push_str("```console\n");
out.push_str("$ cargo hack --help\n");
out.push_str(&new);
for line in &mut lines {
if line == "<!-- readme-long-help:end -->" {
out.push_str("```\n");
out.push_str(line);
out.push('\n');
end = true;
break;
}
}
}
}
if start && end {
assert_diff(path, out);
} else if start {
panic!("missing `<!-- readme-long-help:end -->` comment in README.md");
} else {
panic!("missing `<!-- readme-long-help:start -->` comment in README.md");
}
Ok(())
}
}