parmacl/lib.rs
1#![warn(missing_docs)]
2//! A command line parser for Rust
3//!
4//! The primary purpose of this library is to parse a full command line. That is, a string containing the complete command line.
5//! It can also parse rust environment command line arguments as supplied by Rust (`std::env::Args`) however there are other libraries which
6//! specialise in this and may be a better choice.
7//!
8//! While the Rust standard library does not give access to the full environment command line, this library could be used to
9//! parse internal commands entered within an application.
10//!
11//! See [Features](#features).
12//!
13//! # Usage
14//!
15//! Follow the steps below to parse a command line:
16//! 1. Create an enum with one variant for each type of option argument expected in the command line.
17//! 1. Create an enum with one variant for each type of parameter argument expected in the command line.
18//! 1. Create an instance of [parmacl::Parser](Parser).
19//! 1. If necessary, set relevant properties of the Parser instance to reflect the style of the command line.
20//! 1. Add a [matcher](Matcher) for all possible arguments to the parser. Tag each matcher with the appropriate enum.
21//! 1. Call [Parser.parse_line(command_line)](Parser::parse_line) which will parse the command line and return a result containing
22//! either a [vector of parsed arguments](Args) or an error.
23//! 1. Loop through returned arguments and process each. The arguments are ordered by appearance in the command line.
24//!
25//! ## Example
26//!
27//! ```
28//!use parmacl::{Parser, Arg, RegexOrText, OptionHasValue};
29//!
30//!const LINE: &str = r#""binary name" -a "1st ""Param""" -B optValue "param2" -c "C OptValue""#;
31//!
32//!#[derive(Default)]
33//!enum OptionEnum {
34//! #[default] A,
35//! B,
36//! C,
37//!}
38//!#[derive(Default)]
39//!enum ParamEnum {
40//! #[default] Param1,
41//! Param2,
42//!}
43//!
44//!let mut parser: Parser<OptionEnum, ParamEnum> = Parser::new();
45//!
46//!parser
47//! .push_new_option_matcher("optionA")
48//! .set_option_tag(OptionEnum::A)
49//! .some_option_codes(&[RegexOrText::with_text("a")]);
50//!
51//!parser
52//! .push_new_option_matcher("optionB")
53//! .set_option_tag(OptionEnum::B)
54//! .some_option_codes(&[RegexOrText::with_text("b")])
55//! .set_option_has_value(OptionHasValue::IfPossible);
56//!
57//!parser
58//! .push_new_option_matcher("optionC")
59//! .set_option_tag(OptionEnum::C)
60//! .some_option_codes(&[RegexOrText::with_text("c")])
61//! .set_option_has_value(OptionHasValue::Always);
62//!
63//!parser
64//! .push_new_param_matcher("param1")
65//! .set_param_tag(ParamEnum::Param1)
66//! .some_param_indices(&[0]);
67//!
68//!parser
69//! .push_new_param_matcher("param2")
70//! .set_param_tag(ParamEnum::Param2)
71//! .some_param_indices(&[1]);
72//!
73//!let args = parser.parse_line(LINE).unwrap();
74//!
75//!assert_eq!(args.len(), 6);
76//!
77//!for arg in args {
78//! match arg {
79//! Arg::Binary(properties) => {
80//! assert_eq!(properties.arg_index, 0);
81//! assert_eq!(properties.value_text, "binary name");
82//! assert_eq!(properties.char_index, 0);
83//! },
84//! Arg::Option(properties) => {
85//! match properties.matcher.option_tag() {
86//! OptionEnum::A => {
87//! // Process option A
88//! assert_eq!(properties.matcher.name(), "optionA");
89//! assert_eq!(properties.arg_index, 1);
90//! assert_eq!(properties.option_index, 0);
91//! assert_eq!(properties.code, "a");
92//! assert_eq!(properties.value_text, None);
93//! assert_eq!(properties.char_index, 14);
94//! },
95//! OptionEnum::B => {
96//! // Process option B
97//! },
98//! OptionEnum::C => {
99//! // Process option C
100//! },
101//! }
102//! }
103//! Arg::Param(properties) => {
104//! match properties.matcher.param_tag() {
105//! ParamEnum::Param1 => {
106//! // Process parameter Param1
107//! assert_eq!(properties.matcher.name(), "param1");
108//! assert_eq!(properties.arg_index, 2);
109//! assert_eq!(properties.param_index, 0);
110//! assert_eq!(properties.value_text, "1st \"Param\"");
111//! assert_eq!(properties.char_index, 17);
112//! },
113//! ParamEnum::Param2 => {
114//! // Process parameter Param2
115//! },
116//! }
117//! }
118//! }
119//!}
120//!```
121//! # Parsing environment arguments
122//!
123//! Parmacl can also be used to parse the environment command line arguments passed to an application. The above example could be used to parse
124//! environment arguments with the following changes.
125//!
126//! Instead of using the [new()](Parser::new) constructor function, use the [with_env_args_defaults()](Parser::with_env_args_defaults) constructor.
127//! This will create an instance of Parser with defaults suitable for parsing environment arguments.
128//!```
129//!# use parmacl::{Parser, Arg, RegexOrText, OptionHasValue};
130//!#
131//!# const LINE: &str = r#""binary name" -a "1st ""Param""" -B optValue "param2" -c "C OptValue""#;
132//!#
133//!# #[derive(Default)]
134//!# enum OptionEnum {
135//!# #[default] A,
136//!# B,
137//!# C,
138//!# }
139//!# #[derive(Default)]
140//!# enum ParamEnum {
141//!# #[default] Param1,
142//!# Param2,
143//!# }
144//!#
145//! let mut parser: Parser<OptionEnum, ParamEnum> = Parser::with_env_args_defaults();
146//!#
147//!# parser
148//!# .push_new_option_matcher("optionA")
149//!# .set_option_tag(OptionEnum::A)
150//!# .some_option_codes(&[RegexOrText::with_text("a")]);
151//!#
152//!# parser
153//!# .push_new_option_matcher("optionB")
154//!# .set_option_tag(OptionEnum::B)
155//!# .some_option_codes(&[RegexOrText::with_text("b")])
156//!# .set_option_has_value(OptionHasValue::IfPossible);
157//!#
158//!# parser
159//!# .push_new_option_matcher("optionC")
160//!# .set_option_tag(OptionEnum::C)
161//!# .some_option_codes(&[RegexOrText::with_text("c")])
162//!# .set_option_has_value(OptionHasValue::Always);
163//!#
164//!# parser
165//!# .push_new_param_matcher("param1")
166//!# .set_param_tag(ParamEnum::Param1)
167//!# .some_param_indices(&[0]);
168//!#
169//!# parser
170//!# .push_new_param_matcher("param2")
171//!# .set_param_tag(ParamEnum::Param2)
172//!# .some_param_indices(&[1]);
173//!#
174//!# let args = parser.parse_env().unwrap();
175//!#
176//!# // assert_eq!(args.len(), 6); // commented out to allow documentation tests to pass
177//!#
178//!# for arg in args {
179//!# match arg {
180//!# Arg::Binary(properties) => {
181//!# assert_eq!(properties.arg_index, 0);
182//!# // assert_eq!(properties.value_text, "binary name"); // commented out to allow documentation tests to pass
183//!# assert_eq!(properties.env_line_approximate_char_index, 0);
184//!# },
185//!# Arg::Option(properties) => {
186//!# match properties.matcher.option_tag() {
187//!# OptionEnum::A => {
188//!# // Process option A
189//!# assert_eq!(properties.matcher.name(), "optionA");
190//!# assert_eq!(properties.arg_index, 1);
191//!# assert_eq!(properties.option_index, 0);
192//!# assert_eq!(properties.code, "a");
193//!# assert_eq!(properties.value_text, None);
194//!# assert_eq!(properties.env_line_approximate_char_index, 14);
195//!# },
196//!# OptionEnum::B => {
197//!# // Process option B
198//!# },
199//!# OptionEnum::C => {
200//!# // Process option C
201//!# },
202//!# }
203//!# }
204//!# Arg::Param(properties) => {
205//!# match properties.matcher.param_tag() {
206//!# ParamEnum::Param1 => {
207//!# // Process parameter Param1
208//!# assert_eq!(properties.matcher.name(), "param1");
209//!# assert_eq!(properties.arg_index, 2);
210//!# assert_eq!(properties.param_index, 0);
211//!# assert_eq!(properties.value_text, "1st \"Param\"");
212//!# assert_eq!(properties.env_line_approximate_char_index, 17);
213//!# },
214//!# ParamEnum::Param2 => {
215//!# // Process parameter Param2
216//!# },
217//!# }
218//!# }
219//!# }
220//!# }
221//!```
222//! Use the [parse_env()](Parser::parse_env) function instead of the [parse()](Parser::parse_line) function.
223//!```
224//!# use parmacl::{Parser, Arg, RegexOrText, OptionHasValue};
225//!#
226//!# const LINE: &str = r#""binary name" -a "1st ""Param""" -B optValue "param2" -c "C OptValue""#;
227//!#
228//!# #[derive(Default)]
229//!# enum OptionEnum {
230//!# #[default] A,
231//!# B,
232//!# C,
233//!# }
234//!# #[derive(Default)]
235//!# enum ParamEnum {
236//!# #[default] Param1,
237//!# Param2,
238//!# }
239//!#
240//!# let mut parser: Parser<OptionEnum, ParamEnum> = Parser::with_env_args_defaults();
241//!#
242//!# parser
243//!# .push_new_option_matcher("optionA")
244//!# .set_option_tag(OptionEnum::A)
245//!# .some_option_codes(&[RegexOrText::with_text("a")]);
246//!#
247//!# parser
248//!# .push_new_option_matcher("optionB")
249//!# .set_option_tag(OptionEnum::B)
250//!# .some_option_codes(&[RegexOrText::with_text("b")])
251//!# .set_option_has_value(OptionHasValue::IfPossible);
252//!#
253//!# parser
254//!# .push_new_option_matcher("optionC")
255//!# .set_option_tag(OptionEnum::C)
256//!# .some_option_codes(&[RegexOrText::with_text("c")])
257//!# .set_option_has_value(OptionHasValue::Always);
258//!#
259//!# parser
260//!# .push_new_param_matcher("param1")
261//!# .set_param_tag(ParamEnum::Param1)
262//!# .some_param_indices(&[0]);
263//!#
264//!# parser
265//!# .push_new_param_matcher("param2")
266//!# .set_param_tag(ParamEnum::Param2)
267//!# .some_param_indices(&[1]);
268//!#
269//! let args = parser.parse_env().unwrap();
270//!#
271//!# // assert_eq!(args.len(), 6); // commented out to allow documentation tests to pass
272//!#
273//!# for arg in args {
274//!# match arg {
275//!# Arg::Binary(properties) => {
276//!# assert_eq!(properties.arg_index, 0);
277//!# // assert_eq!(properties.value_text, "binary name"); // commented out to allow documentation tests to pass
278//!# assert_eq!(properties.env_line_approximate_char_index, 0);
279//!# },
280//!# Arg::Option(properties) => {
281//!# match properties.matcher.option_tag() {
282//!# OptionEnum::A => {
283//!# // Process option A
284//!# assert_eq!(properties.matcher.name(), "optionA");
285//!# assert_eq!(properties.arg_index, 1);
286//!# assert_eq!(properties.option_index, 0);
287//!# assert_eq!(properties.code, "a");
288//!# assert_eq!(properties.value_text, None);
289//!# assert_eq!(properties.env_line_approximate_char_index, 14);
290//!# },
291//!# OptionEnum::B => {
292//!# // Process option B
293//!# },
294//!# OptionEnum::C => {
295//!# // Process option C
296//!# },
297//!# }
298//!# }
299//!# Arg::Param(properties) => {
300//!# match properties.matcher.param_tag() {
301//!# ParamEnum::Param1 => {
302//!# // Process parameter Param1
303//!# assert_eq!(properties.matcher.name(), "param1");
304//!# assert_eq!(properties.arg_index, 2);
305//!# assert_eq!(properties.param_index, 0);
306//!# assert_eq!(properties.value_text, "1st \"Param\"");
307//!# assert_eq!(properties.env_line_approximate_char_index, 17);
308//!# },
309//!# ParamEnum::Param2 => {
310//!# // Process parameter Param2
311//!# },
312//!# }
313//!# }
314//!# }
315//!# }
316//!```
317//! Since the shell will already have parsed the command line, and passed the individual arguments to the application, the parser can
318//! only guess the position of each argument in the command line. Use property `env_line_approximate_char_index` instead of `char_index`
319//! in [ParamProperties](ParamProperties) or [OptionProperties](OptionProperties) to get the approximate position of a the argument in
320//! the command line.
321//!```
322//!# use parmacl::{Parser, Arg, RegexOrText, OptionHasValue};
323//!#
324//!# const LINE: &str = r#""binary name" -a "1st ""Param""" -B optValue "param2" -c "C OptValue""#;
325//!#
326//!# #[derive(Default)]
327//!# enum OptionEnum {
328//!# #[default] A,
329//!# B,
330//!# C,
331//!# }
332//!# #[derive(Default)]
333//!# enum ParamEnum {
334//!# #[default] Param1,
335//!# Param2,
336//!# }
337//!#
338//!# let mut parser: Parser<OptionEnum, ParamEnum> = Parser::with_env_args_defaults();
339//!#
340//!# parser
341//!# .push_new_option_matcher("optionA")
342//!# .set_option_tag(OptionEnum::A)
343//!# .some_option_codes(&[RegexOrText::with_text("a")]);
344//!#
345//!# parser
346//!# .push_new_option_matcher("optionB")
347//!# .set_option_tag(OptionEnum::B)
348//!# .some_option_codes(&[RegexOrText::with_text("b")])
349//!# .set_option_has_value(OptionHasValue::IfPossible);
350//!#
351//!# parser
352//!# .push_new_option_matcher("optionC")
353//!# .set_option_tag(OptionEnum::C)
354//!# .some_option_codes(&[RegexOrText::with_text("c")])
355//!# .set_option_has_value(OptionHasValue::Always);
356//!#
357//!# parser
358//!# .push_new_param_matcher("param1")
359//!# .set_param_tag(ParamEnum::Param1)
360//!# .some_param_indices(&[0]);
361//!#
362//!# parser
363//!# .push_new_param_matcher("param2")
364//!# .set_param_tag(ParamEnum::Param2)
365//!# .some_param_indices(&[1]);
366//!#
367//!# let args = parser.parse_env().unwrap();
368//!#
369//!# // assert_eq!(args.len(), 6); // commented out to allow documentation tests to pass
370//!#
371//!# for arg in args {
372//!# match arg {
373//!# Arg::Binary(properties) => {
374//!# assert_eq!(properties.arg_index, 0);
375//!# // assert_eq!(properties.value_text, "binary name"); // commented out to allow documentation tests to pass
376//!# assert_eq!(properties.env_line_approximate_char_index, 0);
377//!# },
378//!# Arg::Option(properties) => {
379//!# match properties.matcher.option_tag() {
380//! OptionEnum::A => {
381//! // Process option A
382//! assert_eq!(properties.matcher.name(), "optionA");
383//! assert_eq!(properties.arg_index, 1);
384//! assert_eq!(properties.option_index, 0);
385//! assert_eq!(properties.code, "a");
386//! assert_eq!(properties.value_text, None);
387//! assert_eq!(properties.env_line_approximate_char_index, 14);
388//! },
389//!# OptionEnum::B => {
390//!# // Process option B
391//!# },
392//!# OptionEnum::C => {
393//!# // Process option C
394//!# },
395//!# }
396//!# }
397//!# Arg::Param(properties) => {
398//!# match properties.matcher.param_tag() {
399//!# ParamEnum::Param1 => {
400//!# // Process parameter Param1
401//!# assert_eq!(properties.matcher.name(), "param1");
402//!# assert_eq!(properties.arg_index, 2);
403//!# assert_eq!(properties.param_index, 0);
404//!# assert_eq!(properties.value_text, "1st \"Param\"");
405//!# assert_eq!(properties.env_line_approximate_char_index, 17);
406//!# },
407//!# ParamEnum::Param2 => {
408//!# // Process parameter Param2
409//!# },
410//!# }
411//!# }
412//!# }
413//!# }
414//!```
415//! # Understanding the command line
416//!
417//! Parmacl considers a command line to have 3 types of arguments
418//! * **Binary name**\
419//! This normally is the first argument in the command line and is normally the path to the application's executable file (binary).
420//! * **Parameters**\
421//! Strings which the application will interpret. Parameters are typically identified by their order in the command line. In the above
422//! [example](#example), `"1st ""Param"""` and `"param2"` are parameters.
423//! * **Options**\
424//! An option is an argument identified by a code. As such it can be placed anywhere in the command line. It can optionally have a value.
425//! If it does not have a value, it behaves like a flag/boolean. In the above [example](#example), `-a` is an option with code `a` that
426//! behaves like a flag. The options `-B optValue` (code `B`) and `-c "C OptValue"` (code `c`) are options with respective values
427//! `optValue` and `C OptValue`.
428//!
429//! Note that these arguments do not necessarily correspond to environment arguments created by a shell and passed to an application.
430//! For example, an option with a value is identified by Parmacl as one argument whereas the shell may identify it as 2 arguments
431//! (depending on what character is used to separate option values from the option code).
432//!
433//! # Overview of parsing
434//!
435//! Before parsing a command line, the parser needs to be configured with the style of the command line. This includes things like
436//! specifying whether parameters and option values can be quoted, whether quotes can be included in quoted parameters and option values,
437//! specifying escaping of special characters.
438//!
439//! It also needs to be configured with a list of [matchers](Matcher). A matcher is a struct with a set of filters. The filters are used
440//! to match arguments identified by the parser. An argument needs to meet all filter conditions in order for it to be 'matched'
441//!
442//! When a command line is parsed, the parser will identify arguments based on its configured style. The arguments are identified in order
443//! from start to end of the command line. As each argument is identified, it is matched against one of the [matchers](Matcher) assigned to
444//! the parser. It is possible for an argument to match more than one matcher. The parser will attempt to match each argument to matchers
445//! in order of matchers in the parser's matcher list. Accordingly, more specific matchers should be inserted earlier in this list.
446//!
447//! When an argument is matched, the parser generates a corresponding [Arg](Arg) variant with details of the argument. For parameter and
448//! option arguments, the variant is also assigned a copy of either the respective Matcher's [param_tag](Matcher::param_tag) or
449//! [option_tag](Matcher::option_tag) value.
450//!
451//! These [Arg](Arg) variants are stored in an array (in same order as the arguments in the command line) which is returned as the success
452//! result.
453//!
454//! All arguments must be matched. If an argument is not matched, then the user specified an unsupported parameter or option and an
455//! [unmatched parameter](ParseErrorTypeId::UnmatchedParam) or [unmatched option](ParseErrorTypeId::UnmatchedOption) will be returned.
456//!
457//! If the parser detects an error in the command line, an error result will be returned containing a [ParseError](ParseError) struct.
458//! This struct [identifies](ParseErrorTypeId) the reason for the error and where in the line the error occurred.
459//!
460//! # Main types
461//!
462//! * [Parser](Parser)\
463//! The main object. To parse a command line, create an instance of this, set its properties to reflect the style of the command line,
464//! assign matchers and then call one its parse functions. The result will either be the array of arguments or an error object.
465//! * [Matcher](Matcher)\
466//! Each argument must be matched against a matcher. Typically one matcher is created for each argument however matchers can also
467//! be used to match multiple arguments.
468//! * [Arg](Arg)\
469//! An enum with 3 variants: Binary, Param, Option. The Parser's parse functions return an array of these variants - each of which
470//! identify an argument the parser found in the command line.
471//! * [ArgProperties](ArgProperties)\
472//! A trait shared by structs [BinaryProperties](BinaryProperties), [ParamProperties](ParamProperties) and
473//! [OptionProperties](OptionProperties). Instances of these structs are associated with the respective [Arg](Arg) variants returned
474//! by the parse function and provide details about each identified argument.
475//! * [RegexOrText](RegexOrText)\
476//! A struct representing either a Regex or text (string). An instance of RegexOrText can be assigned to the
477//! [option_codes](Matcher::option_codes) or [value_text](Matcher::value_text) Matcher filter properties and determines whether the
478//! filtering is by text or Regex.
479//! * [ParseError](ParseError)\
480//! The struct returned with an Error result from a parse function. Specifies the type of error and where in the line the error
481//! occurred.
482//!
483//! # Features
484//!
485//! * Command line parsing
486//! * Environment arguments parsing
487//! * Command Line Style
488//! * Specify which character(s) can be used to quote parameters and option values
489//! * Specify which character(s) can be used to announce an option
490//! * Specify which character(s) can be used to announce an option value (space character can be included)
491//! * Specify which character(s) will terminate parsing of a command line
492//! * Case sensitivity when matching parameters, option codes and option values
493//! * Whether options with code that have more than one character, require 2 announcer characters (eg --anOpt)
494//! * Use double quotes to embed quote characters within quoted parameters and option values
495//! * Use escaping to include characters with special meaning
496//! * Whether first argument in command line is the binary's name/path
497//! * Argument Matching
498//! * Parameter or Option
499//! * Argument indices
500//! * Parameter indices
501//! * Parameter text (string or Regex)
502//! * Option indices
503//! * Option codes (string or Regex)
504//! * Whether option has value (None, IfPossible, Always)
505//! * Option value text (string or Regex)
506//! * Whether option value can start with an option announcer character
507//! * Tag parameters and options arguments with with any enum (or any other type) from matcher for easy identification
508//! * Parse error result has properties detailing the type of error and where it occurred.
509
510
511
512#![allow(clippy::collapsible_else_if)]
513
514mod env_char;
515mod parse_error_type_id;
516mod parse_error;
517mod regex_or_text;
518mod matcher;
519mod arg;
520mod parser;
521
522mod parse_state;
523
524pub use parse_error_type_id::{
525 ParseErrorTypeId,
526};
527
528pub use parse_error::{
529 ParseError,
530};
531
532pub use regex_or_text::{
533 RegexOrText,
534};
535
536pub use matcher:: {
537 Matcher,
538 Matchers,
539 DefaultTagType,
540 OptionHasValue,
541 MatchArgTypeId,
542 DEFAULT_OPTION_HAS_VALUE,
543};
544
545pub use arg::{
546 ArgProperties,
547 BinaryProperties,
548 OptionProperties,
549 ParamProperties,
550 Arg,
551 Args,
552};
553
554pub use parser::{
555 Parser,
556 EscapeableLogicalChar,
557 DEFAULT_LINE_QUOTE_CHARS,
558 DEFAULT_LINE_OPTION_ANNOUNCER_CHARS,
559 DEFAULT_LINE_OPTION_CODES_CASE_SENSITIVE,
560 DEFAULT_LINE_MULTI_CHAR_OPTION_CODE_REQUIRES_DOUBLE_ANNOUNCER,
561 DEFAULT_LINE_OPTION_VALUE_ANNOUNCER_CHARS,
562 DEFAULT_LINE_OPTION_VALUES_CASE_SENSITIVE,
563 DEFAULT_LINE_PARAMS_CASE_SENSITIVE,
564 DEFAULT_LINE_EMBED_QUOTE_CHAR_WITH_DOUBLE,
565 DEFAULT_LINE_ESCAPE_CHAR,
566 DEFAULT_LINE_ESCAPEABLE_LOGICAL_CHARS,
567 DEFAULT_LINE_ESCAPEABLE_CHARS,
568 DEFAULT_LINE_PARSE_TERMINATE_CHARS,
569 DEFAULT_LINE_FIRST_ARG_IS_BINARY,
570 DEFAULT_ENV_ARGS_QUOTE_CHARS,
571 DEFAULT_ENV_ARGS_OPTION_ANNOUNCER_CHARS,
572 DEFAULT_ENV_ARGS_OPTION_CODES_CASE_SENSITIVE,
573 DEFAULT_ENV_ARGS_OPTION_CODE_CAN_BE_EMPTY,
574 DEFAULT_ENV_ARGS_MULTI_CHAR_OPTION_CODE_REQUIRES_DOUBLE_ANNOUNCER,
575 DEFAULT_ENV_ARGS_OPTION_VALUE_ANNOUNCER_CHARS,
576 DEFAULT_ENV_ARGS_OPTION_VALUES_CASE_SENSITIVE,
577 DEFAULT_ENV_ARGS_PARAMS_CASE_SENSITIVE,
578 DEFAULT_ENV_ARGS_EMBED_QUOTE_CHAR_WITH_DOUBLE,
579 DEFAULT_ENV_ARGS_ESCAPE_CHAR,
580 DEFAULT_ENV_ARGS_ESCAPEABLE_LOGICAL_CHARS,
581 DEFAULT_ENV_ARGS_ESCAPEABLE_CHARS,
582 DEFAULT_ENV_ARGS_PARSE_TERMINATE_CHARS,
583 DEFAULT_ENV_ARGS_FIRST_ARG_IS_BINARY,
584};