1 // Copyright 2017 The Chromium OS Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 //! Handles argument parsing.
6 //!
7 //! # Example
8 //!
9 //! ```
10 //! const ARGUMENTS: &'static [Argument] = &[
11 //! Argument::positional("FILES", "files to operate on"),
12 //! Argument::short_value('p', "program", "PROGRAM", "Program to apply to each file"),
13 //! Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)"),
14 //! Argument::flag("unmount", "Unmount the root"),
15 //! Argument::short_flag('h', "help", "Print help message."),
16 //! ];
17 //!
18 //! let match_res = set_arguments(args, ARGUMENTS, |name, value| {
19 //! match name {
20 //! "" => println!("positional arg! {}", value.unwrap()),
21 //! "program" => println!("gonna use program {}", value.unwrap()),
22 //! "cpus" => {
23 //! let v: u32 = value.unwrap().parse().map_err(|_| {
24 //! Error::InvalidValue {
25 //! value: value.unwrap().to_owned(),
26 //! expected: "this value for `cpus` needs to be integer",
27 //! }
28 //! })?;
29 //! }
30 //! "unmount" => println!("gonna unmount"),
31 //! "help" => return Err(Error::PrintHelp),
32 //! _ => unreachable!(),
33 //! }
34 //! }
35 //!
36 //! match match_res {
37 //! Ok(_) => println!("running with settings"),
38 //! Err(Error::PrintHelp) => print_help("best_program", "FILES", ARGUMENTS),
39 //! Err(e) => println!("{}", e),
40 //! }
41 //! ```
42
43 use std::fmt::{self, Display};
44 use std::result;
45
46 /// An error with argument parsing.
47 #[derive(Debug)]
48 pub enum Error {
49 /// There was a syntax error with the argument.
50 Syntax(String),
51 /// The argumen's name is unused.
52 UnknownArgument(String),
53 /// The argument was required.
54 ExpectedArgument(String),
55 /// The argument's given value is invalid.
56 InvalidValue {
57 value: String,
58 expected: &'static str,
59 },
60 /// The argument was already given and none more are expected.
61 TooManyArguments(String),
62 /// The argument expects a value.
63 ExpectedValue(String),
64 /// The help information was requested
65 PrintHelp,
66 }
67
68 impl Display for Error {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result69 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70 use self::Error::*;
71
72 match self {
73 Syntax(s) => write!(f, "syntax error: {}", s),
74 UnknownArgument(s) => write!(f, "unknown argument: {}", s),
75 ExpectedArgument(s) => write!(f, "expected argument: {}", s),
76 InvalidValue { value, expected } => {
77 write!(f, "invalid value {:?}: {}", value, expected)
78 }
79 TooManyArguments(s) => write!(f, "too many arguments: {}", s),
80 ExpectedValue(s) => write!(f, "expected parameter value: {}", s),
81 PrintHelp => write!(f, "help was requested"),
82 }
83 }
84 }
85
86 /// Result of a argument parsing.
87 pub type Result<T> = result::Result<T, Error>;
88
89 /// Information about an argument expected from the command line.
90 ///
91 /// # Examples
92 ///
93 /// To indicate a flag style argument:
94 ///
95 /// ```
96 /// Argument::short_flag('f', "flag", "enable awesome mode")
97 /// ```
98 ///
99 /// To indicate a parameter style argument that expects a value:
100 ///
101 /// ```
102 /// // "VALUE" and "NETMASK" are placeholder values displayed in the help message for these
103 /// // arguments.
104 /// Argument::short_value('v', "val", "VALUE", "how much do you value this usage information")
105 /// Argument::value("netmask", "NETMASK", "hides your netface")
106 /// ```
107 ///
108 /// To indicate an argument with no short version:
109 ///
110 /// ```
111 /// Argument::flag("verbose", "this option is hard to type quickly")
112 /// ```
113 ///
114 /// To indicate a positional argument:
115 ///
116 /// ```
117 /// Argument::positional("VALUES", "these are positional arguments")
118 /// ```
119 #[derive(Default)]
120 pub struct Argument {
121 /// The name of the value to display in the usage information. Use None to indicate that there
122 /// is no value expected for this argument.
123 pub value: Option<&'static str>,
124 /// Optional single character shortened argument name.
125 pub short: Option<char>,
126 /// The long name of this argument.
127 pub long: &'static str,
128 /// Helpfuly usage information for this argument to display to the user.
129 pub help: &'static str,
130 }
131
132 impl Argument {
positional(value: &'static str, help: &'static str) -> Argument133 pub fn positional(value: &'static str, help: &'static str) -> Argument {
134 Argument {
135 value: Some(value),
136 long: "",
137 help,
138 ..Default::default()
139 }
140 }
141
value(long: &'static str, value: &'static str, help: &'static str) -> Argument142 pub fn value(long: &'static str, value: &'static str, help: &'static str) -> Argument {
143 Argument {
144 value: Some(value),
145 long,
146 help,
147 ..Default::default()
148 }
149 }
150
short_value( short: char, long: &'static str, value: &'static str, help: &'static str, ) -> Argument151 pub fn short_value(
152 short: char,
153 long: &'static str,
154 value: &'static str,
155 help: &'static str,
156 ) -> Argument {
157 Argument {
158 value: Some(value),
159 short: Some(short),
160 long,
161 help,
162 }
163 }
164
flag(long: &'static str, help: &'static str) -> Argument165 pub fn flag(long: &'static str, help: &'static str) -> Argument {
166 Argument {
167 long,
168 help,
169 ..Default::default()
170 }
171 }
172
short_flag(short: char, long: &'static str, help: &'static str) -> Argument173 pub fn short_flag(short: char, long: &'static str, help: &'static str) -> Argument {
174 Argument {
175 short: Some(short),
176 long,
177 help,
178 ..Default::default()
179 }
180 }
181 }
182
parse_arguments<I, R, F>(args: I, mut f: F) -> Result<()> where I: Iterator<Item = R>, R: AsRef<str>, F: FnMut(&str, Option<&str>) -> Result<()>,183 fn parse_arguments<I, R, F>(args: I, mut f: F) -> Result<()>
184 where
185 I: Iterator<Item = R>,
186 R: AsRef<str>,
187 F: FnMut(&str, Option<&str>) -> Result<()>,
188 {
189 enum State {
190 // Initial state at the start and after finishing a single argument/value.
191 Top,
192 // The remaining arguments are all positional.
193 Positional,
194 // The next string is the value for the argument `name`.
195 Value { name: String },
196 }
197 let mut s = State::Top;
198 for arg in args {
199 let arg = arg.as_ref();
200 s = match s {
201 State::Top => {
202 if arg == "--" {
203 State::Positional
204 } else if arg.starts_with("--") {
205 let param = arg.trim_start_matches('-');
206 if param.contains('=') {
207 let mut iter = param.splitn(2, '=');
208 let name = iter.next().unwrap();
209 let value = iter.next().unwrap();
210 if name.is_empty() {
211 return Err(Error::Syntax(
212 "expected parameter name before `=`".to_owned(),
213 ));
214 }
215 if value.is_empty() {
216 return Err(Error::Syntax(
217 "expected parameter value after `=`".to_owned(),
218 ));
219 }
220 f(name, Some(value))?;
221 State::Top
222 } else if let Err(e) = f(param, None) {
223 if let Error::ExpectedValue(_) = e {
224 State::Value {
225 name: param.to_owned(),
226 }
227 } else {
228 return Err(e);
229 }
230 } else {
231 State::Top
232 }
233 } else if arg.starts_with('-') {
234 if arg.len() == 1 {
235 return Err(Error::Syntax(
236 "expected argument short name after `-`".to_owned(),
237 ));
238 }
239 let name = &arg[1..2];
240 let value = if arg.len() > 2 { Some(&arg[2..]) } else { None };
241 if let Err(e) = f(name, value) {
242 if let Error::ExpectedValue(_) = e {
243 State::Value {
244 name: name.to_owned(),
245 }
246 } else {
247 return Err(e);
248 }
249 } else {
250 State::Top
251 }
252 } else {
253 f("", Some(&arg))?;
254 State::Positional
255 }
256 }
257 State::Positional => {
258 f("", Some(&arg))?;
259 State::Positional
260 }
261 State::Value { name } => {
262 f(&name, Some(&arg))?;
263 State::Top
264 }
265 };
266 }
267 Ok(())
268 }
269
270 /// Parses the given `args` against the list of know arguments `arg_list` and calls `f` with each
271 /// present argument and value if required.
272 ///
273 /// This function guarantees that only valid long argument names from `arg_list` are sent to the
274 /// callback `f`. It is also guaranteed that if an arg requires a value (i.e.
275 /// `arg.value.is_some()`), the value will be `Some` in the callbacks arguments. If the callback
276 /// returns `Err`, this function will end parsing and return that `Err`.
277 ///
278 /// See the [module level](index.html) example for a usage example.
set_arguments<I, R, F>(args: I, arg_list: &[Argument], mut f: F) -> Result<()> where I: Iterator<Item = R>, R: AsRef<str>, F: FnMut(&str, Option<&str>) -> Result<()>,279 pub fn set_arguments<I, R, F>(args: I, arg_list: &[Argument], mut f: F) -> Result<()>
280 where
281 I: Iterator<Item = R>,
282 R: AsRef<str>,
283 F: FnMut(&str, Option<&str>) -> Result<()>,
284 {
285 parse_arguments(args, |name, value| {
286 let mut matches = None;
287 for arg in arg_list {
288 if let Some(short) = arg.short {
289 if name.len() == 1 && name.starts_with(short) {
290 if value.is_some() != arg.value.is_some() {
291 return Err(Error::ExpectedValue(short.to_string()));
292 }
293 matches = Some(arg.long);
294 }
295 }
296 if matches.is_none() && arg.long == name {
297 if value.is_some() != arg.value.is_some() {
298 return Err(Error::ExpectedValue(arg.long.to_owned()));
299 }
300 matches = Some(arg.long);
301 }
302 }
303 match matches {
304 Some(long) => f(long, value),
305 None => Err(Error::UnknownArgument(name.to_owned())),
306 }
307 })
308 }
309
310 /// Prints command line usage information to stdout.
311 ///
312 /// Usage information is printed according to the help fields in `args` with a leading usage line.
313 /// The usage line is of the format "`program_name` [ARGUMENTS] `required_arg`".
print_help(program_name: &str, required_arg: &str, args: &[Argument])314 pub fn print_help(program_name: &str, required_arg: &str, args: &[Argument]) {
315 println!(
316 "Usage: {} {}{}\n",
317 program_name,
318 if args.is_empty() { "" } else { "[ARGUMENTS] " },
319 required_arg
320 );
321 if args.is_empty() {
322 return;
323 }
324 println!("Argument{}:", if args.len() > 1 { "s" } else { "" });
325 for arg in args {
326 match arg.short {
327 Some(s) => print!(" -{}, ", s),
328 None => print!(" "),
329 }
330 if arg.long.is_empty() {
331 print!(" ");
332 } else {
333 print!("--");
334 }
335 print!("{:<12}", arg.long);
336 if let Some(v) = arg.value {
337 if arg.long.is_empty() {
338 print!(" ");
339 } else {
340 print!("=");
341 }
342 print!("{:<10}", v);
343 } else {
344 print!("{:<11}", "");
345 }
346 println!("{}", arg.help);
347 }
348 }
349
350 #[cfg(test)]
351 mod tests {
352 use super::*;
353
354 #[test]
request_help()355 fn request_help() {
356 let arguments = [Argument::short_flag('h', "help", "Print help message.")];
357
358 let match_res = set_arguments(["-h"].iter(), &arguments[..], |name, _| {
359 match name {
360 "help" => return Err(Error::PrintHelp),
361 _ => unreachable!(),
362 };
363 });
364 match match_res {
365 Err(Error::PrintHelp) => {}
366 _ => unreachable!(),
367 }
368 }
369
370 #[test]
mixed_args()371 fn mixed_args() {
372 let arguments = [
373 Argument::positional("FILES", "files to operate on"),
374 Argument::short_value('p', "program", "PROGRAM", "Program to apply to each file"),
375 Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)"),
376 Argument::flag("unmount", "Unmount the root"),
377 Argument::short_flag('h', "help", "Print help message."),
378 ];
379
380 let mut unmount = false;
381 let match_res = set_arguments(
382 ["--cpus", "3", "--program", "hello", "--unmount", "file"].iter(),
383 &arguments[..],
384 |name, value| {
385 match name {
386 "" => assert_eq!(value.unwrap(), "file"),
387 "program" => assert_eq!(value.unwrap(), "hello"),
388 "cpus" => {
389 let c: u32 = value.unwrap().parse().map_err(|_| Error::InvalidValue {
390 value: value.unwrap().to_owned(),
391 expected: "this value for `cpus` needs to be integer",
392 })?;
393 assert_eq!(c, 3);
394 }
395 "unmount" => unmount = true,
396 "help" => return Err(Error::PrintHelp),
397 _ => unreachable!(),
398 };
399 Ok(())
400 },
401 );
402 assert!(match_res.is_ok());
403 assert!(unmount);
404 }
405
406 #[test]
name_value_pair()407 fn name_value_pair() {
408 let arguments = [Argument::short_value(
409 'c',
410 "cpus",
411 "N",
412 "Number of CPUs to use. (default: 1)",
413 )];
414 let match_res = set_arguments(
415 ["-c", "5", "--cpus", "5", "-c5", "--cpus=5"].iter(),
416 &arguments[..],
417 |name, value| {
418 assert_eq!(name, "cpus");
419 assert_eq!(value, Some("5"));
420 Ok(())
421 },
422 );
423 assert!(match_res.is_ok());
424 }
425 }
426