arrrg/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::str::FromStr;
4
5use getopts::Fail;
6
7//////////////////////////////////////////// CommandLine ///////////////////////////////////////////
8
9/// [CommandLine] creates a command line parser for anyone who implements [CommandLine::add_opts],
10/// [CommandLine::matches] and [CommandLine::canonical_command_line].  This is a wrapper around
11/// getopts to tie together options and matches.
12pub trait CommandLine: Sized + Default + Eq + PartialEq {
13    /// Add options to the getopts parser.
14    fn add_opts(&self, prefix: Option<&str>, opts: &mut getopts::Options);
15
16    /// Assign values to self using the provided getopts matches.
17    fn matches(&mut self, prefix: Option<&str>, matches: &getopts::Matches);
18
19    /// Return the canonical command line for this [CommandLine].
20    fn canonical_command_line(&self, prefix: Option<&str>) -> Vec<String>;
21
22    /// Parse from the command line.  This function will panic if a non-canonical command line is
23    /// provided.
24    fn from_command_line(usage: &str) -> (Self, Vec<String>) {
25        let args: Vec<String> = std::env::args().collect();
26        let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();
27        Self::from_arguments(usage, &args[1..])
28    }
29
30    /// Parse from the command line.  This function will allow a non-canonical command line to
31    /// execute.
32    fn from_command_line_relaxed(usage: &str) -> (Self, Vec<String>) {
33        let args: Vec<String> = std::env::args().collect();
34        let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();
35        Self::from_arguments_relaxed(usage, &args[1..])
36    }
37
38    /// Parse from the provided arguments.  This function will panic if a non-canonical command
39    /// line is provided.
40    fn from_arguments(usage: &str, args: &[&str]) -> (Self, Vec<String>) {
41        let (command_line, free) = Self::from_arguments_relaxed(usage, args);
42        let mut reconstructed_args = command_line.canonical_command_line(None);
43        let mut free_p = free.clone();
44        reconstructed_args.append(&mut free_p);
45        let mut args = args.to_vec();
46        args.retain(|a| *a != "--");
47        reconstructed_args.retain(|a| *a != "--");
48        if args != reconstructed_args {
49            panic!(
50                "non-canonical commandline specified:
51provided: {:?}
52expected: {:?}
53check argument order amongst other differences",
54                &args, reconstructed_args
55            );
56        }
57        (command_line, free)
58    }
59
60    /// Parse from the provided arguments.  This function will allow a non-canonical command line to
61    /// execute.
62    fn from_arguments_relaxed(usage: &str, args: &[&str]) -> (Self, Vec<String>) {
63        let mut command_line = Self::default();
64        let mut opts = getopts::Options::new();
65        opts.parsing_style(getopts::ParsingStyle::StopAtFirstFree);
66        opts.long_only(true);
67        opts.optflag("h", "help", "Print this help menu.");
68        command_line.add_opts(None, &mut opts);
69
70        let matches = match opts.parse(args) {
71            Ok(matches) => matches,
72            Err(Fail::OptionMissing(which)) => {
73                Self::error(&mut command_line, format!("missing argument: --{which}"));
74                Self::usage(&mut command_line, opts, usage);
75                return (command_line, vec![]);
76            }
77            Err(err) => {
78                Self::error(
79                    &mut command_line,
80                    format!("could not parse command line: {err}"),
81                );
82                Self::exit(&mut command_line, 64);
83                return (command_line, vec![]);
84            }
85        };
86        if matches.opt_present("h") {
87            Self::usage(&mut command_line, opts, usage);
88            return (command_line, vec![]);
89        }
90        command_line.matches(None, &matches);
91        let free: Vec<String> = matches.free.to_vec();
92        (command_line, free)
93    }
94
95    /// Display the usage and exit 1.
96    fn usage(&mut self, opts: getopts::Options, brief: &str) {
97        self.error(opts.usage(brief));
98        self.exit(1);
99    }
100
101    /// Report an error.
102    fn error(&mut self, msg: impl AsRef<str>) {
103        eprintln!("{}", msg.as_ref());
104    }
105
106    /// Exit with the provided status.
107    fn exit(&mut self, status: i32) {
108        std::process::exit(status);
109    }
110}
111
112///////////////////////////////////////// NoExitCommandLine ////////////////////////////////////////
113
114/// A non-exiting wrapper for command line parsing.  Will store command line in 0, messages in
115/// element 1, exit status in 2.
116#[derive(Default, Eq, PartialEq)]
117pub struct NoExitCommandLine<T: CommandLine>(T, Vec<String>, i32);
118
119impl<T: CommandLine> AsRef<T> for NoExitCommandLine<T> {
120    fn as_ref(&self) -> &T {
121        &self.0
122    }
123}
124
125impl<T: CommandLine> NoExitCommandLine<T> {
126    pub fn into_inner(self) -> T {
127        self.0
128    }
129
130    pub fn into_parts(self) -> (T, Vec<String>, i32) {
131        (self.0, self.1, self.2)
132    }
133}
134
135impl<T: CommandLine> CommandLine for NoExitCommandLine<T> {
136    fn add_opts(&self, prefix: Option<&str>, opts: &mut getopts::Options) {
137        self.0.add_opts(prefix, opts);
138    }
139
140    fn matches(&mut self, prefix: Option<&str>, matches: &getopts::Matches) {
141        self.0.matches(prefix, matches);
142    }
143
144    fn canonical_command_line(&self, prefix: Option<&str>) -> Vec<String> {
145        self.0.canonical_command_line(prefix)
146    }
147
148    fn error(&mut self, msg: impl AsRef<str>) {
149        self.1.push(msg.as_ref().to_string());
150    }
151
152    fn exit(&mut self, status: i32) {
153        self.2 = status;
154    }
155}
156
157//////////////////////////////////////////// macro utils ///////////////////////////////////////////
158
159#[doc(hidden)]
160pub fn getopt_str(prefix: Option<&str>, field_arg: &str) -> String {
161    match prefix {
162        Some(prefix) => {
163            format!("{prefix}-{field_arg}")
164        }
165        None => field_arg.to_string(),
166    }
167}
168
169#[doc(hidden)]
170pub fn dashed_str(prefix: Option<&str>, field_arg: &str) -> String {
171    format!("--{}", getopt_str(prefix, field_arg))
172}
173
174#[doc(hidden)]
175pub fn parse_field<T>(arg_str: &str, s: &str) -> T
176where
177    T: FromStr,
178    <T as FromStr>::Err: std::fmt::Display,
179{
180    match s.parse::<T>() {
181        Ok(t) => t,
182        Err(err) => {
183            panic!("field --{arg_str} is unparseable: {err}");
184        }
185    }
186}