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
53pub 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 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 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 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 let mut arg_split = arg.split("=");
122 let option = arg_split.next().unwrap(); let value = arg_split.next().unwrap(); 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 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 arg.len() > 1 {
144 let (option, value) = arg.split_at(2); 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 pub fn parse(&mut self) -> Vec<String> {
168 self.parse_from(std::env::args().collect())
170 }
171
172 pub fn is_enabled(&self, name: &str) -> bool {
174 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 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 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 pub fn help_text(&self) -> String {
257 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}