cmd_args/
parser.rs

1use std::collections::HashMap;
2use std::{result, env};
3use std::rc::Rc;
4use crate::error::ParserError;
5use crate::{Group, HelpEntry, HelpPrinter};
6use crate::option;
7use crate::arg;
8use crate::help::DefaultHelpPrinter;
9
10/// Type alias for parser results.
11pub type Result<T> = result::Result<T, ParserError>;
12
13static OPTION_PREFIX: char = '-';
14static OPTION_KEY_VALUE_SPLIT: char = '=';
15static HELP_OPTION: &str = "help";
16static HELP_OPTION_ALIAS: &str = "?";
17
18/// Options to customize the parser.
19pub struct ParseOptions {
20    /// Specify a custom help printer or the default one will be used.
21    pub help_printer: Option<Box<dyn HelpPrinter>>,
22}
23
24/// Parse from env::args() using the passed group.
25pub fn parse(group: Group, options: Option<ParseOptions>) -> Result<()> {
26    let args: Vec<String> = env::args().collect();
27    let args: Vec<&str> = args.iter().map(AsRef::as_ref).collect();
28
29    parse_from(group, &args[..], options)
30}
31
32/// Parse the passed command line arguments using the passed group.
33pub fn parse_from(group: Group, args: &[&str], options: Option<ParseOptions>) -> Result<()> {
34    let group = Rc::new(group);
35
36    let (ctx_group, anticipated_options, parse_start_pos) = prepare_parsing_context(Rc::clone(&group), args)?;
37    let arg_descriptors = ctx_group.get_arguments();
38
39    let option_descriptor_lookup = prepare_option_descriptor_lookup(&anticipated_options)?;
40
41    let (raw_options, raw_arguments) = split_raw_arguments(&args[parse_start_pos..], &option_descriptor_lookup)?;
42
43    let mut option_value_lookup = parse_options(raw_options, &option_descriptor_lookup)?;
44    fill_default_options(&mut option_value_lookup, &anticipated_options);
45
46    // Show help if specified as option
47    if let option::Value::Bool { value } = option_value_lookup.get(HELP_OPTION).unwrap() {
48        if *value {
49            show_help(
50                &ctx_group,
51                &anticipated_options,
52                arg_descriptors,
53                if options.is_some() { options.unwrap().help_printer } else { None },
54            );
55            return Ok(());
56        }
57    }
58
59    let argument_values = parse_arguments(arg_descriptors, raw_arguments)?;
60
61    // Call group consumer.
62    ctx_group.get_consumer()(&argument_values, &option_value_lookup);
63    Ok(())
64}
65
66/// Prepare the parsing context for the passed group and arguments.
67/// Returns the group context, anticipated options to parse as well as the rest of the raw
68/// command line arguments to parse.
69fn prepare_parsing_context<'a>(group: Rc<Group>, args: &[&str]) -> Result<(Rc<Group>, HashMap<Rc<String>, Rc<option::Descriptor>>, usize)> {
70    let mut anticipated_options: HashMap<Rc<String>, Rc<option::Descriptor>> = HashMap::new();
71
72    // Add help option to anticipated options.
73    let help_option_descriptor = option::Descriptor::new(HELP_OPTION, option::Type::Bool { default: false }, "Get this information displayed")
74        .add_alias(HELP_OPTION_ALIAS);
75    anticipated_options.insert(help_option_descriptor.take_name(), Rc::new(help_option_descriptor));
76
77    // Save root groups options.
78    for (option_name, option_descriptor) in group.get_options() {
79        anticipated_options.insert(Rc::clone(option_name), Rc::clone(option_descriptor));
80    }
81
82    // Find command context (via specified groups).
83    let mut cur_group = group;
84    let mut args_pos = 1;
85
86    for arg in &args[1..] {
87        let arg = *arg;
88
89        match cur_group.get_child_known_for(arg) {
90            Some(v) => {
91                cur_group = v;
92
93                // Save current groups options.
94                for (option_name, option_descriptor) in cur_group.get_options() {
95                    if anticipated_options.contains_key(option_name) {
96                        return Err(ParserError {
97                            message: format!("Option '{}' declared multiple times in group specifications", option_name)
98                        });
99                    }
100                    anticipated_options.insert(Rc::clone(option_name), Rc::clone(option_descriptor));
101                }
102            }
103            None => break // Command context path found
104        };
105
106        args_pos += 1;
107    }
108
109    Ok((cur_group, anticipated_options, args_pos))
110}
111
112/// Prepare a lookup to find option descriptors by their name or alias.
113fn prepare_option_descriptor_lookup(anticipated_options: &HashMap<Rc<String>, Rc<option::Descriptor>>) -> Result<HashMap<&String, &option::Descriptor>> {
114    let mut option_descriptor_lookup = HashMap::new();
115
116    for (option_name, option_descriptor) in anticipated_options {
117        if option_descriptor_lookup.contains_key(option_name.as_ref()) {
118            return Err(ParserError {
119                message: format!("Option name or alias '{}' specified more than once", option_name.as_ref()),
120            });
121        }
122        option_descriptor_lookup.insert(option_name.as_ref(), option_descriptor.as_ref());
123
124        for alias in option_descriptor.get_aliases() {
125            if option_descriptor_lookup.contains_key(alias) {
126                return Err(ParserError {
127                    message: format!("Option name or alias '{}' specified more than once", alias),
128                });
129            }
130            option_descriptor_lookup.insert(alias, option_descriptor.as_ref());
131        }
132    }
133
134    Ok(option_descriptor_lookup)
135}
136
137/// Get the option descriptor for the passed option name.
138fn get_option_descriptor_for_name<'a>(option_name: &str, option_descriptor_lookup: &HashMap<&String, &'a option::Descriptor>) -> Result<&'a option::Descriptor> {
139    match option_descriptor_lookup.get(&String::from(option_name)) {
140        Some(o) => Ok(*o),
141        None => Err(ParserError {
142            message: format!("Option '--{}' is unknown in the command context", option_name)
143        })
144    }
145}
146
147/// Check whether the passed raw argument string is a option.
148fn is_option(raw_arg: &str) -> bool {
149    raw_arg.starts_with(OPTION_PREFIX)
150}
151
152/// Split the passed raw command line arguments into options (name and value) and arguments.
153fn split_raw_arguments<'a>(args: &[&'a str], option_descriptor_lookup: &HashMap<&String, &option::Descriptor>) -> Result<(HashMap<&'a str, &'a str>, Vec<&'a str>)> {
154    let mut raw_options = HashMap::new();
155    let mut raw_arguments = Vec::new();
156
157    let mut skip_next = false;
158    for i in 0..args.len() {
159        if skip_next {
160            skip_next = false;
161            continue;
162        }
163
164        let arg = args[i];
165
166        if is_option(arg) {
167            let raw_option = arg.trim_start_matches(OPTION_PREFIX); // Strip leading '-' chars
168
169            let is_key_value_option = arg.contains(OPTION_KEY_VALUE_SPLIT);
170            let (option_name, option_value) = if is_key_value_option {
171                // Value is in same string separated by '='
172                let parts: Vec<&str> = raw_option.split(OPTION_KEY_VALUE_SPLIT).collect();
173                (parts[0], parts[1])
174            } else {
175                // Value is in next raw command line argument
176                let next_arg = if args.len() > i + 1 { Some(&args[i + 1]) } else { None };
177
178                let is_option_without_value = next_arg.is_none()
179                    || is_option(next_arg.unwrap());
180
181                let option_value = if is_option_without_value {
182                    // Option without value! Only allowed for boolean options.
183                    let option_type = get_option_descriptor_for_name(raw_option, option_descriptor_lookup)?.value_type();
184
185                    match option_type {
186                        option::Type::Bool { default: _ } => "true",
187                        _ => return Err(ParserError {
188                            message: format!("Encountered option '{}' without value that is not of type boolean. Specify a value for the option.", raw_option)
189                        })
190                    }
191                } else {
192                    next_arg.unwrap()
193                };
194
195                skip_next = true; // Skip the next raw command line argument since it was already processed
196
197                (raw_option, option_value)
198            };
199
200            raw_options.insert(option_name, option_value);
201        } else {
202            raw_arguments.push(arg);
203        }
204    }
205
206    Ok((raw_options, raw_arguments))
207}
208
209/// Parse raw options to their actual values.
210fn parse_options<'a>(raw_options: HashMap<&str, &str>, option_descriptor_lookup: &HashMap<&String, &'a option::Descriptor>) -> Result<HashMap<&'a str, option::Value>> {
211    let mut option_value_lookup: HashMap<&str, option::Value> = HashMap::new();
212
213    for (option_name, raw_value) in raw_options.into_iter() {
214        let (option_name, option_value) = parse_option(option_name, raw_value, option_descriptor_lookup)?;
215        option_value_lookup.insert(option_name, option_value);
216    }
217
218    Ok(option_value_lookup)
219}
220
221/// Parse the passed option (name and raw value).
222fn parse_option<'a>(name: &str, raw_value: &str, option_descriptor_lookup: &HashMap<&String, &'a option::Descriptor>) -> Result<(&'a String, option::Value)> {
223    let option_descriptor = get_option_descriptor_for_name(name, option_descriptor_lookup)?;
224
225    Ok((option_descriptor.name(), match option::Value::parse(option_descriptor.value_type(), raw_value) {
226        Ok(v) => v,
227        Err(_) => return Err(ParserError {
228            message: format!("Expected value '{}' of option '--{}' to be of type '{}'", raw_value, name, option_descriptor.value_type())
229        })
230    }))
231}
232
233/// Add all missing options in the lookup with default values.
234fn fill_default_options<'a>(option_value_lookup: &mut HashMap<&'a str, option::Value>, anticipated_options: &'a HashMap<Rc<String>, Rc<option::Descriptor>>) {
235    for (option_name, descriptor) in anticipated_options {
236        if !option_value_lookup.contains_key(option_name as &str) {
237            option_value_lookup.insert(option_name, option::Value::from_default(descriptor.value_type()));
238        }
239    }
240}
241
242/// Parse the passed raw command line arguments to their actual argument values.
243fn parse_arguments(descriptors: &Vec<arg::Descriptor>, raw_arguments: Vec<&str>) -> Result<Vec<arg::Value>> {
244    if raw_arguments.len() != descriptors.len() {
245        return Err(ParserError {
246            message: format!("Expected to have {} arguments but got {}", descriptors.len(), raw_arguments.len())
247        });
248    }
249
250    let mut argument_values = Vec::with_capacity(raw_arguments.len());
251    for i in 0..raw_arguments.len() {
252        let desc = &descriptors[i];
253        let arg = raw_arguments[i];
254
255        // Check if argument is parsable using the argument descriptor information
256        let value = match arg::Value::parse(desc.value_type(), arg) {
257            Ok(v) => v,
258            Err(_) => return Err(ParserError {
259                message: format!("Expected argument '{}' at position {} to be of type '{}'", arg, i + 1, desc.value_type())
260            })
261        };
262
263        argument_values.push(value);
264    }
265
266    Ok(argument_values)
267}
268
269/// Show help for the passed group configuration.
270fn show_help(group: &Group, option_descriptors: &HashMap<Rc<String>, Rc<option::Descriptor>>, arg_descriptors: &Vec<arg::Descriptor>, help_printer: Option<Box<dyn HelpPrinter>>) {
271    // Collect subcommand entries
272    let mut subcommand_entries = Vec::with_capacity(group.get_children().len());
273    for (group_name, group) in group.get_children() {
274        subcommand_entries.push(HelpEntry {
275            key: group_name,
276            value: group,
277        });
278    }
279    subcommand_entries.sort_by(|a, b| a.key.cmp(b.key));
280
281    // Collect option entries
282    let mut option_entries = Vec::with_capacity(option_descriptors.len());
283    for (option_name, option_descriptor) in option_descriptors {
284        option_entries.push(HelpEntry {
285            key: option_name,
286            value: option_descriptor,
287        });
288    }
289    option_entries.sort_by(|a, b| a.key.cmp(b.key));
290
291    match help_printer {
292        Some(v) => v.print(group, &subcommand_entries, &option_entries, arg_descriptors),
293        None => DefaultHelpPrinter {}.print(group, &subcommand_entries, &option_entries, arg_descriptors),
294    }
295}