1#[derive(Debug, Clone, Copy)]
5pub struct Opt {
6 pub name: &'static str,
7 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
28pub 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 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 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 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
97pub 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;