diff --git a/Cargo.lock b/Cargo.lock index 972dd9370c..014ffff099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,12 +91,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - [[package]] name = "bitflags" version = "1.3.2" @@ -175,44 +169,47 @@ dependencies = [ "atty", "bitflags 1.3.2", "strsim 0.8.0", - "textwrap 0.11.0", + "textwrap", "unicode-width", "vec_map", ] [[package]] name = "clap" -version = "3.2.25" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" dependencies = [ - "atty", - "bitflags 1.3.2", + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +dependencies = [ + "anstream", + "anstyle", "clap_lex", - "indexmap", - "strsim 0.10.0", - "termcolor", + "strsim 0.11.0", "terminal_size", - "textwrap 0.16.1", ] [[package]] name = "clap_complete" -version = "3.2.5" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8" +checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" dependencies = [ - "clap 3.2.25", + "clap 4.5.1", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" @@ -438,12 +435,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "heck" version = "0.3.3" @@ -489,27 +480,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.5", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "itoa" version = "1.0.10" @@ -524,7 +494,7 @@ dependencies = [ "atty", "blake3", "camino", - "clap 3.2.25", + "clap 4.5.1", "clap_complete", "cradle", "ctrlc", @@ -593,12 +563,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -659,12 +623,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "pretty_assertions" version = "1.4.0" @@ -822,20 +780,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.31" @@ -845,7 +789,7 @@ dependencies = [ "bitflags 2.4.2", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys", "windows-sys 0.52.0", ] @@ -948,9 +892,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "structopt" @@ -1034,7 +978,7 @@ checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "rustix 0.38.31", + "rustix", "windows-sys 0.52.0", ] @@ -1047,22 +991,13 @@ dependencies = [ "tempfile", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.37.27", + "rustix", "windows-sys 0.48.0", ] @@ -1075,15 +1010,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" -dependencies = [ - "terminal_size", -] - [[package]] name = "thiserror" version = "1.0.57" @@ -1192,7 +1118,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.31", + "rustix", "windows-sys 0.52.0", ] @@ -1212,15 +1138,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index a7eb2e068d..f3afd51967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ ansi_term = "0.12.0" atty = "0.2.0" blake3 = { version = "1.5.0", features = ["rayon", "mmap"] } camino = "1.0.4" -clap = { version = "3.0.0", features = ["wrap_help"] } -clap_complete = "3.0.0" +clap = { version = "4.0.0", features = ["color", "wrap_help"] } +clap_complete = "4.0.0" ctrlc = { version = "3.1.1", features = ["termination"] } derivative = "2.0.0" dirs = "5.0.1" diff --git a/src/config.rs b/src/config.rs index eb7329ecd4..ec4965282b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,9 @@ use { super::*, - clap::{App, AppSettings, Arg, ArgAction, ArgGroup, ArgMatches, ArgSettings}, + clap::{ + builder::{styling::AnsiColor, PossibleValuesParser, Styles}, + value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, + }, }; pub(crate) const CHOOSER_ENVIRONMENT_KEY: &str = "JUST_CHOOSER"; @@ -136,44 +139,48 @@ mod arg { } impl Config { - pub(crate) fn app() -> App<'static> { - let app = App::new(env!("CARGO_PKG_NAME")) + pub(crate) fn app() -> Command { + let app = Command::new(env!("CARGO_PKG_NAME")) .bin_name(env!("CARGO_PKG_NAME")) - .help_message("Print help information") - .version_message("Print version information") - .setting(AppSettings::ColoredHelp) - .setting(AppSettings::TrailingVarArg) + .trailing_var_arg(true) + .styles( + Styles::styled() + .header(AnsiColor::Yellow.on_default()) + .usage(AnsiColor::Yellow.on_default()) + .literal(AnsiColor::Green.on_default()) + .placeholder(AnsiColor::Green.on_default()) + ) .arg( - Arg::with_name(arg::CHECK) + Arg::new(arg::CHECK) .long("check") .action(ArgAction::SetTrue) .requires(cmd::FORMAT) .help("Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required."), ) .arg( - Arg::with_name(arg::CHOOSER) + Arg::new(arg::CHOOSER) .long("chooser") .action(ArgAction::Set) .help("Override binary invoked by `--choose`"), ) .arg( - Arg::with_name(arg::COLOR) + Arg::new(arg::COLOR) .long("color") .action(ArgAction::Set) - .possible_values(arg::COLOR_VALUES) + .value_parser(PossibleValuesParser::new(arg::COLOR_VALUES)) .default_value(arg::COLOR_AUTO) .help("Print colorful output"), ) .arg( - Arg::with_name(arg::COMMAND_COLOR) + Arg::new(arg::COMMAND_COLOR) .long("command-color") .action(ArgAction::Set) - .possible_values(arg::COMMAND_COLOR_VALUES) + .value_parser(PossibleValuesParser::new(arg::COMMAND_COLOR_VALUES)) .help("Echo recipe lines in "), ) - .arg(Arg::with_name(arg::YES).long("yes").action(ArgAction::SetTrue).help("Automatically confirm all recipes.")) + .arg(Arg::new(arg::YES).long("yes").action(ArgAction::SetTrue).help("Automatically confirm all recipes.")) .arg( - Arg::with_name(arg::DRY_RUN) + Arg::new(arg::DRY_RUN) .short('n') .long("dry-run") .action(ArgAction::SetTrue) @@ -181,64 +188,65 @@ impl Config { .conflicts_with(arg::QUIET), ) .arg( - Arg::with_name(arg::DUMP_FORMAT) + Arg::new(arg::DUMP_FORMAT) .long("dump-format") .action(ArgAction::Set) - .possible_values(arg::DUMP_FORMAT_VALUES) + .value_parser(PossibleValuesParser::new(arg::DUMP_FORMAT_VALUES)) .default_value(arg::DUMP_FORMAT_JUST) .value_name("FORMAT") .help("Dump justfile as "), ) .arg( - Arg::with_name(arg::HIGHLIGHT) + Arg::new(arg::HIGHLIGHT) .long("highlight") .action(ArgAction::SetTrue) .help("Highlight echoed recipe lines in bold") .overrides_with(arg::NO_HIGHLIGHT), ) .arg( - Arg::with_name(arg::LIST_HEADING) + Arg::new(arg::LIST_HEADING) .long("list-heading") .help("Print before list") .value_name("TEXT") .action(ArgAction::Set), ) .arg( - Arg::with_name(arg::LIST_PREFIX) + Arg::new(arg::LIST_PREFIX) .long("list-prefix") .help("Print before each list item") .value_name("TEXT") .action(ArgAction::Set), ) .arg ( - Arg::with_name(arg::NO_DEPS) + Arg::new(arg::NO_DEPS) .long("no-deps") .alias("no-dependencies") .action(ArgAction::SetTrue) .help("Don't run recipe dependencies") ) .arg( - Arg::with_name(arg::NO_DOTENV) + Arg::new(arg::NO_DOTENV) .long("no-dotenv") .action(ArgAction::SetTrue) .help("Don't load `.env` file"), ) .arg( - Arg::with_name(arg::NO_HIGHLIGHT) + Arg::new(arg::NO_HIGHLIGHT) .long("no-highlight") .action(ArgAction::SetTrue) .help("Don't highlight echoed recipe lines in bold") .overrides_with(arg::HIGHLIGHT), ) .arg( - Arg::with_name(arg::JUSTFILE) + Arg::new(arg::JUSTFILE) .short('f') .long("justfile") .action(ArgAction::Set) + .value_parser(value_parser!(PathBuf)) .help("Use as justfile"), ) .arg( - Arg::with_name(arg::QUIET) + Arg::new(arg::QUIET) .short('q') .long("quiet") .action(ArgAction::SetTrue) @@ -246,21 +254,21 @@ impl Config { .conflicts_with(arg::DRY_RUN), ) .arg( - Arg::with_name(arg::SET) + Arg::new(arg::SET) .long("set") .action(ArgAction::Append) .number_of_values(2) - .value_names(&["VARIABLE", "VALUE"]) + .value_names(["VARIABLE", "VALUE"]) .help("Override with "), ) .arg( - Arg::with_name(arg::SHELL) + Arg::new(arg::SHELL) .long("shell") .action(ArgAction::Set) .help("Invoke to run recipes"), ) .arg( - Arg::with_name(arg::SHELL_ARG) + Arg::new(arg::SHELL_ARG) .long("shell-arg") .action(ArgAction::Append) .allow_hyphen_values(true) @@ -268,92 +276,93 @@ impl Config { .help("Invoke shell with as an argument"), ) .arg( - Arg::with_name(arg::SHELL_COMMAND) + Arg::new(arg::SHELL_COMMAND) .long("shell-command") .requires(cmd::COMMAND) .action(ArgAction::SetTrue) .help("Invoke with the shell used to run recipe lines and backticks"), ) .arg( - Arg::with_name(arg::CLEAR_SHELL_ARGS) + Arg::new(arg::CLEAR_SHELL_ARGS) .long("clear-shell-args") .action(ArgAction::SetTrue) .overrides_with(arg::SHELL_ARG) .help("Clear shell arguments"), ) .arg( - Arg::with_name(arg::UNSORTED) + Arg::new(arg::UNSORTED) .long("unsorted") .short('u') .action(ArgAction::SetTrue) .help("Return list and summary entries in source order"), ) .arg( - Arg::with_name(arg::UNSTABLE) + Arg::new(arg::UNSTABLE) .long("unstable") .action(ArgAction::SetTrue) .help("Enable unstable features"), ) .arg( - Arg::with_name(arg::VERBOSE) + Arg::new(arg::VERBOSE) .short('v') .long("verbose") .action(ArgAction::Count) .help("Use verbose output"), ) .arg( - Arg::with_name(arg::WORKING_DIRECTORY) + Arg::new(arg::WORKING_DIRECTORY) .short('d') .long("working-directory") .action(ArgAction::Set) + .value_parser(value_parser!(PathBuf)) .help("Use as working directory. --justfile must also be set") .requires(arg::JUSTFILE), ) .arg( - Arg::with_name(cmd::CHANGELOG) + Arg::new(cmd::CHANGELOG) .long("changelog") .action(ArgAction::SetTrue) .help("Print changelog"), ) - .arg(Arg::with_name(cmd::CHOOSE).long("choose").action(ArgAction::SetTrue).help(CHOOSE_HELP)) + .arg(Arg::new(cmd::CHOOSE).long("choose").action(ArgAction::SetTrue).help(CHOOSE_HELP)) .arg( - Arg::with_name(cmd::COMMAND) + Arg::new(cmd::COMMAND) .long("command") .short('c') - .min_values(1) + .num_args(1..) .allow_hyphen_values(true) .action(ArgAction::Append) - .value_parser(clap::value_parser!(std::ffi::OsString)) + .value_parser(value_parser!(std::ffi::OsString)) .help( "Run an arbitrary command with the working directory, `.env`, overrides, and exports \ set", ), ) .arg( - Arg::with_name(cmd::COMPLETIONS) + Arg::new(cmd::COMPLETIONS) .long("completions") .action(ArgAction::Append) - .min_values(1) + .num_args(1..) .value_name("SHELL") - .possible_values(clap_complete::Shell::possible_values()) - .set(ArgSettings::CaseInsensitive) + .value_parser(value_parser!(clap_complete::Shell)) + .ignore_case(true) .help("Print shell completion script for "), ) .arg( - Arg::with_name(cmd::DUMP) + Arg::new(cmd::DUMP) .long("dump") .action(ArgAction::SetTrue) .help("Print justfile"), ) .arg( - Arg::with_name(cmd::EDIT) + Arg::new(cmd::EDIT) .short('e') .long("edit") .action(ArgAction::SetTrue) .help("Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`"), ) .arg( - Arg::with_name(cmd::EVALUATE) + Arg::new(cmd::EVALUATE) .long("evaluate") .action(ArgAction::SetTrue) .help( @@ -362,28 +371,28 @@ impl Config { ), ) .arg( - Arg::with_name(cmd::FORMAT) + Arg::new(cmd::FORMAT) .long("fmt") .alias("format") .action(ArgAction::SetTrue) .help("Format and overwrite justfile"), ) .arg( - Arg::with_name(cmd::INIT) + Arg::new(cmd::INIT) .long("init") .alias("initialize") .action(ArgAction::SetTrue) .help("Initialize new justfile in project root"), ) .arg( - Arg::with_name(cmd::LIST) + Arg::new(cmd::LIST) .short('l') .long("list") .action(ArgAction::SetTrue) .help("List available recipes and their arguments"), ) .arg( - Arg::with_name(cmd::SHOW) + Arg::new(cmd::SHOW) .short('s') .long("show") .action(ArgAction::Set) @@ -392,34 +401,35 @@ impl Config { .help("Show information about "), ) .arg( - Arg::with_name(cmd::SUMMARY) + Arg::new(cmd::SUMMARY) .long("summary") .action(ArgAction::SetTrue) .help("List names of available recipes"), ) .arg( - Arg::with_name(cmd::VARIABLES) + Arg::new(cmd::VARIABLES) .long("variables") .action(ArgAction::SetTrue) .help("List names of variables"), ) .arg( - Arg::with_name(arg::DOTENV_FILENAME) + Arg::new(arg::DOTENV_FILENAME) .long("dotenv-filename") .action(ArgAction::Set) .help("Search for environment file named instead of `.env`") .conflicts_with(arg::DOTENV_PATH), ) .arg( - Arg::with_name(arg::DOTENV_PATH) + Arg::new(arg::DOTENV_PATH) .long("dotenv-path") .action(ArgAction::Set) + .value_parser(value_parser!(PathBuf)) .help("Load environment file at instead of searching for one"), ) - .group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL)) + .group(ArgGroup::new("SUBCOMMAND").args(cmd::ALL)) .arg( - Arg::with_name(arg::ARGUMENTS) - .multiple_values(true) + Arg::new(arg::ARGUMENTS) + .num_args(1..) .action(ArgAction::Append) .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"), ); @@ -444,12 +454,12 @@ impl Config { fn color_from_matches(matches: &ArgMatches) -> ConfigResult { let value = matches - .value_of(arg::COLOR) + .get_one::(arg::COLOR) .ok_or_else(|| ConfigError::Internal { message: "`--color` had no value".to_string(), })?; - match value { + match value.as_str() { arg::COLOR_AUTO => Ok(Color::auto()), arg::COLOR_ALWAYS => Ok(Color::always()), arg::COLOR_NEVER => Ok(Color::never()), @@ -460,8 +470,8 @@ impl Config { } fn command_color_from_matches(matches: &ArgMatches) -> ConfigResult> { - if let Some(value) = matches.value_of(arg::COMMAND_COLOR) { - match value { + if let Some(value) = matches.get_one::(arg::COMMAND_COLOR) { + match value.as_str() { arg::COMMAND_COLOR_BLACK => Ok(Some(ansi_term::Color::Black)), arg::COMMAND_COLOR_BLUE => Ok(Some(ansi_term::Color::Blue)), arg::COMMAND_COLOR_CYAN => Ok(Some(ansi_term::Color::Cyan)), @@ -479,13 +489,14 @@ impl Config { } fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult { - let value = matches - .value_of(arg::DUMP_FORMAT) - .ok_or_else(|| ConfigError::Internal { - message: "`--dump-format` had no value".to_string(), - })?; + let value = + matches + .get_one::(arg::DUMP_FORMAT) + .ok_or_else(|| ConfigError::Internal { + message: "`--dump-format` had no value".to_string(), + })?; - match value { + match value.as_str() { arg::DUMP_FORMAT_JSON => Ok(DumpFormat::Json), arg::DUMP_FORMAT_JUST => Ok(DumpFormat::Just), _ => Err(ConfigError::Internal { @@ -513,15 +524,21 @@ impl Config { } } - let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS)); + let positional = Positional::from_values( + matches + .get_many::(arg::ARGUMENTS) + .map(|s| s.map(String::as_str)), + ); for (name, value) in positional.overrides { overrides.insert(name.clone(), value.clone()); } let search_config = { - let justfile = matches.value_of(arg::JUSTFILE).map(PathBuf::from); - let working_directory = matches.value_of(arg::WORKING_DIRECTORY).map(PathBuf::from); + let justfile = matches.get_one::(arg::JUSTFILE).map(Into::into); + let working_directory = matches + .get_one::(arg::WORKING_DIRECTORY) + .map(Into::into); if let Some(search_directory) = positional.search_directory.map(PathBuf::from) { if justfile.is_some() || working_directory.is_some() { @@ -578,20 +595,18 @@ impl Config { Subcommand::Changelog } else if matches.get_flag(cmd::CHOOSE) { Subcommand::Choose { - chooser: matches.value_of(arg::CHOOSER).map(str::to_owned), + chooser: matches.get_one::(arg::CHOOSER).map(Into::into), overrides, } - } else if let Some(values) = matches.values_of_os(cmd::COMMAND) { - let mut arguments = values.map(OsStr::to_owned).collect::>(); + } else if let Some(values) = matches.get_many::(cmd::COMMAND) { + let mut arguments = values.map(Into::into).collect::>(); Subcommand::Command { binary: arguments.remove(0), arguments, overrides, } - } else if let Some(shell) = matches.value_of(cmd::COMPLETIONS) { - Subcommand::Completions { - shell: shell.to_owned(), - } + } else if let Some(&shell) = matches.get_one::(cmd::COMPLETIONS) { + Subcommand::Completions { shell } } else if matches.get_flag(cmd::EDIT) { Subcommand::Edit } else if matches.get_flag(cmd::SUMMARY) { @@ -604,10 +619,8 @@ impl Config { Subcommand::Init } else if matches.get_flag(cmd::LIST) { Subcommand::List - } else if let Some(name) = matches.value_of(cmd::SHOW) { - Subcommand::Show { - name: name.to_owned(), - } + } else if let Some(name) = matches.get_one::(cmd::SHOW).map(Into::into) { + Subcommand::Show { name } } else if matches.get_flag(cmd::EVALUATE) { if positional.arguments.len() > 1 { return Err(ConfigError::SubcommandArguments { @@ -650,24 +663,24 @@ impl Config { check: matches.get_flag(arg::CHECK), color, command_color, - dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned), - dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from), + dotenv_filename: matches + .get_one::(arg::DOTENV_FILENAME) + .map(Into::into), + dotenv_path: matches.get_one::(arg::DOTENV_PATH).map(Into::into), dry_run: matches.get_flag(arg::DRY_RUN), dump_format: Self::dump_format_from_matches(matches)?, highlight: !matches.get_flag(arg::NO_HIGHLIGHT), invocation_directory, list_heading: matches - .value_of(arg::LIST_HEADING) - .unwrap_or("Available recipes:\n") - .to_owned(), + .get_one::(arg::LIST_HEADING) + .map_or_else(|| "Available recipes:\n".into(), Into::into), list_prefix: matches - .value_of(arg::LIST_PREFIX) - .unwrap_or(" ") - .to_owned(), + .get_one::(arg::LIST_PREFIX) + .map_or_else(|| " ".into(), Into::into), load_dotenv: !matches.get_flag(arg::NO_DOTENV), no_dependencies: matches.get_flag(arg::NO_DEPS), search_config, - shell: matches.value_of(arg::SHELL).map(str::to_owned), + shell: matches.get_one::(arg::SHELL).map(Into::into), shell_args, shell_command: matches.get_flag(arg::SHELL_COMMAND), subcommand, @@ -701,6 +714,7 @@ impl Config { mod tests { use super::*; + use clap::error::{ContextKind, ContextValue}; use pretty_assertions::assert_eq; macro_rules! test { @@ -750,7 +764,7 @@ mod tests { fn test(arguments: &[&str], want: Config) { let app = Config::app(); let matches = app - .get_matches_from_safe(arguments) + .try_get_matches_from(arguments) .expect("argument parsing failed"); let have = Config::from_matches(&matches).expect("config parsing failed"); assert_eq!(have, want); @@ -770,7 +784,7 @@ mod tests { let app = Config::app(); - app.get_matches_from_safe(arguments).expect_err("Expected clap error"); + app.try_get_matches_from(arguments).expect_err("Expected clap error"); } }; { @@ -788,7 +802,7 @@ mod tests { let app = Config::app(); - let matches = app.get_matches_from_safe(arguments).expect("Matching fails"); + let matches = app.try_get_matches_from(arguments).expect("Matching fails"); match Config::from_matches(&matches).expect_err("config parsing succeeded") { $error => { $($check)? } @@ -814,7 +828,7 @@ mod tests { let app = Config::app(); - match app.get_matches_from_safe(arguments) { + match app.try_get_matches_from(arguments) { Err($error) => { $($check)? } other => panic!("Unexpected result from get matches: {other:?}") } @@ -1147,13 +1161,13 @@ mod tests { test! { name: subcommand_completions, args: ["--completions", "bash"], - subcommand: Subcommand::Completions{shell: "bash".to_owned()}, + subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, } test! { name: subcommand_completions_uppercase, args: ["--completions", "BASH"], - subcommand: Subcommand::Completions{shell: "BASH".to_owned()}, + subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, } error! { @@ -1426,9 +1440,14 @@ mod tests { error_matches! { name: completions_arguments, args: ["--completions", "zsh", "foo"], - error: clap::Error { kind: clap::ErrorKind::InvalidValue, info, .. }, + error: error, check: { - assert_eq!(info, &["--completions ...", "foo", "bash", "elvish", "fish", "powershell", "zsh"]); + assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); + assert_eq!(error.context().collect::>(), vec![ + (ContextKind::InvalidArg, &ContextValue::String("--completions ...".into())), + (ContextKind::InvalidValue, &ContextValue::String("foo".into())), + (ContextKind::ValidValue, &ContextValue::Strings(["bash".into(), "elvish".into(), "fish".into(), "powershell".into(), "zsh".into()].into())), + ]); }, } @@ -1515,9 +1534,14 @@ mod tests { error_matches! { name: show_arguments, args: ["--show", "foo", "bar"], - error: clap::Error { kind: clap::ErrorKind::ArgumentConflict, info, .. }, + error: error, check: { - assert_eq!(info, &["..."]); + assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); + assert_eq!(error.context().collect::>(), vec![ + (ContextKind::InvalidArg, &ContextValue::String("--show ".into())), + (ContextKind::PriorArg, &ContextValue::String("[ARGUMENTS]...".into())), + (ContextKind::Usage, &ContextValue::StyledStr("\u{1b}[33mUsage:\u{1b}[0m \u{1b}[32mjust\u{1b}[0m \u{1b}[32m--show\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[32m\u{1b}[0m \u{1b}[32m[ARGUMENTS]...\u{1b}[0m".into())), + ]); }, } diff --git a/src/function.rs b/src/function.rs index fee4bfc5e9..b7e03e5553 100644 --- a/src/function.rs +++ b/src/function.rs @@ -504,7 +504,10 @@ mod tests { fn dir_not_unicode() { use std::os::unix::ffi::OsStrExt; assert_eq!( - dir("foo", || Some(OsStr::from_bytes(b"\xe0\x80\x80").into())).unwrap_err(), + dir("foo", || Some( + std::ffi::OsStr::from_bytes(b"\xe0\x80\x80").into() + )) + .unwrap_err(), "unable to convert foo directory path to string: ���", ); } diff --git a/src/lib.rs b/src/lib.rs index 7f113ca678..65592f8e54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ pub(crate) use { cmp, collections::{BTreeMap, BTreeSet, HashMap}, env, - ffi::{OsStr, OsString}, + ffi::OsString, fmt::{self, Debug, Display, Formatter}, fs, io::{self, Write}, diff --git a/src/subcommand.rs b/src/subcommand.rs index ed2b569704..c3aaf77600 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,3 +1,7 @@ +use std::io::Read; + +use tempfile::tempfile; + use super::*; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; @@ -15,7 +19,7 @@ pub(crate) enum Subcommand { overrides: BTreeMap, }, Completions { - shell: String, + shell: clap_complete::Shell, }, Dump, Edit, @@ -50,7 +54,7 @@ impl Subcommand { Self::changelog(); return Ok(()); } - Completions { shell } => return Self::completions(crate::config::Config::app(), shell), + Completions { shell } => return Self::completions(&crate::config::Config::app(), *shell), Init => return Self::init(config), Run { arguments, @@ -275,7 +279,7 @@ impl Subcommand { justfile.run(config, search, overrides, &recipes) } - fn completions(command: clap::Command<'_>, shell: &str) -> RunResult<'static, ()> { + fn completions(command: &clap::Command, shell: clap_complete::Shell) -> RunResult<'static, ()> { use clap_complete::{Generator, Shell}; fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> { @@ -289,13 +293,16 @@ impl Subcommand { } } - let shell = shell - .parse::() - .expect("Invalid value for clap::Shell"); - - let mut buffer = Vec::new(); - shell.generate(&command, &mut buffer); - let mut script = String::from_utf8(buffer).expect("Clap completion not UTF-8"); + let mut script = { + let mut tmp = tempfile() + .map_err(|e| Error::internal(format!("Failed to create tempfile for completions: {e}")))?; + shell.generate(command, &mut tmp); + let mut buffer = String::new(); + tmp + .read_to_string(&mut buffer) + .map_err(|e| Error::internal(format!("Failed to read completions from tempfile: {e}")))?; + buffer + }; match shell { Shell::Bash => { diff --git a/src/testing.rs b/src/testing.rs index 09ddb3e16a..56241dc332 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -10,7 +10,7 @@ pub(crate) fn config(args: &[&str]) -> Config { let app = Config::app(); - let matches = app.get_matches_from_safe(args).unwrap(); + let matches = app.try_get_matches_from(args).unwrap(); Config::from_matches(&matches).unwrap() } diff --git a/tests/command.rs b/tests/command.rs index 4a956945d2..44bde24f56 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -40,9 +40,9 @@ test! { ", args: ("--command"), stderr: " - error: The argument '--command ...' requires a value but none was supplied + error: a value is required for '--command ...' but none was supplied - For more information try --help + For more information, try '--help'. ", status: 2, } diff --git a/tests/fmt.rs b/tests/fmt.rs index 0c5e16ff95..1d70a6f9ab 100644 --- a/tests/fmt.rs +++ b/tests/fmt.rs @@ -15,8 +15,8 @@ test! { name: check_without_fmt, justfile: "", args: ("--check"), - stderr_regex: "error: The following required arguments were not provided: - --fmt + stderr_regex: "error: the following required arguments were not provided: + --fmt (.|\\n)+", status: 2, } diff --git a/tests/test.rs b/tests/test.rs index 2a3a6ac37d..f2a3cea6bf 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -189,6 +189,7 @@ impl Test { } impl Test { + #[track_caller] pub(crate) fn run(self) -> Output { if let Some(justfile) = &self.justfile { let justfile = unindent(justfile);