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