artichoke/ruby/
cli.rs

1//! Command line interface parser for the `ruby` binary.
2
3use std::env;
4use std::ffi::OsString;
5use std::iter;
6use std::path::PathBuf;
7use std::process;
8
9use clap::builder::ArgAction;
10use clap::{Arg, ArgMatches, Command};
11
12use super::Args;
13
14/// Parse CLI arguments into an [`Args`] struct.
15///
16/// # Errors
17///
18/// If an invalid argument is provided, an error is returned.
19#[must_use]
20#[expect(clippy::missing_panics_doc, reason = "clap errors are not surfaced due to defaults")]
21pub fn parse_args() -> Args {
22    let matches = clap_matches(env::args_os());
23
24    let commands = matches
25        .get_many::<OsString>("commands")
26        .into_iter()
27        .flat_map(|s| s.map(Clone::clone))
28        .collect::<Vec<_>>();
29    let mut args = Args::empty()
30        .with_copyright(*matches.get_one::<bool>("copyright").expect("defaulted by clap"))
31        .with_fixture(matches.get_one::<PathBuf>("fixture").cloned());
32
33    // If no `-e` arguments are given, the first positional argument is the
34    // `programfile`. All trailing arguments are ARGV to the script.
35    //
36    // If there are `-e` arguments given, there is no programfile and all
37    // positional arguments are ARGV to the inline script.
38    //
39    // ```console
40    // $ ruby -e 'puts ARGV.inspect' a b c
41    // ["a", "b", "c"]
42    // $ cat foo.rb
43    // puts ARGV.inspect
44    // $ ruby foo.rb a b c
45    // ["a", "b", "c"]
46    // $ ruby bar.rb a b c
47    // ruby: No such file or directory -- bar.rb (LoadError)
48    // ```
49    if commands.is_empty() {
50        if let Some(programfile) = matches.get_one::<PathBuf>("programfile").cloned() {
51            args = args.with_programfile(Some(programfile));
52            if let Some(argv) = matches.get_many::<OsString>("arguments") {
53                let ruby_program_argv = argv.map(Clone::clone).collect::<Vec<_>>();
54                args = args.with_argv(ruby_program_argv);
55            }
56        }
57    } else {
58        args = args.with_commands(commands);
59        if let Some(first_arg) = matches.get_one::<PathBuf>("programfile").cloned() {
60            if let Some(argv) = matches.get_many::<OsString>("arguments") {
61                let ruby_program_argv = iter::once(OsString::from(first_arg))
62                    .chain(argv.map(Clone::clone))
63                    .collect::<Vec<_>>();
64                args = args.with_argv(ruby_program_argv);
65            } else {
66                args = args.with_argv(vec![OsString::from(first_arg)]);
67            }
68        }
69    }
70
71    args
72}
73
74// NOTE: This routine is plucked from `ripgrep` as of commit
75// `9f924ee187d4c62aa6ebe4903d0cfc6507a5adb5`.
76//
77// `ripgrep` is licensed with the MIT License Copyright (c) 2015 Andrew Gallant.
78//
79// <https://github.com/BurntSushi/ripgrep/blob/9f924ee187d4c62aa6ebe4903d0cfc6507a5adb5/LICENSE-MIT>
80//
81// See <https://github.com/artichoke/artichoke/issues/1301>.
82
83/// Returns a clap matches object if the given arguments parse successfully.
84///
85/// Otherwise, if an error occurred, then it is returned unless the error
86/// corresponds to a `--help` or `--version` request. In which case, the
87/// corresponding output is printed and the current process is exited
88/// successfully.
89#[must_use]
90fn clap_matches<I, T>(args: I) -> ArgMatches
91where
92    I: IntoIterator<Item = T>,
93    T: Into<OsString> + Clone,
94{
95    let err = match cli().try_get_matches_from(args) {
96        Ok(matches) => return matches,
97        Err(err) => err,
98    };
99    // Explicitly ignore any error returned by write!. The most likely error
100    // at this point is a broken pipe error, in which case, we want to ignore
101    // it and exit quietly.
102    let _ignored = err.print();
103    process::exit(0);
104}
105
106/// Build a [`clap`] CLI parser.
107#[must_use]
108pub fn cli() -> Command {
109    Command::new("artichoke")
110        .about("Artichoke is a Ruby made with Rust.")
111        .version(env!("CARGO_PKG_VERSION"))
112        .arg(
113            Arg::new("copyright")
114                .long("copyright")
115                .action(ArgAction::SetTrue)
116                .help("print the copyright"),
117        )
118        .arg(
119            Arg::new("commands")
120                .short('e')
121                .action(ArgAction::Append)
122                .value_parser(clap::value_parser!(OsString))
123                .help(r"one line of script. Several -e's allowed. Omit [programfile]"),
124        )
125        .arg(
126            Arg::new("fixture")
127                .long("with-fixture")
128                .value_parser(clap::value_parser!(PathBuf))
129                .help("file whose contents will be read into the `$fixture` global"),
130        )
131        .arg(Arg::new("programfile").value_parser(clap::value_parser!(PathBuf)))
132        .arg(
133            Arg::new("arguments")
134                .num_args(..)
135                .value_parser(clap::value_parser!(OsString))
136                .trailing_var_arg(true),
137        )
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn verify_cli() {
146        cli().debug_assert();
147    }
148}