From 3eca7b9abbf237dabc237d57c71f053d01502f44 Mon Sep 17 00:00:00 2001 From: Ryan Lopopolo Date: Wed, 28 Sep 2022 18:46:24 -0700 Subject: [PATCH] Update `artichoke` CLI to clap v4 https://epage.github.io/blog/2022/09/clap4/ `--help` output is changed, so update the ui-test snapshots. --- Cargo.lock | 48 +++--- Cargo.toml | 2 +- README.md | 22 ++- src/bin/artichoke.rs | 157 +----------------- src/ruby.rs | 2 + src/ruby/cli.rs | 153 +++++++++++++++++ .../tests/snapshots/artichoke__help_unix.snap | 24 ++- .../snapshots/artichoke__help_windows.snap | 24 ++- 8 files changed, 209 insertions(+), 223 deletions(-) create mode 100644 src/ruby/cli.rs diff --git a/Cargo.lock b/Cargo.lock index 6abe57992ddc..427246d72bb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,10 +76,15 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.1.0" +name = "atty" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] [[package]] name = "base64" @@ -132,22 +137,22 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.20" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" +checksum = "31c9484ccdc4cb8e7b117cbd0eb150c7c0f04464854e4679aeb50ef03b32d003" dependencies = [ + "atty", "bitflags", "clap_lex", - "indexmap", "strsim", - "textwrap", + "termcolor", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] @@ -235,10 +240,13 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "hermit-abi" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] [[package]] name = "iana-time-zone" @@ -254,16 +262,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] - [[package]] name = "intaglio" version = "1.7.0" @@ -705,12 +703,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 60a23c3485a1..a634245f0a39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ homepage.workspace = true documentation.workspace = true [dependencies] -clap = { version = "3.2.5", optional = true, default-features = false, features = ["std", "suggestions"] } +clap = { version = "4.0.2", optional = true } # XXX: load-bearing unused dependency. # # `rustyline` improperly declares its minimum version on `log` as `0.4` despite diff --git a/README.md b/README.md index 69faaff23af6..f7e772c053f0 100644 --- a/README.md +++ b/README.md @@ -94,22 +94,20 @@ temporary workaround is to inject data into the interpreter with the ```console $ artichoke --help -artichoke 0.1.0-pre.0 Artichoke is a Ruby made with Rust. -USAGE: - artichoke [OPTIONS] [ARGS] +Usage: artichoke [OPTIONS] [programfile] [arguments]... -ARGS: - - ... +Arguments: + [programfile] + [arguments]... -OPTIONS: - --copyright print the copyright - -e one line of script. Several -e's allowed. Omit [programfile] - -h, --help Print help information - -V, --version Print version information - --with-fixture file whose contents will be read into the `$fixture` global +Options: + --copyright print the copyright + -e one line of script. Several -e's allowed. Omit [programfile] + --with-fixture file whose contents will be read into the `$fixture` global + -h, --help Print help information + -V, --version Print version information ``` ## Design and Goals diff --git a/src/bin/artichoke.rs b/src/bin/artichoke.rs index 5aab468a743d..f588bed88115 100644 --- a/src/bin/artichoke.rs +++ b/src/bin/artichoke.rs @@ -19,43 +19,16 @@ //! Artichoke does not yet support reading from the local file system. A //! temporary workaround is to inject data into the interpreter with the //! `--with-fixture` flag, which reads file contents into a `$fixture` global. -//! -//! ```console -//! $ cargo run -q --bin artichoke -- --help -//! artichoke 0.1.0-pre.0 -//! Artichoke is a Ruby made with Rust. -//! -//! USAGE: -//! artichoke [OPTIONS] [ARGS] -//! -//! ARGS: -//! -//! ... -//! -//! OPTIONS: -//! --copyright print the copyright -//! -e one line of script. Several -e's allowed. Omit [programfile] -//! -h, --help Print help information -//! -V, --version Print version information -//! --with-fixture file whose contents will be read into the `$fixture` global -//! ``` -use std::env; -use std::error; -use std::ffi::OsString; use std::io::{self, Write}; -use std::path::PathBuf; use std::process; -use artichoke::ruby::{self, Args, ExecutionResult}; -use clap::builder::ArgAction; -use clap::{Arg, ArgMatches, Command}; +use artichoke::ruby::cli; +use artichoke::ruby::{self, ExecutionResult}; use termcolor::{ColorChoice, StandardStream, WriteColor}; -type Result = ::std::result::Result>; - fn main() { - let args = match parse_args() { + let args = match cli::parse_args() { Ok(args) => args, Err(err) => { // Suppress all errors at this point (e.g. from a broken pipe) since @@ -80,127 +53,3 @@ fn main() { } } } - -fn parse_args() -> Result { - let matches = clap_matches(env::args_os())?; - - let commands = matches - .get_many::("commands") - .into_iter() - .flat_map(|s| s.map(Clone::clone)) - .collect::>(); - let mut args = Args::empty() - .with_copyright(*matches.get_one::("copyright").expect("defaulted by clap")) - .with_fixture(matches.get_one::("fixture").cloned()); - - // If no `-e` arguments are given, the first positional argument is the - // `programfile`. All trailing arguments are ARGV to the script. - // - // If there are `-e` arguments given, there is no programfile and all - // positional arguments are ARGV to the inline script. - // - // ```console - // $ ruby -e 'puts ARGV.inspect' a b c - // ["a", "b", "c"] - // $ cat foo.rb - // puts ARGV.inspect - // $ ruby foo.rb a b c - // ["a", "b", "c"] - // $ ruby bar.rb a b c - // ruby: No such file or directory -- bar.rb (LoadError) - // ``` - if commands.is_empty() { - if let Some(programfile) = matches.get_one::("programfile").cloned() { - args = args.with_programfile(Some(programfile)); - if let Some(argv) = matches.get_many::("arguments") { - let ruby_program_argv = argv.map(Clone::clone).collect::>(); - args = args.with_argv(ruby_program_argv); - } - } - } else { - args = args.with_commands(commands); - if let Some(first_arg) = matches.get_one::("programfile").cloned() { - if let Some(argv) = matches.get_many::("arguments") { - let ruby_program_argv = [OsString::from(first_arg)] - .into_iter() - .chain(argv.map(Clone::clone)) - .collect::>(); - args = args.with_argv(ruby_program_argv); - } else { - args = args.with_argv(vec![OsString::from(first_arg)]); - } - } - } - - Ok(args) -} - -fn command() -> Command<'static> { - Command::new("artichoke") - .about("Artichoke is a Ruby made with Rust.") - .version(env!("CARGO_PKG_VERSION")) - .arg( - Arg::new("copyright") - .long("copyright") - .action(ArgAction::SetTrue) - .help("print the copyright"), - ) - .arg( - Arg::new("commands") - .short('e') - .action(ArgAction::Append) - .value_parser(clap::value_parser!(OsString)) - .help(r"one line of script. Several -e's allowed. Omit [programfile]"), - ) - .arg( - Arg::new("fixture") - .long("with-fixture") - .takes_value(true) - .value_parser(clap::value_parser!(PathBuf)) - .help("file whose contents will be read into the `$fixture` global"), - ) - .arg(Arg::new("programfile").value_parser(clap::value_parser!(PathBuf))) - .arg( - Arg::new("arguments") - .multiple_values(true) - .value_parser(clap::value_parser!(OsString)), - ) - .trailing_var_arg(true) -} - -// NOTE: This routine is plucked from `ripgrep` as of commit -// `9f924ee187d4c62aa6ebe4903d0cfc6507a5adb5`. -// -// `ripgrep` is licensed with the MIT License Copyright (c) 2015 Andrew Gallant. -// -// https://github.com/BurntSushi/ripgrep/blob/9f924ee187d4c62aa6ebe4903d0cfc6507a5adb5/LICENSE-MIT -// -// See https://github.com/artichoke/artichoke/issues/1301. - -/// Returns a clap matches object if the given arguments parse successfully. -/// -/// Otherwise, if an error occurred, then it is returned unless the error -/// corresponds to a `--help` or `--version` request. In which case, the -/// corresponding output is printed and the current process is exited -/// successfully. -fn clap_matches(args: I) -> Result -where - I: IntoIterator, - T: Into + Clone, -{ - let err = match command().try_get_matches_from(args) { - Ok(matches) => return Ok(matches), - Err(err) => err, - }; - if err.use_stderr() { - return Err(err.into()); - } - // Explicitly ignore any error returned by write!. The most likely error - // at this point is a broken pipe error, in which case, we want to ignore - // it and exit quietly. - // - // (This is the point of this helper function. clap's functionality for - // doing this will panic on a broken pipe error.) - let _ignored = write!(io::stdout(), "{}", err); - process::exit(0); -} diff --git a/src/ruby.rs b/src/ruby.rs index a5710565d55a..440452ee4237 100644 --- a/src/ruby.rs +++ b/src/ruby.rs @@ -19,6 +19,8 @@ use crate::backtrace; use crate::filename::INLINE_EVAL_SWITCH; use crate::prelude::*; +pub mod cli; + /// Command line arguments for Artichoke `ruby` frontend. #[derive(Default, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Args { diff --git a/src/ruby/cli.rs b/src/ruby/cli.rs new file mode 100644 index 000000000000..f362704330c3 --- /dev/null +++ b/src/ruby/cli.rs @@ -0,0 +1,153 @@ +//! Command line interface parser for the `ruby` binary. + +use std::env; +use std::error::Error; +use std::ffi::OsString; +use std::io::{self, Write as _}; +use std::path::PathBuf; +use std::process; + +use clap::builder::ArgAction; +use clap::{Arg, ArgMatches, Command}; + +use super::Args; + +/// Parse CLI arguments into an [`Args`] struct. +/// +/// # Errors +/// +/// If an invalid argument is provided, an error is returned. +pub fn parse_args() -> Result> { + let matches = clap_matches(env::args_os())?; + + let commands = matches + .get_many::("commands") + .into_iter() + .flat_map(|s| s.map(Clone::clone)) + .collect::>(); + let mut args = Args::empty() + .with_copyright(*matches.get_one::("copyright").expect("defaulted by clap")) + .with_fixture(matches.get_one::("fixture").cloned()); + + // If no `-e` arguments are given, the first positional argument is the + // `programfile`. All trailing arguments are ARGV to the script. + // + // If there are `-e` arguments given, there is no programfile and all + // positional arguments are ARGV to the inline script. + // + // ```console + // $ ruby -e 'puts ARGV.inspect' a b c + // ["a", "b", "c"] + // $ cat foo.rb + // puts ARGV.inspect + // $ ruby foo.rb a b c + // ["a", "b", "c"] + // $ ruby bar.rb a b c + // ruby: No such file or directory -- bar.rb (LoadError) + // ``` + if commands.is_empty() { + if let Some(programfile) = matches.get_one::("programfile").cloned() { + args = args.with_programfile(Some(programfile)); + if let Some(argv) = matches.get_many::("arguments") { + let ruby_program_argv = argv.map(Clone::clone).collect::>(); + args = args.with_argv(ruby_program_argv); + } + } + } else { + args = args.with_commands(commands); + if let Some(first_arg) = matches.get_one::("programfile").cloned() { + if let Some(argv) = matches.get_many::("arguments") { + let ruby_program_argv = [OsString::from(first_arg)] + .into_iter() + .chain(argv.map(Clone::clone)) + .collect::>(); + args = args.with_argv(ruby_program_argv); + } else { + args = args.with_argv(vec![OsString::from(first_arg)]); + } + } + } + + Ok(args) +} + +// NOTE: This routine is plucked from `ripgrep` as of commit +// `9f924ee187d4c62aa6ebe4903d0cfc6507a5adb5`. +// +// `ripgrep` is licensed with the MIT License Copyright (c) 2015 Andrew Gallant. +// +// https://github.com/BurntSushi/ripgrep/blob/9f924ee187d4c62aa6ebe4903d0cfc6507a5adb5/LICENSE-MIT +// +// See https://github.com/artichoke/artichoke/issues/1301. + +/// Returns a clap matches object if the given arguments parse successfully. +/// +/// Otherwise, if an error occurred, then it is returned unless the error +/// corresponds to a `--help` or `--version` request. In which case, the +/// corresponding output is printed and the current process is exited +/// successfully. +fn clap_matches(args: I) -> Result> +where + I: IntoIterator, + T: Into + Clone, +{ + let err = match cli().try_get_matches_from(args) { + Ok(matches) => return Ok(matches), + Err(err) => err, + }; + if err.use_stderr() { + return Err(err.into()); + } + // Explicitly ignore any error returned by write!. The most likely error + // at this point is a broken pipe error, in which case, we want to ignore + // it and exit quietly. + // + // (This is the point of this helper function. clap's functionality for + // doing this will panic on a broken pipe error.) + let _ignored = write!(io::stdout(), "{}", err); + process::exit(0); +} + +/// Build a [`clap`] CLI parser. +#[must_use] +pub fn cli() -> Command { + Command::new("artichoke") + .about("Artichoke is a Ruby made with Rust.") + .version(env!("CARGO_PKG_VERSION")) + .arg( + Arg::new("copyright") + .long("copyright") + .action(ArgAction::SetTrue) + .help("print the copyright"), + ) + .arg( + Arg::new("commands") + .short('e') + .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) + .help(r"one line of script. Several -e's allowed. Omit [programfile]"), + ) + .arg( + Arg::new("fixture") + .long("with-fixture") + .value_parser(clap::value_parser!(PathBuf)) + .help("file whose contents will be read into the `$fixture` global"), + ) + .arg(Arg::new("programfile").value_parser(clap::value_parser!(PathBuf))) + .arg( + Arg::new("arguments") + .num_args(..) + .value_parser(clap::value_parser!(OsString)) + .trailing_var_arg(true), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_cli() { + cli().debug_assert(); + } +} diff --git a/ui-tests/tests/snapshots/artichoke__help_unix.snap b/ui-tests/tests/snapshots/artichoke__help_unix.snap index 3d441d06babf..3746cd1547c8 100644 --- a/ui-tests/tests/snapshots/artichoke__help_unix.snap +++ b/ui-tests/tests/snapshots/artichoke__help_unix.snap @@ -1,26 +1,22 @@ --- source: tests/artichoke.rs -assertion_line: 15 expression: "run(BINARY, &[\"--help\"]).unwrap()" - --- call_args = ['--help'] status = 0 stdout = ''' -"artichoke 0.1.0-pre.0" "Artichoke is a Ruby made with Rust." "" -"USAGE:" -" artichoke [OPTIONS] [ARGS]" +"Usage: artichoke [OPTIONS] [programfile] [arguments]..." "" -"ARGS:" -" " -" ... " +"Arguments:" +" [programfile] " +" [arguments]... " "" -"OPTIONS:" -" --copyright print the copyright" -" -e one line of script. Several -e\'s allowed. Omit [programfile]" -" -h, --help Print help information" -" -V, --version Print version information" -" --with-fixture file whose contents will be read into the `$fixture` global"''' +"Options:" +" --copyright print the copyright" +" -e one line of script. Several -e\'s allowed. Omit [programfile]" +" --with-fixture file whose contents will be read into the `$fixture` global" +" -h, --help Print help information" +" -V, --version Print version information"''' stderr = '' diff --git a/ui-tests/tests/snapshots/artichoke__help_windows.snap b/ui-tests/tests/snapshots/artichoke__help_windows.snap index ba4e1b6ade90..2bb690c30e3b 100644 --- a/ui-tests/tests/snapshots/artichoke__help_windows.snap +++ b/ui-tests/tests/snapshots/artichoke__help_windows.snap @@ -1,26 +1,22 @@ --- source: tests/artichoke.rs -assertion_line: 9 expression: "run(BINARY, &[\"--help\"]).unwrap()" - --- call_args = ['--help'] status = 0 stdout = ''' -"artichoke 0.1.0-pre.0" "Artichoke is a Ruby made with Rust." "" -"USAGE:" -" artichoke.exe [OPTIONS] [ARGS]" +"Usage: artichoke.exe [OPTIONS] [programfile] [arguments]..." "" -"ARGS:" -" " -" ... " +"Arguments:" +" [programfile] " +" [arguments]... " "" -"OPTIONS:" -" --copyright print the copyright" -" -e one line of script. Several -e\'s allowed. Omit [programfile]" -" -h, --help Print help information" -" -V, --version Print version information" -" --with-fixture file whose contents will be read into the `$fixture` global"''' +"Options:" +" --copyright print the copyright" +" -e one line of script. Several -e\'s allowed. Omit [programfile]" +" --with-fixture file whose contents will be read into the `$fixture` global" +" -h, --help Print help information" +" -V, --version Print version information"''' stderr = ''