Skip to main content

argot_cli/
arg_parser.rs

1use std::collections::HashMap;
2use std::io;
3
4#[cfg(feature = "serde")]
5use serde::Serialize;
6
7use crate::types::{ConfigEntry, ConfigEntries, LabeledEntry, OptionValue};
8
9#[derive(Clone, Debug, PartialEq)]
10#[cfg_attr(feature = "serde", derive(Serialize))]
11pub struct ParseResult {
12    options: HashMap<String, OptionValue>,
13    parameters: HashMap<String, String>,
14    operands: Vec<String>,
15}
16
17impl ParseResult {
18    pub fn options(&self) -> &HashMap<String, OptionValue> {
19        &self.options
20    }
21
22    pub fn parameters(&self) -> &HashMap<String, String> {
23        &self.parameters
24    }
25
26    pub fn operands(&self) -> &[String] {
27        &self.operands
28    }
29}
30
31pub struct ParserConfig {
32    configs: HashMap<String, ConfigEntry>,
33}
34
35impl ParserConfig {
36    pub fn new(entries: ConfigEntries) -> io::Result<Self> {
37        let size: usize = entries.len();
38        let mut aliases: Vec<(String, String)> = Vec::with_capacity(size);
39        let mut configs = HashMap::with_capacity(size);
40        match entries {
41            ConfigEntries::Map(map) => {
42                for (option, config) in map {
43                    configs.insert(option.to_string(), config.clone());
44                    if let ConfigEntry::Alias { target } = config {
45                        aliases.push((option.to_string(), target.to_string()));
46                    }
47                }
48            },
49            ConfigEntries::List(list) => {
50                for LabeledEntry { option, entry } in list {
51                    configs.insert(option.to_string(), entry.clone());
52                    if let ConfigEntry::Alias { target } = entry {
53                        aliases.push((option.to_string(), target.to_string()));
54                    }
55                }
56            },
57        };
58
59        for (name, target) in aliases {
60            if !configs.contains_key(&target) {
61                let kind = io::ErrorKind::InvalidData;
62                let msg = format!(
63                    "target value '{}' for option '{}' was not found",
64                    target,
65                    name
66                );
67                return Err(io::Error::new(kind, msg));
68            }
69        }
70
71        Ok(Self { configs })
72    }
73
74    pub fn into_inner(self) -> HashMap<String, ConfigEntry> {
75        let Self { configs } = self;
76        configs
77    }
78}
79
80pub struct ArgParser {
81    configs: HashMap<String, ConfigEntry>,
82}
83
84#[derive(Debug, Clone, PartialEq)]
85enum CliArg {
86    /// A GNU-style long option
87    Long { name: String, value: Option<String> },
88
89    /// A Unix-style short option
90    Short { flags: String },
91
92    /// A name=value parameter assignment
93    Parameter(String, String),
94
95    /// A positional argument
96    Operand,
97}
98
99fn get_opt_value(arg: &str) -> CliArg {
100    if arg == "--" {
101        CliArg::Operand
102    } else if let Some(stripped) = arg.strip_prefix("--") {
103        let mut parts = stripped.splitn(2, '=');
104        let name = parts.next().unwrap_or_default().to_string();
105        let value = parts.next().map(|v| v.to_string());
106        if name.is_empty() {
107            CliArg::Operand
108        } else {
109            CliArg::Long { name, value }
110        }
111    } else if let Some(flags) = arg.strip_prefix('-') {
112        CliArg::Short { flags: flags.to_string() }
113    } else {
114        let mut parts = arg.splitn(2, '=');
115        let name: String = parts.next().unwrap_or_default().to_string();
116        if let Some(value) = parts.next().map(|v: &str| v.to_string()) {
117            CliArg::Parameter(name, value)
118        } else {
119            CliArg::Operand
120        }
121    }
122}
123
124impl ArgParser {
125    pub fn new(configs: ParserConfig) -> Self {
126        let configs = configs.into_inner();
127        Self { configs }
128    }
129
130    pub fn parse<I, S>(&self, arg_list: I) -> Result<ParseResult, String>
131    where
132        I: IntoIterator<Item = S>,
133        S: AsRef<str>,
134    {
135        let mut options: HashMap<String, OptionValue> = HashMap::new();
136        let mut parameters: HashMap<String, String> = HashMap::new();
137        let mut operands: Vec<String> = Vec::new();
138
139        let mut stop_parsing = false;
140        let mut args = arg_list.into_iter().peekable();
141
142        while let Some(arg) = args.next() {
143            let arg: &str = arg.as_ref();
144
145            if arg == "--" && !stop_parsing {
146                stop_parsing = true;
147                continue;
148            }
149
150            if stop_parsing {
151                operands.push(arg.into());
152                continue;
153            }
154
155            match get_opt_value(arg) {
156                CliArg::Short { flags } => {
157                    let (skip, mut pairs) = self
158                        .parse_short_option(&flags, args.peek())?;
159
160                    for (name, value) in pairs.drain() {
161                        match self.configs.get(&name) {
162                            Some(ConfigEntry::Count) => {
163                                match options.get_mut(&name) {
164                                    Some(OptionValue::Int(old)) => {
165                                        if let OptionValue::Int(new) = value {
166                                            *old += new;
167                                        } else {
168                                            todo!();
169                                        };
170                                    },
171                                    None => {
172                                        options.insert(name, value);
173                                    },
174                                    _ => { todo!(); },
175                                }
176                            },
177                            Some(ConfigEntry::List { .. }) => {
178                                match options.get_mut(&name) {
179                                    Some(OptionValue::List(old)) => {
180                                        if let OptionValue::List(new) = value {
181                                            old.extend_from_slice(&new);
182                                        } else {
183                                            todo!();
184                                        };
185                                    },
186                                    None => {
187                                        options.insert(name, value);
188                                    },
189                                    _ => { todo!(); },
190                                }
191                            },
192                            _ => {
193                                options.insert(name, value);
194                            }
195                        }
196                    }
197
198                    if skip {
199                        let _ = args.next();
200                    }
201                },
202                CliArg::Long { name, value } => {
203                    let (name, value) = self
204                        .parse_long_option(&name, value.as_deref())?;
205                    match self.configs.get(name) {
206                        Some(ConfigEntry::Count) => {
207                            match options.get_mut(name) {
208                                Some(OptionValue::Int(old_value)) => {
209                                    if let OptionValue::Int(new_value) = value {
210                                        *old_value += new_value;
211                                    } else {
212                                        todo!(); /* parsed value is not int */
213                                    };
214                                },
215                                None => {
216                                    options.insert(name.into(), value);
217                                },
218                                _ => { todo!() /* stored value is not int */ },
219                            }
220                        },
221                        Some(ConfigEntry::List { .. }) => {
222                            match options.get_mut(name) {
223                                Some(OptionValue::List(old_value)) => {
224                                    if let OptionValue::List(new_value) = value {
225                                        old_value.extend_from_slice(&new_value);
226                                    } else {
227                                        todo!(); /* parsed value is not list */
228                                    };
229                                },
230                                None => {
231                                    options.insert(name.into(), value);
232                                },
233                                _ => { todo!() /* stored value is not list */ },
234                            }
235                        },
236                        /* other entry types */ _ => {
237                            options.insert(name.into(), value);
238                        },
239                    }
240                },
241                CliArg::Parameter(name, value) => {
242                    parameters.insert(name, value);
243                },
244                CliArg::Operand => {
245                    operands.push(arg.into());
246                },
247            }
248        }
249
250        Ok(ParseResult {
251            options,
252            parameters,
253            operands,
254        })
255    }
256
257    fn parse_long_option<'parser, 'option, 'result>(
258        &'parser self,
259        name: &'option str,
260        value: Option<&str>,
261    ) -> Result<(&'result str, OptionValue), String>
262    where
263        'parser: 'result,
264        'option: 'result,
265    {
266        macro_rules! flag_option {
267            ($name:ident) => {{ Ok(($name, OptionValue::Flag)) }}
268        }
269
270        macro_rules! text_option {
271            ($name:ident, $value:ident, $default:ident) => {{
272                if let Some(text) = $value {
273                    Ok(($name, OptionValue::Text(text.into())))
274                } else if let Some(text) = $default {
275                    Ok(($name, OptionValue::Text(text.into())))
276                } else {
277                    Err("null arg".into())
278                }
279            }}
280        }
281
282        macro_rules! int_option {
283            ($name:ident, $value:ident, $default:ident) => {{
284                match $value {
285                    Some(text) if !text.is_empty() => {
286                        if let Ok(num) = text.parse::<i64>() {
287                            Ok(($name, OptionValue::Int(num)))
288                        } else {
289                            Err("invalid int".into())
290                        }
291                    },
292                    _ => {
293                        if let Some(num) = $default {
294                            Ok(($name, OptionValue::Int(*num)))
295                        } else {
296                            Err("null int".into())
297                        }
298                    }
299                }
300            }}
301        }
302
303        macro_rules! count_option {
304            ($name:ident, $val:ident) => {{
305                if $val.is_some() && $val.unwrap().parse::<i64>().is_err() {
306                    Err("invalid int".into())
307                } else if let Some(text) = $val {
308                    let num = text.parse().unwrap();
309                    Ok(($name, OptionValue::Int(num)))
310                } else {
311                    Ok(($name, OptionValue::Int(1)))
312                }
313            }}
314        }
315
316        macro_rules! list_option {
317            ($name:ident, $value:ident, $sep:ident) => {{
318                if $value.is_some() && $value.unwrap().is_empty() {
319                    Ok(($name, OptionValue::List(Vec::new())))
320                } else if let Some(text) = $value {
321                    let sep: &str = $sep.as_deref().unwrap_or(",");
322                    let list: Vec<String> = text
323                        .split(sep)
324                        .map(|item: &str| item.to_string())
325                        .collect();
326                    Ok(($name, OptionValue::List(list)))
327                } else {
328                    Err("null arg".into())
329                }
330            }}
331        }
332
333        if let Some(entry) = self.configs.get(name) {
334            match entry {
335                ConfigEntry::Flag => flag_option!(name),
336                ConfigEntry::Text { default } => {
337                    text_option!(name, value, default)
338                },
339                ConfigEntry::Int { default } => {
340                    int_option!(name, value, default)
341                },
342                ConfigEntry::Count => count_option!(name, value),
343                ConfigEntry::List { sep } => {
344                    list_option!(name, value, sep)
345                },
346                ConfigEntry::Alias { target } => {
347                    if let Some(target_entry) = self.configs.get(target) {
348                        match target_entry {
349                            ConfigEntry::Flag => flag_option!(target),
350                            ConfigEntry::Text { default } => {
351                                text_option!(target, value, default)
352                            },
353                            ConfigEntry::Int { default } => {
354                                int_option!(target, value, default)
355                            },
356                            ConfigEntry::Count => count_option!(target, value),
357                            ConfigEntry::List { sep } => {
358                                list_option!(target, value, sep)
359                            },
360                            ConfigEntry::Alias { .. } => {
361                                Err("alias to alias".into())
362                            },
363                        }
364                    } else {
365                        Err("target option not found".into())
366                    }
367                },
368            }
369        } else {
370            Err("config option not found".into())
371        }
372    }
373
374    fn parse_short_option<S>(
375        &self,
376        arg: &str,
377        next_arg: Option<&S>,
378    ) -> Result<(bool, HashMap<String, OptionValue>), String>
379    where
380        S: AsRef<str>,
381    {
382        let n: usize = arg.len();
383        let mut pairs: HashMap<String, OptionValue> = HashMap::new();
384        let iter = arg.char_indices();
385
386        for (i, flag) in iter {
387            let name: String = String::from(flag);
388            let next_value: Option<&str> = next_arg.map(|v| v.as_ref());
389            let Some(entry) = self.configs.get(&name) else {
390                return Err(format!("option '{}' is not supported", name));
391            };
392
393            match entry {
394                ConfigEntry::Flag => {
395                    pairs.insert(name, OptionValue::Flag);
396                },
397                ConfigEntry::Text { default } => {
398                    if i < n - flag.len_utf8() {
399                        let value = arg[i + flag.len_utf8()..n].to_string();
400                        pairs.insert(name, OptionValue::Text(value));
401                        return Ok((false, pairs));
402                    } else if let Some(value) = default {
403                        pairs.insert(name, OptionValue::Text(value.into()));
404                        return Ok((false, pairs));
405                    } else if let Some(value) = next_value {
406                        pairs.insert(name, OptionValue::Text(value.into()));
407                        return Ok((true, pairs));
408                    }
409                    return Err("null arg".into());
410                },
411                ConfigEntry::Int { default } => {
412                    if i < n - flag.len_utf8() {
413                        let value = arg[i + flag.len_utf8()..n].to_string();
414                        if let Ok(num) = value.parse() {
415                            pairs.insert(name, OptionValue::Int(num));
416                            return Ok((false, pairs));
417                        } else {
418                            return Err("invalid int".into());
419                        }
420                    } else if let Some(num) = default {
421                        pairs.insert(name, OptionValue::Int(*num));
422                        return Ok((false, pairs));
423                    } else if let Some(value) = next_value {
424                        if let Ok(num) = value.parse() {
425                            pairs.insert(name, OptionValue::Int(num));
426                            return Ok((true, pairs));
427                        } else {
428                            return Err("invalid int".into());
429                        }
430                    }
431                    return Err("null int".into());
432                },
433                ConfigEntry::Count => {
434                    let default = OptionValue::Int(0);
435                    let old_value: &OptionValue = pairs.get(&name)
436                        .unwrap_or(&default);
437                    pairs.insert(name, old_value.clone() + 1);
438                },
439                ConfigEntry::List { sep } => {
440                    let sep: &str = sep.as_deref().unwrap_or(",");
441                    if i < n - flag.len_utf8() {
442                        let value = arg[i + flag.len_utf8()..n].to_string();
443                        let parsed_value: Vec<String> = value
444                            .split(sep)
445                            .map(|item: &str| item.to_string())
446                            .collect();
447                        pairs.insert(name, OptionValue::List(parsed_value));
448                        return Ok((false, pairs));
449                    } else if let Some(value) = next_value {
450                        let parsed_value = if value.is_empty() {
451                            Vec::new()
452                        } else {
453                            value
454                                .split(sep)
455                                .map(|item: &str| item.to_string())
456                                .collect()
457                        };
458                        pairs.insert(name, OptionValue::List(parsed_value));
459                        return Ok((true, pairs));
460                    }
461                    return Err("null arg".into());
462                },
463                ConfigEntry::Alias { target } => {
464                    if let Some(target_entry) = self.configs.get(target) {
465                        let target: String = target.clone();
466                        match target_entry {
467                            ConfigEntry::Flag => {
468                                pairs.insert(target, OptionValue::Flag);
469                            },
470                            ConfigEntry::Text { default } => {
471                                if i < n - flag.len_utf8() {
472                                    let value = arg[i + flag.len_utf8()..n]
473                                        .to_string();
474                                    pairs.insert(
475                                        target,
476                                        OptionValue::Text(value)
477                                    );
478                                    return Ok((false, pairs));
479                                } else if let Some(value) = default {
480                                    pairs.insert(
481                                        target,
482                                        OptionValue::Text(value.into())
483                                    );
484                                    return Ok((false, pairs));
485                                } else if let Some(value) = next_value {
486                                    pairs.insert(
487                                        target,
488                                        OptionValue::Text(value.into())
489                                    );
490                                    return Ok((true, pairs));
491                                }
492                                return Err("null arg".into());
493                            },
494                            ConfigEntry::Int { default } => {
495                                if i < n - flag.len_utf8() {
496                                    let value = arg[i + flag.len_utf8()..n]
497                                        .to_string();
498                                    if let Ok(num) = value.parse() {
499                                        pairs.insert(
500                                            target,
501                                            OptionValue::Int(num),
502                                        );
503                                        return Ok((false, pairs));
504                                    } else {
505                                        return Err("invalid int".into());
506                                    }
507                                } else if let Some(num) = default {
508                                    pairs.insert(
509                                        target,
510                                        OptionValue::Int(*num),
511                                    );
512                                    return Ok((false, pairs));
513                                } else if let Some(value) = next_value {
514                                    if let Ok(num) = value.parse() {
515                                        pairs.insert(
516                                            target,
517                                            OptionValue::Int(num),
518                                        );
519                                        return Ok((true, pairs));
520                                    } else {
521                                        return Err("invalid int".into());
522                                    }
523                                }
524                                return Err("null int".into());
525                            },
526                            ConfigEntry::Count => {
527                                let default = OptionValue::Int(0);
528                                let old_value = pairs.get(&target)
529                                    .unwrap_or(&default);
530                                pairs.insert(target, old_value.clone() + 1);
531                            },
532                            ConfigEntry::List { sep } => {
533                                let sep: &str = sep.as_deref().unwrap_or(",");
534                                if i < n - flag.len_utf8() {
535                                    let value = arg[i + flag.len_utf8()..n]
536                                        .to_string();
537                                    let parsed_value: Vec<String> = value
538                                        .split(sep)
539                                        .map(|item: &str| item.to_string())
540                                        .collect();
541                                    pairs.insert(
542                                        target,
543                                        OptionValue::List(parsed_value),
544                                    );
545                                    return Ok((false, pairs));
546                                } else if let Some(value) = next_value {
547                                    let parsed_value = if value.is_empty() {
548                                        Vec::new()
549                                    } else {
550                                        value
551                                            .split(sep)
552                                            .map(|item: &str| item.to_string())
553                                            .collect()
554                                    };
555                                    pairs.insert(
556                                        target,
557                                        OptionValue::List(parsed_value),
558                                    );
559                                    return Ok((true, pairs));
560                                }
561                                return Err("null arg".into());
562                            },
563                            ConfigEntry::Alias { .. } => {
564                                return Err("alias to alias".into());
565                            },
566                        }
567                    } else {
568                        return Err("target option not found".into());
569                    }
570                },
571            }
572        }
573
574        Ok((false, pairs))
575    }
576}
577
578#[cfg(test)]
579mod test_parse {
580    use super::*;
581    use crate::entries;
582
583    #[test]
584    fn parse_input() {
585        let configs = entries! {
586            "quiet" => Flag,
587            "color" => Text,
588        };
589        let parser = ArgParser::new(configs.unwrap());
590        let input = ["--quiet", "build", "CC=clang"];
591        let result = parser.parse(input);
592
593        let operands = ["build"];
594        let options: HashMap<String, OptionValue> = HashMap::from([
595            ("quiet".to_string(), OptionValue::Flag),
596        ]);
597        let parameters: HashMap<String, String> = HashMap::from([
598            ("CC".to_string(), "clang".to_string()),
599        ]);
600
601        assert_eq!(result.clone().unwrap().options, options);
602        assert_eq!(result.clone().unwrap().parameters, parameters);
603        assert_eq!(result.clone().unwrap().operands, operands);
604    }
605
606    #[test]
607    fn parse_short_options() {
608        let configs = entries! {
609            "c" => Text,
610            "s" => Text,
611            "j" => Int { default: 0 },
612        };
613        let parser = ArgParser::new(configs.unwrap());
614        let input = ["-c./some_file.txt", "some arg"];
615        let result = parser.parse(input);
616
617        let operands = ["some arg"];
618        let options: HashMap<String, OptionValue> = HashMap::from([
619            ("c".to_string(), OptionValue::Text("./some_file.txt".to_string())),
620        ]);
621
622        assert_eq!(result.clone().unwrap().options, options);
623        assert_eq!(result.clone().unwrap().operands, operands);
624    }
625}