1use crate::errors;
5use crate::spec::field::FieldMeta;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FlagKind {
9 Str,
10 Bool,
11 Slice,
12}
13
14#[derive(Debug, Clone)]
16pub struct FlagDef {
17 pub long: String,
18 pub short: Option<char>,
19 pub kind: FlagKind,
20 pub field: FieldMeta,
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct FlagVal {
26 pub seen: bool,
27 pub str: String,
28 pub list: Vec<String>,
29 pub boolean: bool,
30}
31
32pub fn parse_flags(
34 defs: &[FlagDef],
35 args: &[String],
36) -> errors::Result<(Vec<FlagVal>, Vec<String>)> {
37 let mut long_idx: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
38 let mut short_idx: std::collections::HashMap<char, usize> = std::collections::HashMap::new();
39 for (i, d) in defs.iter().enumerate() {
40 long_idx.insert(d.long.as_str(), i);
41 if let Some(sh) = d.short {
42 short_idx.insert(sh, i);
43 }
44 }
45 let mut fvals: Vec<FlagVal> = defs.iter().map(|_| FlagVal::default()).collect();
46 let mut pos: Vec<String> = Vec::new();
47 let mut i = 0;
48 while i < args.len() {
49 let a = &args[i];
50 if a == "--" {
51 pos.extend_from_slice(&args[i + 1..]);
52 break;
53 }
54 if let Some(rest) = a.strip_prefix("--") {
55 let (name, has_val, val) = match rest.split_once('=') {
56 Some((n, v)) => (n, true, v.to_string()),
57 None => (rest, false, String::new()),
58 };
59 let Some(&di) = long_idx.get(name) else {
60 return Err(errors::Error::new(
61 errors::Kind::InvalidInput,
62 format!("unknown flag: --{name}"),
63 ));
64 };
65 let fv = &mut fvals[di];
66 if defs[di].kind == FlagKind::Bool {
67 let mut b = true;
68 if has_val {
69 b = crate::spec::scalar::parse_bool(&val).map_err(|_| {
70 errors::Error::new(
71 errors::Kind::InvalidInput,
72 format!("invalid boolean value {val:?} for --{name}"),
73 )
74 })?;
75 }
76 fv.seen = true;
77 fv.boolean = b;
78 i += 1;
79 continue;
80 }
81 let val = if !has_val {
82 if i + 1 >= args.len() {
83 return Err(errors::Error::new(
84 errors::Kind::InvalidInput,
85 format!("flag needs an argument: --{name}"),
86 ));
87 }
88 i += 1;
89 args[i].clone()
90 } else {
91 val
92 };
93 fv.seen = true;
94 if defs[di].kind == FlagKind::Slice {
95 fv.list.push(val);
96 } else {
97 fv.str = val;
98 }
99 i += 1;
100 continue;
101 }
102 if a.starts_with('-') && a.len() > 1 {
103 let chars: Vec<char> = a.chars().collect();
104 let sh = chars[1];
105 let Some(&di) = short_idx.get(&sh) else {
106 return Err(errors::Error::new(
107 errors::Kind::InvalidInput,
108 format!("unknown shorthand flag: -{sh}"),
109 ));
110 };
111 let fv = &mut fvals[di];
112 let mut rest: String = chars.iter().skip(2).collect();
113 if let Some(stripped) = rest.strip_prefix('=') {
114 rest = stripped.to_string();
115 }
116 if defs[di].kind == FlagKind::Bool {
117 if rest.is_empty() {
118 fv.seen = true;
119 fv.boolean = true;
120 i += 1;
121 continue;
122 }
123 let b = crate::spec::scalar::parse_bool(&rest).map_err(|_| {
124 errors::Error::new(
125 errors::Kind::InvalidInput,
126 format!("invalid boolean value {rest:?} for -{sh}"),
127 )
128 })?;
129 fv.seen = true;
130 fv.boolean = b;
131 i += 1;
132 continue;
133 }
134 let rest = if rest.is_empty() {
135 if i + 1 >= args.len() {
136 return Err(errors::Error::new(
137 errors::Kind::InvalidInput,
138 format!("flag needs an argument: -{sh}"),
139 ));
140 }
141 i += 1;
142 args[i].clone()
143 } else {
144 rest
145 };
146 fv.seen = true;
147 if defs[di].kind == FlagKind::Slice {
148 fv.list.push(rest);
149 } else {
150 fv.str = rest;
151 }
152 i += 1;
153 continue;
154 }
155 pos.push(a.clone());
156 i += 1;
157 }
158 Ok((fvals, pos))
159}
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::spec::field::{FieldKind, FieldMeta};
164
165 fn str_def(name: &str, short: Option<char>) -> FlagDef {
166 FlagDef {
167 long: name.to_string(),
168 short,
169 kind: FlagKind::Str,
170 field: fm(name),
171 }
172 }
173 fn bool_def(name: &str, short: Option<char>) -> FlagDef {
174 FlagDef {
175 long: name.to_string(),
176 short,
177 kind: FlagKind::Bool,
178 field: fm(name),
179 }
180 }
181 fn slice_def(name: &str, short: Option<char>) -> FlagDef {
182 FlagDef {
183 long: name.to_string(),
184 short,
185 kind: FlagKind::Slice,
186 field: fm(name),
187 }
188 }
189 fn fm(name: &str) -> FieldMeta {
190 FieldMeta {
191 name: name.to_string(),
192 json_name: name.to_string(),
193 kind: FieldKind::String,
194 ..Default::default()
195 }
196 }
197 fn args(v: &[&str]) -> Vec<String> {
198 v.iter().map(|s| s.to_string()).collect()
199 }
200
201 #[test]
202 fn long_short_and_eq_forms() {
203 let defs = vec![str_def("query", Some('q')), bool_def("verbose", Some('v'))];
204 let (vals, pos) =
205 parse_flags(&defs, &args(&["--query", "x", "-v", "--query=y", "p"])).unwrap();
206 assert!(vals[0].seen);
207 assert_eq!(vals[0].str, "y"); assert!(vals[1].boolean);
209 assert_eq!(pos, vec!["p".to_string()]);
210 }
211
212 #[test]
213 fn slice_accumulates() {
214 let defs = vec![slice_def("tags", Some('t'))];
215 let (vals, _) = parse_flags(&defs, &args(&["--tags", "a", "-t", "b"])).unwrap();
216 assert_eq!(vals[0].list, vec!["a", "b"]);
217 }
218
219 #[test]
220 fn bool_with_value() {
221 let defs = vec![bool_def("v", None)];
222 let (vals, _) = parse_flags(&defs, &args(&["--v=false"])).unwrap();
223 assert!(!vals[0].boolean);
224 let (vals, _) = parse_flags(&defs, &args(&["--v"])).unwrap();
225 assert!(vals[0].boolean);
226 }
227
228 #[test]
229 fn double_dash_stops_parsing() {
230 let defs = vec![str_def("a", None)];
231 let (vals, pos) = parse_flags(&defs, &args(&["--a", "1", "--", "--a", "2"])).unwrap();
232 assert_eq!(vals[0].str, "1");
233 assert_eq!(pos, vec!["--a".to_string(), "2".to_string()]);
234 }
235
236 #[test]
237 fn unknown_flags_error() {
238 let defs = vec![str_def("a", Some('a'))];
239 let err = parse_flags(&defs, &args(&["--nope"])).unwrap_err();
240 assert!(err.to_string().contains("unknown flag: --nope"), "{err}");
241 let err = parse_flags(&defs, &args(&["-z"])).unwrap_err();
242 assert!(
243 err.to_string().contains("unknown shorthand flag: -z"),
244 "{err}"
245 );
246 let err = parse_flags(&defs, &args(&["--a"])).unwrap_err();
247 assert!(
248 err.to_string().contains("flag needs an argument: --a"),
249 "{err}"
250 );
251 }
252
253 #[test]
254 fn shorthand_with_attached_value() {
255 let defs = vec![str_def("name", Some('n'))];
256 let (vals, _) = parse_flags(&defs, &args(&["-nalice"])).unwrap();
257 assert_eq!(vals[0].str, "alice");
258 let (vals, _) = parse_flags(&defs, &args(&["-n=alice"])).unwrap();
259 assert_eq!(vals[0].str, "alice");
260 }
261}