cli_parser_helper/
lib.rs

1#![allow(dead_code)]
2
3use std::{collections::HashMap, ops::Index};
4
5#[derive(Debug)]
6struct Header {
7    text: String,
8}
9
10#[derive(Debug)]
11struct Footer {
12    text: String,
13}
14
15impl Header {
16    fn new(text: String) -> Header {
17        Header { text }
18    }
19}
20
21impl Footer {
22    fn new(text: String) -> Footer {
23        Footer { text }
24    }
25}
26
27type Name = String;
28
29#[derive(Debug)]
30struct CliOption {
31    long_form: Option<String>,
32    short_form: Option<String>,
33    help_text: String,
34    name: Name,
35}
36
37#[derive(Debug)]
38struct OptionValue {
39    is_enabled: bool,
40    values: Vec<String>,
41}
42
43impl OptionValue {
44    fn is_enabled(&self) -> bool {
45        self.is_enabled
46    }
47
48    fn values(&self) -> &Vec<String> {
49        &self.values
50    }
51}
52
53/// Struct to parse the option arguments
54/// Ignores invalid arguments passed
55pub struct CliOptionParser {
56    name_to_cli_option_map: HashMap<Name, CliOption>,
57    name_to_option_value_map: HashMap<Name, OptionValue>,
58    short_form_to_name_map: HashMap<String, Name>,
59    long_form_to_name_map: HashMap<String, Name>,
60    empty_option_list: Vec<String>,
61    header: Header,
62    footer: Footer,
63}
64
65impl Index<&str> for CliOptionParser {
66    type Output = Vec<String>;
67
68    fn index(&self, index: &str) -> &Self::Output {
69        self.get_option_values(index)
70    }
71}
72
73impl CliOptionParser {
74    /// Gives a new instance of CliOptionParser
75    pub fn new(header: String, footer: String) -> CliOptionParser {
76        CliOptionParser {
77            name_to_cli_option_map: HashMap::new(),
78            name_to_option_value_map: HashMap::new(),
79            short_form_to_name_map: HashMap::new(),
80            long_form_to_name_map: HashMap::new(),
81            empty_option_list: vec![],
82            header: Header { text: header },
83            footer: Footer { text: footer },
84        }
85    }
86
87    /// sets the enabled flag to true for the flag `option`
88    fn enable_option(&mut self, option_name: String) {
89        let option_value_entry = self
90            .name_to_option_value_map
91            .entry(option_name.to_string())
92            .or_insert(OptionValue {
93                is_enabled: false,
94                values: vec![],
95            });
96
97        option_value_entry.is_enabled = true;
98    }
99
100    /// appends the value given to the corresponding flag `option`
101    fn add_value_to_option(&mut self, option_name: String, value: String) {
102        let option_value_entry = self
103            .name_to_option_value_map
104            .entry(option_name.to_string())
105            .or_insert(OptionValue {
106                is_enabled: false,
107                values: vec![],
108            });
109
110        option_value_entry.values.push(value);
111        option_value_entry.is_enabled = true;
112    }
113
114    fn parse_from(&mut self, args: Vec<String>) -> Vec<String> {
115        let mut arguments: Vec<String> = vec![];
116
117        for arg in args {
118            if arg.starts_with("--") {
119                if arg.contains("=") {
120                    // example --hello=world
121                    let mut arg_split = arg.split("=");
122                    let option = arg_split.next().unwrap(); // --hello
123                    let value = arg_split.next().unwrap(); // world
124
125                    if !self.long_form_to_name_map.contains_key(option) {
126                        continue;
127                    }
128
129                    let option_name = &self.long_form_to_name_map[option];
130
131                    // add the value to option
132                    self.add_value_to_option(option_name.to_string(), value.to_string());
133                } else {
134                    if !self.long_form_to_name_map.contains_key(&arg) {
135                        continue;
136                    }
137
138                    let option_name = &self.long_form_to_name_map[&arg];
139                    self.enable_option(option_name.to_string());
140                }
141            } else if arg.starts_with("-") {
142                // if length is greater than 1, like -lHelloWorld
143                if arg.len() > 1 {
144                    let (option, value) = arg.split_at(2); // left = -l, right = HelloWorld
145                    if !self.short_form_to_name_map.contains_key(option) {
146                        continue;
147                    }
148                    let option_name = &self.short_form_to_name_map[option];
149                    self.add_value_to_option(option_name.to_string(), value.to_string());
150                } else {
151                    if !self.short_form_to_name_map.contains_key(&arg) {
152                        continue;
153                    }
154
155                    let option_name = &self.short_form_to_name_map[&arg];
156                    self.enable_option(option_name.to_string());
157                }
158            } else {
159                arguments.push(arg);
160            }
161        }
162
163        arguments
164    }
165
166    /// function to parse and then returns the normal arguments
167    pub fn parse(&mut self) -> Vec<String> {
168        // function to parse
169        self.parse_from(std::env::args().collect())
170    }
171
172    /// Checks if an option with the given `name` is present/enabled
173    pub fn is_enabled(&self, name: &str) -> bool {
174        // return false if invalid option
175        if !self.name_to_option_value_map.contains_key(name) {
176            return false;
177        }
178
179        self.name_to_option_value_map[name].is_enabled()
180    }
181
182    /// if an option with the given `name` is enabled, returns the reference to value list
183    /// else returns an empty list
184    pub fn get_option_values(&self, name: &str) -> &Vec<String> {
185        if !self.is_enabled(name) {
186            return &self.empty_option_list;
187        }
188
189        &self.name_to_option_value_map[name].values()
190    }
191
192    /// Registers an Option with the given short form, long form, help text with the name `name`
193    /// the option can be accessed by using the `name`
194    pub fn register_option(
195        &mut self,
196        short_form: Option<String>,
197        long_form: Option<String>,
198        help_text: &str,
199        name: &str,
200    ) {
201        let help_text = help_text.to_string();
202        let name = name.to_string();
203
204        if self.name_to_cli_option_map.contains_key(&name) {
205            panic!("name : {name} already registered as option");
206        }
207
208        if short_form.is_none() && long_form.is_none() {
209            panic!("Both long form and short form cannot be none for name : {name}");
210        }
211
212        if let Some(short_form_name) = short_form.as_ref() {
213            if self
214                .short_form_to_name_map
215                .contains_key(&short_form_name.clone())
216            {
217                panic!("Short form of {short_form_name} already defined");
218            }
219
220            self.short_form_to_name_map
221                .insert(short_form_name.clone(), name.clone());
222        }
223
224        if let Some(long_form_name) = long_form.as_ref() {
225            if self
226                .long_form_to_name_map
227                .contains_key(&long_form_name.clone())
228            {
229                panic!("Short form of {long_form_name} already defined");
230            }
231
232            self.long_form_to_name_map
233                .insert(long_form_name.clone(), name.clone());
234        }
235
236        self.name_to_cli_option_map.insert(
237            name.clone(),
238            CliOption {
239                long_form,
240                short_form,
241                help_text,
242                name: name.clone(),
243            },
244        );
245
246        self.name_to_option_value_map.insert(
247            name.clone(),
248            OptionValue {
249                is_enabled: false,
250                values: vec![],
251            },
252        );
253    }
254
255    /// function to return help text when asked
256    pub fn help_text(&self) -> String {
257        // display help text
258        let mut help_text = format!("{}\n\n", self.header.text);
259
260        for (_, cli_option) in &self.name_to_cli_option_map {
261            match cli_option.short_form.as_ref() {
262                Some(short) => help_text += &format!("{}    ", short),
263                None => help_text += &format!("     "),
264            }
265
266            match cli_option.long_form.as_ref() {
267                Some(long) => help_text += &format!("{}    ", long),
268                None => help_text += &format!("     "),
269            }
270
271            help_text += &format!("     {}\n", cli_option.help_text.replace('\n', "\n\t\t\t"));
272        }
273
274        help_text += "\n";
275
276        help_text += &format!("{}\n\n", self.footer.text);
277
278        help_text
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn check_parsing() {
288        let mut cli_option_parser =
289            CliOptionParser::new("header".to_string(), "footer".to_string());
290        cli_option_parser.register_option(
291            Some("-c".to_string()),
292            Some("--count".to_string()),
293            "print only count of selected lines",
294            "count",
295        );
296
297        cli_option_parser.register_option(
298            Some("-C".to_string()),
299            Some("--context".to_string()),
300            "print NUM lines of output contex when\n given with --context=NUM",
301            "context",
302        );
303
304        let mock_args = vec![
305            "program_name".to_string(),
306            "-c123".to_string(),
307            "--count=456".to_string(),
308            "-C456".to_string(),
309            "--context=712".to_string(),
310        ];
311
312        let arguments = cli_option_parser.parse_from(mock_args);
313
314        assert_eq!(arguments, vec!["program_name"]);
315
316        assert!(cli_option_parser.is_enabled("count"));
317        assert!(cli_option_parser.is_enabled("context"));
318
319        assert!(cli_option_parser["count"].len() == 2);
320        assert_eq!(cli_option_parser["count"], vec!["123", "456"]);
321
322        assert!(cli_option_parser["context"].len() == 2);
323        assert_eq!(cli_option_parser["context"], vec!["456", "712"]);
324
325        let help_text = cli_option_parser.help_text();
326
327        assert!(help_text.contains("header"));
328        assert!(help_text.contains("footer"));
329        assert!(help_text.contains("-c    --count         print only count of selected lines"));
330        assert!(help_text.contains("-C    --context         print NUM lines of output contex when\n\t\t\t given with --context=NUM"));
331    }
332}