Skip to main content

commandlines/parsers/
mod.rs

1// Copyright 2018 Christopher Simpkins
2// Licensed under the MIT license
3
4//! Command line string parsing support
5
6use std::collections::HashMap;
7
8/// Returns `Vec<String>` of command line option arguments in a command line string.
9pub fn parse_options(argv: &[String]) -> Vec<String> {
10    let mut options: Vec<String> = Vec::new();
11    for arg in argv {
12        if arg.starts_with('-') {
13            // test to confirm that this is not a single hyphen argument
14            // Per POSIX guidelines, the single hyphen is used to represent
15            // stdin/stdout and should not be parsed as an option
16            if arg == "-" {
17                continue;
18            }
19            // test to confirm that we haven't encountered a double
20            // hyphen command line argument
21            // Per POSIX guidelines, the double hyphen indicates that all
22            // subsequent argument parsing for options should be ignored
23            if is_double_hyphen_option(&arg[..]) {
24                break;
25            }
26            // test for a definition formatted option (e.g., `--option=definition`)
27            // parse to option and definition parts if identified
28            if is_definition_option(&arg[..]) {
29                let option_definition_vec = get_definition_parts(&arg[..]);
30                let option = &option_definition_vec[0];
31                options.push(option.to_string());
32            } else {
33                options.push(arg.clone());
34            }
35        }
36    }
37
38    options
39}
40
41/// Returns `std::collections::HashMap<String, String>` with key:value mapped as option:definition.
42pub fn parse_definitions(argv: &[String]) -> HashMap<String, String> {
43    let mut definitions: HashMap<String, String> = HashMap::new();
44    for arg in argv {
45        if arg.starts_with('-') {
46            // test to confirm that we haven't encountered a double
47            // hyphen command line option as this indicates that all
48            // subsequent argument parsing for options should be ignored
49            if is_double_hyphen_option(&arg[..]) {
50                break;
51            }
52            // test for a definition formatted option (e.g., `--option=definition`)
53            // parse to option and definition parts if identified
54            if is_definition_option(&arg[..]) {
55                let option_definition_vec = get_definition_parts(&arg[..]);
56                let option = &option_definition_vec[0];
57                let definition = &option_definition_vec[1];
58                definitions.insert(option.to_string(), definition.to_string());
59            }
60        }
61    }
62
63    definitions
64}
65
66/// Returns `Option<String>` with the first positional argument to the executable.
67/// Returns `None` if the command was entered as the executable only.
68pub fn parse_first_arg(arg_list: &[String]) -> Option<String> {
69    match arg_list.get(1) {
70        Some(x) => Some(x.clone()),
71        None => None,
72    }
73}
74
75/// Returns `Option<String>` with the last positional argument to the executable.
76/// Returns `None` if the command was entered as the executable only.
77pub fn parse_last_arg(arg_list: &[String]) -> Option<String> {
78    if arg_list.len() > 1 {
79        match arg_list.get(arg_list.len() - 1) {
80            Some(x) => Some(x.clone()),
81            None => None,
82        }
83    } else {
84        None // return None if this is an executable only (e.g. only includes index position 0 with length = 1) command
85    }
86}
87
88/// Returns `Options<Vec<String>>` with Vector of arguments following a double hyphen `--` command line argument idiom.
89/// Returns `None` if there was no double hyphen idiom present or there are no arguments following the double hyphen argument.
90pub fn parse_double_hyphen_args(arg_list: &[String]) -> Option<Vec<String>> {
91    for (index, value) in arg_list.iter().enumerate() {
92        if is_double_hyphen_option(&value[..]) {
93            let sub_vec = arg_list[(index + 1)..arg_list.len()].to_vec();
94            if !sub_vec.is_empty() {
95                return Some(sub_vec);
96            } else {
97                return None;
98            }
99        }
100    }
101
102    None
103}
104
105/// Returns `Option<Vec<String>>` that includes unique short options parsed from the command arguments, including any multi-option short syntax options.
106/// Returns `None` if there were no short options in the command
107///
108/// # Remarks
109/// Note that this function is not UTF-8 compliant and enforces a stricter character set definition than other areas of the library. It will not properly parse multi-option short syntax options that include characters outside of the Unicode Basic Latin set.  This function supports the multi-option short syntax argument style defined in the POSIX guidelines (i.e., POSIX option strings should include only characters in the alphanumeric subset of the Unicode Basic Latin set).
110pub fn parse_mops(arg_list: &[String]) -> Option<Vec<String>> {
111    let mut return_vec: Vec<String> = Vec::new();
112
113    // iterate through the option argument list argument
114    for arg in arg_list {
115        if !arg.starts_with("--") {
116            // exclude long option format
117            let t = &arg[..];
118            for x in t.chars() {
119                // iterate through characters in short options
120                if x != '-' {
121                    // assume any character in a short option is a unique option
122                    let option_string = format!("-{}", x); // format as `-[x]` for storage
123                    return_vec.push(option_string);
124                }
125            }
126        }
127    }
128
129    // if there were no short options parsed with this function, return `None`
130    if return_vec.is_empty() {
131        None
132    } else {
133        Some(return_vec)
134    }
135}
136
137/// Returns boolean for the question "Is `needle` a definition option?".
138///
139/// # Remarks
140/// A definition option is defined as a command line argument that includes
141/// an equal symbol to define the option argument (e.g., `--name=SomeGuy`).
142pub fn is_definition_option(needle: &str) -> bool {
143    needle.contains('=')
144}
145
146/// Returns boolean for the question "Is `needle` a double hyphen option?".
147///
148/// # Remarks
149/// The `--` command line idiom is used to indicate that arguments following this indicator should not be parsed as options.
150pub fn is_double_hyphen_option(needle: &str) -> bool {
151    needle == "--"
152}
153
154/// Returns boolean for the question "Is `needle` a multi-option short syntax (mops) style option argument?"
155pub fn is_mops_option(needle: &str) -> bool {
156    // must have single hyphen syntax with more than one option character
157    needle.starts_with('-') && !needle.starts_with("--") && needle.len() > 2
158}
159
160/// Returns `Vec<String>` of definition option parts with two index positions.
161///
162/// These index position definitions are:
163/// * index position `0`: option argument String (i.e., before the equal symbol)
164/// * index position `1`: definition argument String (i.e., after the equal symbol)
165pub fn get_definition_parts(needle: &str) -> Vec<String> {
166    let opt_def: Vec<_> = needle.split('=').collect();
167    vec![String::from(opt_def[0]), String::from(opt_def[1])]
168}
169
170// Tests
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn function_parse_options() {
177        let test_vec = vec![
178            String::from("tester"),
179            String::from("subcommand"),
180            String::from("-o"),
181            String::from("spacedefinition"),
182            String::from("--longoption"),
183            String::from("-"), // should not be parsed as `-` not an option
184            String::from("--defoption=equaldefinition"),
185            String::from("--"),
186            String::from("--afterdoublehyphen"), // should not be parsed as option as follows `--`
187            String::from("-x"),                  // should not be parsed as option as follows `--`
188            String::from("lastpos"),
189        ];
190
191        let expected_vec = vec![
192            String::from("-o"),
193            String::from("--longoption"),
194            String::from("--defoption"),
195        ];
196
197        assert!(parse_options(&test_vec) == expected_vec);
198    }
199
200    #[test]
201    fn function_parse_options_no_options() {
202        let test_vec = vec![
203            String::from("tester"),
204            String::from("subcommand"),
205            String::from("-"), // should not be parsed as `-` not an option
206            String::from("spacedefinition"),
207            String::from("--"),
208            String::from("--afterdoublehyphen"), // should not be parsed as option as follows `--`
209            String::from("-x"),                  // should not be parsed as option as follows `--`
210            String::from("lastpos"),
211        ];
212
213        let expected_vec: Vec<String> = vec![];
214
215        assert!(parse_options(&test_vec) == expected_vec);
216    }
217
218    #[test]
219    fn function_parse_definitions_single_def() {
220        let test_vec = vec![
221            String::from("tester"),
222            String::from("subcommand"),
223            String::from("-o"),
224            String::from("spacedefinition"),
225            String::from("--longoption"),
226            String::from("--defoption=equaldefinition"),
227            String::from("--"),
228            String::from("--output=filepath"), // should not be parsed as option because follows `--`
229            String::from("--afterdoublehyphen"), // should not be parsed as option because follows `--`
230            String::from("-x"), // should not be parsed as option because follows `--`
231            String::from("lastpos"),
232        ];
233
234        let mut expected_hm = HashMap::new();
235        expected_hm.insert("--defoption".to_string(), "equaldefinition".to_string());
236
237        assert_eq!(parse_definitions(&test_vec), expected_hm);
238    }
239
240    #[test]
241    fn function_parse_definitions_multi_def() {
242        let test_vec = vec![
243            String::from("tester"),
244            String::from("subcommand"),
245            String::from("-o"),
246            String::from("spacedefinition"),
247            String::from("--longoption"),
248            String::from("--defoption=equaldefinition"),
249            String::from("--another=anotherdef"),
250            String::from("--"),
251            String::from("--output=filepath"), // should not be parsed as option because follows `--`
252            String::from("--afterdoublehyphen"), // should not be parsed as option because follows `--`
253            String::from("-x"), // should not be parsed as option because follows `--`
254            String::from("lastpos"),
255        ];
256
257        let mut expected_hm = HashMap::new();
258        expected_hm.insert("--defoption".to_string(), "equaldefinition".to_string());
259        expected_hm.insert("--another".to_string(), "anotherdef".to_string());
260
261        assert_eq!(parse_definitions(&test_vec), expected_hm);
262    }
263
264    #[test]
265    fn function_parse_first_arg() {
266        let test_vec = vec![
267            String::from("test"),
268            String::from("subcmd"),
269            String::from("arg"),
270        ];
271        assert_eq!(parse_first_arg(&test_vec), Some(String::from("subcmd")));
272    }
273
274    #[test]
275    fn function_parse_first_arg_executable_only() {
276        let test_vec = vec![String::from("test")];
277        assert_eq!(parse_first_arg(&test_vec), None);
278    }
279
280    #[test]
281    fn function_parse_last_arg() {
282        let test_vec = vec![
283            String::from("test"),
284            String::from("subcmd"),
285            String::from("arg"),
286        ];
287        assert_eq!(parse_last_arg(&test_vec), Some(String::from("arg")));
288    }
289
290    #[test]
291    fn function_parse_last_arg_executable_only() {
292        let test_vec = vec![String::from("test")];
293        assert_eq!(parse_last_arg(&test_vec), None);
294    }
295
296    #[test]
297    fn function_parse_double_hyphen_args() {
298        let test_vec = vec![
299            String::from("test"),
300            String::from("-o"),
301            String::from("path"),
302            String::from("--"),
303            String::from("--this"),
304            String::from("--that"),
305        ];
306        let expected_vec = vec![String::from("--this"), String::from("--that")];
307        assert_eq!(parse_double_hyphen_args(&test_vec), Some(expected_vec));
308    }
309
310    #[test]
311    fn function_parse_double_hyphen_args_no_post_args() {
312        let test_vec = vec![
313            String::from("test"),
314            String::from("-o"),
315            String::from("path"),
316            String::from("--"),
317        ];
318        assert_eq!(parse_double_hyphen_args(&test_vec), None);
319    }
320
321    #[test]
322    fn function_parse_double_hyphen_args_no_double_hyphen() {
323        let test_vec = vec![
324            String::from("test"),
325            String::from("-o"),
326            String::from("path"),
327        ];
328        assert_eq!(parse_double_hyphen_args(&test_vec), None);
329    }
330
331    #[test]
332    fn function_parse_mops() {
333        let test_vec = vec![
334            String::from("--lng"),
335            String::from("-hij"),
336            String::from("-o"),
337        ];
338
339        // should include all short options with multi-option short syntax options parsed to unique individual option items
340        let expected_vec = vec![
341            String::from("-h"),
342            String::from("-i"),
343            String::from("-j"),
344            String::from("-o"),
345        ];
346
347        assert_eq!(parse_mops(&test_vec), Some(expected_vec));
348    }
349
350    #[test]
351    fn function_parse_mops_without_mops() {
352        let test_vec = vec![
353            String::from("--lng"),
354            String::from("--hij"),
355            String::from("--output"),
356        ];
357
358        assert_eq!(parse_mops(&test_vec), None);
359    }
360
361    #[test]
362    fn function_is_definition_option_true() {
363        let true_defintion = "--option=definition";
364        assert_eq!(is_definition_option(true_defintion), true);
365    }
366
367    #[test]
368    fn function_is_definition_option_false() {
369        let false_definition = "--option";
370        assert_eq!(is_definition_option(false_definition), false);
371    }
372
373    #[test]
374    fn function_get_definition_parts() {
375        let definition_string = "--option=definition";
376        let expected: Vec<String> = vec![String::from("--option"), String::from("definition")];
377        assert!(get_definition_parts(definition_string) == expected);
378    }
379
380    #[test]
381    fn function_is_double_hyphen_option_true() {
382        let true_definition = "--";
383        assert_eq!(is_double_hyphen_option(true_definition), true);
384    }
385
386    #[test]
387    fn function_is_double_hyphen_option_false() {
388        let false_definition_1 = "--help";
389        let false_definition_2 = "-s";
390        let false_definition_3 = "subcmd";
391        assert_eq!(is_double_hyphen_option(false_definition_1), false);
392        assert_eq!(is_double_hyphen_option(false_definition_2), false);
393        assert_eq!(is_double_hyphen_option(false_definition_3), false);
394    }
395
396    #[test]
397    fn function_is_mops_option() {
398        let true_definition = "-mpn";
399        let false_definition_1 = "-o";
400        let false_definition_2 = "--output";
401        let false_definition_3 = "command";
402        let false_definition_4 = "--";
403        let false_definition_5 = "-";
404
405        assert_eq!(is_mops_option(true_definition), true);
406        assert_eq!(is_mops_option(false_definition_1), false);
407        assert_eq!(is_mops_option(false_definition_2), false);
408        assert_eq!(is_mops_option(false_definition_3), false);
409        assert_eq!(is_mops_option(false_definition_4), false);
410        assert_eq!(is_mops_option(false_definition_5), false);
411    }
412}