artichoke/ruby/cli.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
//! Command line interface parser for the `ruby` binary.
use std::env;
use std::ffi::OsString;
use std::iter;
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.
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn parse_args() -> Args {
let matches = clap_matches(env::args_os());
let commands = matches
.get_many::<OsString>("commands")
.into_iter()
.flat_map(|s| s.map(Clone::clone))
.collect::<Vec<_>>();
let mut args = Args::empty()
.with_copyright(*matches.get_one::<bool>("copyright").expect("defaulted by clap"))
.with_fixture(matches.get_one::<PathBuf>("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::<PathBuf>("programfile").cloned() {
args = args.with_programfile(Some(programfile));
if let Some(argv) = matches.get_many::<OsString>("arguments") {
let ruby_program_argv = argv.map(Clone::clone).collect::<Vec<_>>();
args = args.with_argv(ruby_program_argv);
}
}
} else {
args = args.with_commands(commands);
if let Some(first_arg) = matches.get_one::<PathBuf>("programfile").cloned() {
if let Some(argv) = matches.get_many::<OsString>("arguments") {
let ruby_program_argv = iter::once(OsString::from(first_arg))
.chain(argv.map(Clone::clone))
.collect::<Vec<_>>();
args = args.with_argv(ruby_program_argv);
} else {
args = args.with_argv(vec![OsString::from(first_arg)]);
}
}
}
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.
#[must_use]
fn clap_matches<I, T>(args: I) -> ArgMatches
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let err = match cli().try_get_matches_from(args) {
Ok(matches) => return matches,
Err(err) => err,
};
// 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.
let _ignored = err.print();
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();
}
}