1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use strum::EnumTryAs;
9
10#[cfg(feature = "docs")]
11use crate::docs;
12use crate::error::UsageErr;
13use crate::{Spec, SpecArg, SpecCommand, SpecFlag};
14
15pub struct ParseOutput {
16 pub cmd: SpecCommand,
17 pub cmds: Vec<SpecCommand>,
18 pub args: IndexMap<SpecArg, ParseValue>,
19 pub flags: IndexMap<SpecFlag, ParseValue>,
20 pub available_flags: BTreeMap<String, SpecFlag>,
21 pub flag_awaiting_value: Vec<SpecFlag>,
22 pub errors: Vec<UsageErr>,
23}
24
25#[derive(Debug, EnumTryAs, Clone)]
26pub enum ParseValue {
27 Bool(bool),
28 String(String),
29 MultiBool(Vec<bool>),
30 MultiString(Vec<String>),
31}
32
33pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
34 let mut out = parse_partial(spec, input)?;
35 trace!("{out:?}");
36 for arg in out.cmd.args.iter().skip(out.args.len()) {
37 if let Some(default) = arg.default.as_ref() {
38 out.args
39 .insert(arg.clone(), ParseValue::String(default.clone()));
40 }
41 }
42 for flag in out.available_flags.values() {
43 if out.flags.contains_key(flag) {
44 continue;
45 }
46 if let Some(default) = flag.default.as_ref() {
47 out.flags
48 .insert(flag.clone(), ParseValue::String(default.clone()));
49 }
50 if let Some(Some(default)) = flag.arg.as_ref().map(|a| &a.default) {
51 out.flags
52 .insert(flag.clone(), ParseValue::String(default.clone()));
53 }
54 }
55 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
56 bail!("{err}");
57 }
58 if !out.errors.is_empty() {
59 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
60 }
61 Ok(out)
62}
63
64pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
65 trace!("parse_partial: {input:?}");
66 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
67 input.pop_front();
68
69 let gather_flags = |cmd: &SpecCommand| {
70 cmd.flags
71 .iter()
72 .flat_map(|f| {
73 let mut flags = f
74 .long
75 .iter()
76 .map(|l| (format!("--{}", l), f.clone()))
77 .chain(f.short.iter().map(|s| (format!("-{}", s), f.clone())))
78 .collect::<Vec<_>>();
79 if let Some(negate) = &f.negate {
80 flags.push((negate.clone(), f.clone()));
81 }
82 flags
83 })
84 .collect()
85 };
86
87 let mut out = ParseOutput {
88 cmd: spec.cmd.clone(),
89 cmds: vec![spec.cmd.clone()],
90 args: IndexMap::new(),
91 flags: IndexMap::new(),
92 available_flags: gather_flags(&spec.cmd),
93 flag_awaiting_value: vec![],
94 errors: vec![],
95 };
96
97 while !input.is_empty() {
98 if let Some(subcommand) = out.cmd.find_subcommand(&input[0]) {
99 let mut subcommand = subcommand.clone();
100 subcommand.mount()?;
101 out.available_flags.retain(|_, f| f.global);
102 out.available_flags.extend(gather_flags(&subcommand));
103 input.pop_front();
104 out.cmds.push(subcommand.clone());
105 out.cmd = subcommand.clone();
106 } else {
107 break;
108 }
109 }
110
111 let mut next_arg = out.cmd.args.first();
112 let mut enable_flags = true;
113 let mut grouped_flag = false;
114
115 while !input.is_empty() {
116 let mut w = input.pop_front().unwrap();
117
118 if w == "--" {
119 enable_flags = false;
120 continue;
121 }
122
123 if enable_flags && w.starts_with("--") {
125 grouped_flag = false;
126 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
127 if !val.is_empty() {
128 input.push_front(val.to_string());
129 }
130 if let Some(f) = out.available_flags.get(word) {
131 if f.arg.is_some() {
132 out.flag_awaiting_value.push(f.clone());
133 } else if f.var {
134 let arr = out
135 .flags
136 .entry(f.clone())
137 .or_insert_with(|| ParseValue::MultiBool(vec![]))
138 .try_as_multi_bool_mut()
139 .unwrap();
140 arr.push(true);
141 } else {
142 let negate = f.negate.clone().unwrap_or_default();
143 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
144 }
145 continue;
146 }
147 if is_help_arg(spec, &w) {
148 out.errors
149 .push(render_help_err(spec, &out.cmd, w.len() > 2));
150 return Ok(out);
151 }
152 }
153
154 if enable_flags && w.starts_with('-') && w.len() > 1 {
156 let short = w.chars().nth(1).unwrap();
157 if let Some(f) = out.available_flags.get(&format!("-{}", short)) {
158 if w.len() > 2 {
159 input.push_front(format!("-{}", &w[2..]));
160 grouped_flag = true;
161 }
162 if f.arg.is_some() {
163 out.flag_awaiting_value.push(f.clone());
164 } else if f.var {
165 let arr = out
166 .flags
167 .entry(f.clone())
168 .or_insert_with(|| ParseValue::MultiBool(vec![]))
169 .try_as_multi_bool_mut()
170 .unwrap();
171 arr.push(true);
172 } else {
173 let negate = f.negate.clone().unwrap_or_default();
174 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
175 }
176 continue;
177 }
178 if is_help_arg(spec, &w) {
179 out.errors
180 .push(render_help_err(spec, &out.cmd, w.len() > 2));
181 return Ok(out);
182 }
183 if grouped_flag {
184 grouped_flag = false;
185 w.remove(0);
186 }
187 }
188
189 if !out.flag_awaiting_value.is_empty() {
190 while let Some(flag) = out.flag_awaiting_value.pop() {
191 let arg = flag.arg.as_ref().unwrap();
192 if flag.var {
193 let arr = out
194 .flags
195 .entry(flag)
196 .or_insert_with(|| ParseValue::MultiString(vec![]))
197 .try_as_multi_string_mut()
198 .unwrap();
199 arr.push(w);
200 } else {
201 if let Some(choices) = &arg.choices {
202 if !choices.choices.contains(&w) {
203 if is_help_arg(spec, &w) {
204 out.errors
205 .push(render_help_err(spec, &out.cmd, w.len() > 2));
206 return Ok(out);
207 }
208 bail!(
209 "Invalid choice for option {}: {w}, expected one of {}",
210 flag.name,
211 choices.choices.join(", ")
212 );
213 }
214 }
215 out.flags.insert(flag, ParseValue::String(w));
216 }
217 w = "".to_string();
218 }
219 continue;
220 }
221
222 if let Some(arg) = next_arg {
223 if arg.var {
224 let arr = out
225 .args
226 .entry(arg.clone())
227 .or_insert_with(|| ParseValue::MultiString(vec![]))
228 .try_as_multi_string_mut()
229 .unwrap();
230 arr.push(w);
231 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
232 next_arg = out.cmd.args.get(out.args.len());
233 }
234 } else {
235 if let Some(choices) = &arg.choices {
236 if !choices.choices.contains(&w) {
237 if is_help_arg(spec, &w) {
238 out.errors
239 .push(render_help_err(spec, &out.cmd, w.len() > 2));
240 return Ok(out);
241 }
242 bail!(
243 "Invalid choice for arg {}: {w}, expected one of {}",
244 arg.name,
245 choices.choices.join(", ")
246 );
247 }
248 }
249 out.args.insert(arg.clone(), ParseValue::String(w));
250 next_arg = out.cmd.args.get(out.args.len());
251 }
252 continue;
253 }
254 if is_help_arg(spec, &w) {
255 out.errors
256 .push(render_help_err(spec, &out.cmd, w.len() > 2));
257 return Ok(out);
258 }
259 bail!("unexpected word: {w}");
260 }
261
262 for arg in out.cmd.args.iter().skip(out.args.len()) {
263 if arg.required && arg.default.is_none() {
264 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
265 }
266 }
267
268 for flag in out.available_flags.values() {
269 if out.flags.contains_key(flag) {
270 continue;
271 }
272 let has_default = flag.default.is_some() || flag.arg.iter().any(|a| a.default.is_some());
273 if flag.required && !has_default {
274 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
275 }
276 }
277
278 Ok(out)
279}
280
281#[cfg(feature = "docs")]
282fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
283 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
284}
285
286#[cfg(not(feature = "docs"))]
287fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
288 UsageErr::Help("help".to_string())
289}
290
291fn is_help_arg(spec: &Spec, w: &str) -> bool {
292 spec.disable_help != Some(true)
293 && (w == "--help"
294 || w == "-h"
295 || w == "-?"
296 || (spec.cmd.subcommands.is_empty() && w == "help"))
297}
298
299impl ParseOutput {
300 pub fn as_env(&self) -> BTreeMap<String, String> {
301 let mut env = BTreeMap::new();
302 for (flag, val) in &self.flags {
303 let key = format!("usage_{}", flag.name.to_snake_case());
304 let val = match val {
305 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
306 ParseValue::String(s) => s.clone(),
307 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
308 ParseValue::MultiString(s) => shell_words::join(s),
309 };
310 env.insert(key, val);
311 }
312 for (arg, val) in &self.args {
313 let key = format!("usage_{}", arg.name.to_snake_case());
314 env.insert(key, val.to_string());
315 }
316 env
317 }
318}
319
320impl Display for ParseValue {
321 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
322 match self {
323 ParseValue::Bool(b) => write!(f, "{}", b),
324 ParseValue::String(s) => write!(f, "{}", s),
325 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
326 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
327 }
328 }
329}
330
331impl Debug for ParseOutput {
332 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
333 f.debug_struct("ParseOutput")
334 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
335 .field(
336 "args",
337 &self
338 .args
339 .iter()
340 .map(|(a, w)| format!("{}: {w}", &a.name))
341 .collect_vec(),
342 )
343 .field(
344 "available_flags",
345 &self
346 .available_flags
347 .iter()
348 .map(|(f, w)| format!("{f}: {w}"))
349 .collect_vec(),
350 )
351 .field(
352 "flags",
353 &self
354 .flags
355 .iter()
356 .map(|(f, w)| format!("{}: {w}", &f.name))
357 .collect_vec(),
358 )
359 .field("flag_awaiting_value", &self.flag_awaiting_value)
360 .field("errors", &self.errors)
361 .finish()
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_parse() {
371 let mut cmd = SpecCommand::default();
372 cmd.name = "test".to_string();
373 cmd.args = vec![SpecArg {
374 name: "arg".to_string(),
375 ..Default::default()
376 }];
377 cmd.flags = vec![SpecFlag {
378 name: "flag".to_string(),
379 long: vec!["flag".to_string()],
380 ..Default::default()
381 }];
382 let spec = Spec {
383 name: "test".to_string(),
384 bin: "test".to_string(),
385 cmd,
386 ..Default::default()
387 };
388 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
389 let parsed = parse(&spec, &input).unwrap();
390 assert_eq!(parsed.cmds.len(), 1);
391 assert_eq!(parsed.cmds[0].name, "test");
392 assert_eq!(parsed.args.len(), 1);
393 assert_eq!(parsed.flags.len(), 1);
394 assert_eq!(parsed.available_flags.len(), 1);
395 }
396
397 #[test]
398 fn test_as_env() {
399 let mut cmd = SpecCommand::default();
400 cmd.name = "test".to_string();
401 cmd.args = vec![SpecArg {
402 name: "arg".to_string(),
403 ..Default::default()
404 }];
405 cmd.flags = vec![
406 SpecFlag {
407 name: "flag".to_string(),
408 long: vec!["flag".to_string()],
409 ..Default::default()
410 },
411 SpecFlag {
412 name: "force".to_string(),
413 long: vec!["force".to_string()],
414 negate: Some("--no-force".to_string()),
415 ..Default::default()
416 },
417 ];
418 let spec = Spec {
419 name: "test".to_string(),
420 bin: "test".to_string(),
421 cmd,
422 ..Default::default()
423 };
424 let input = vec![
425 "test".to_string(),
426 "--flag".to_string(),
427 "--no-force".to_string(),
428 ];
429 let parsed = parse(&spec, &input).unwrap();
430 let env = parsed.as_env();
431 assert_eq!(env.len(), 2);
432 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
433 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
434 }
435}