Skip to main content

taba_cli/args/
mod.rs

1//! Own reading of the arguments (no command-line library), driven by the table of
2//! options of each command.
3
4#[derive(Debug, Clone, Copy)]
5pub struct Opt {
6    pub name: &'static str,
7    /// `true` if the option takes a value; `false` is an on/off switch.
8    pub takes_value: bool,
9    pub short: Option<char>,
10}
11
12pub const fn val(name: &'static str) -> Opt {
13    Opt {
14        name,
15        takes_value: true,
16        short: None,
17    }
18}
19
20pub const fn flag(name: &'static str) -> Opt {
21    Opt {
22        name,
23        takes_value: false,
24        short: None,
25    }
26}
27
28/// The options every command accepts.
29pub const GLOBAL: [Opt; 7] = [
30    val("profile"),
31    val("lang"),
32    val("request-id"),
33    Opt {
34        name: "output",
35        takes_value: true,
36        short: Some('o'),
37    },
38    Opt {
39        name: "quiet",
40        takes_value: false,
41        short: Some('q'),
42    },
43    flag("help"),
44    flag("version"),
45];
46
47#[derive(Debug, Clone, Default, PartialEq, Eq)]
48pub struct Parsed {
49    pub positional: Vec<String>,
50    values: Vec<(String, String)>,
51    flags: Vec<String>,
52}
53
54impl Parsed {
55    /// The last value given to the option.
56    pub fn value(&self, name: &str) -> Option<&str> {
57        self.values
58            .iter()
59            .rev()
60            .find(|(n, _)| n == name)
61            .map(|(_, v)| v.as_str())
62    }
63
64    /// All the option's values, in the order they came.
65    pub fn values(&self, name: &str) -> Vec<&str> {
66        self.values
67            .iter()
68            .filter(|(n, _)| n == name)
69            .map(|(_, v)| v.as_str())
70            .collect()
71    }
72
73    pub fn has_values(&self, name: &str) -> bool {
74        self.values.iter().any(|(n, _)| n == name)
75    }
76
77    pub fn flag(&self, name: &str) -> bool {
78        self.flags.iter().any(|f| f == name)
79    }
80
81    /// The option's value as an unsigned integer.
82    pub fn number(&self, name: &str) -> Result<Option<u64>, String> {
83        match self.value(name) {
84            None => Ok(None),
85            Some(v) => v
86                .parse::<u64>()
87                .map(Some)
88                .map_err(|_| format!("--{name}: `{v}` is not an integer")),
89        }
90    }
91
92    pub fn has_any(&self) -> bool {
93        !self.values.is_empty() || !self.flags.is_empty()
94    }
95}
96
97/// Reads `args` (without the program name or the command words) according to the command's options plus
98/// the global ones. `--option=value`, `--option value`, `-o value`, `-ovalue` and `--` (the rest is positional).
99pub fn parse(args: &[String], opts: &[Opt]) -> Result<Parsed, String> {
100    let find_long = |name: &str| opts.iter().chain(GLOBAL.iter()).find(|o| o.name == name);
101    let find_short = |c: char| {
102        opts.iter()
103            .chain(GLOBAL.iter())
104            .find(|o| o.short == Some(c))
105    };
106    let mut parsed = Parsed::default();
107    let mut i = 0;
108    let mut only_positional = false;
109    while i < args.len() {
110        let arg = &args[i];
111        i += 1;
112        if only_positional || arg == "-" || !arg.starts_with('-') {
113            parsed.positional.push(arg.clone());
114            continue;
115        }
116        if arg == "--" {
117            only_positional = true;
118            continue;
119        }
120        let (opt, inline) = if let Some(long) = arg.strip_prefix("--") {
121            let (name, inline) = match long.split_once('=') {
122                Some((n, v)) => (n, Some(v.to_owned())),
123                None => (long, None),
124            };
125            (
126                find_long(name).ok_or_else(|| format!("unknown option: --{name}"))?,
127                inline,
128            )
129        } else {
130            let mut chars = arg[1..].chars();
131            let c = chars.next().unwrap_or('-');
132            let rest: String = chars.collect();
133            let opt = find_short(c).ok_or_else(|| format!("unknown option: -{c}"))?;
134            (opt, if rest.is_empty() { None } else { Some(rest) })
135        };
136        if opt.takes_value {
137            let value = match inline {
138                Some(v) => v,
139                None => {
140                    let v = args
141                        .get(i)
142                        .ok_or_else(|| format!("--{} needs a value", opt.name))?;
143                    i += 1;
144                    v.clone()
145                }
146            };
147            parsed.values.push((opt.name.to_owned(), value));
148        } else {
149            if inline
150                .as_ref()
151                .is_some_and(|v| arg.starts_with("--") || !v.is_empty())
152            {
153                return Err(format!("--{} takes no value", opt.name));
154            }
155            parsed.flags.push(opt.name.to_owned());
156        }
157    }
158    Ok(parsed)
159}
160
161#[cfg(test)]
162mod tests;