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: Option<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: None,
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
114 while !input.is_empty() {
115 let w = input.pop_front().unwrap();
116
117 if let Some(flag) = out.flag_awaiting_value {
118 out.flag_awaiting_value = None;
119 let arg = flag.arg.as_ref().unwrap();
120 if flag.var {
121 let arr = out
122 .flags
123 .entry(flag)
124 .or_insert_with(|| ParseValue::MultiString(vec![]))
125 .try_as_multi_string_mut()
126 .unwrap();
127 arr.push(w);
128 } else {
129 if let Some(choices) = &arg.choices {
130 if !choices.choices.contains(&w) {
131 if is_help_arg(spec, &w) {
132 out.errors
133 .push(render_help_err(spec, &out.cmd, w.len() > 2));
134 return Ok(out);
135 }
136 bail!(
137 "Invalid choice for option {}: {w}, expected one of {}",
138 flag.name,
139 choices.choices.join(", ")
140 );
141 }
142 }
143 out.flags.insert(flag, ParseValue::String(w));
144 }
145 continue;
146 }
147
148 if w == "--" {
149 enable_flags = false;
150 continue;
151 }
152
153 if enable_flags && w.starts_with("--") {
155 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
156 if !val.is_empty() {
157 input.push_front(val.to_string());
158 }
159 if let Some(f) = out.available_flags.get(word) {
160 if f.arg.is_some() {
161 out.flag_awaiting_value = Some(f.clone());
162 } else if f.var {
163 let arr = out
164 .flags
165 .entry(f.clone())
166 .or_insert_with(|| ParseValue::MultiBool(vec![]))
167 .try_as_multi_bool_mut()
168 .unwrap();
169 arr.push(true);
170 } else {
171 let negate = f.negate.clone().unwrap_or_default();
172 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
173 }
174 continue;
175 }
176 if is_help_arg(spec, &w) {
177 out.errors
178 .push(render_help_err(spec, &out.cmd, w.len() > 2));
179 return Ok(out);
180 }
181 }
182
183 if enable_flags && w.starts_with('-') && w.len() > 1 {
185 let short = w.chars().nth(1).unwrap();
186 if let Some(f) = out.available_flags.get(&format!("-{}", short)) {
187 if w.len() > 2 {
188 input.push_front(w[2..].to_string());
189 }
190 if f.arg.is_some() {
191 out.flag_awaiting_value = Some(f.clone());
192 } else if f.var {
193 let arr = out
194 .flags
195 .entry(f.clone())
196 .or_insert_with(|| ParseValue::MultiBool(vec![]))
197 .try_as_multi_bool_mut()
198 .unwrap();
199 arr.push(true);
200 } else {
201 let negate = f.negate.clone().unwrap_or_default();
202 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
203 }
204 continue;
205 }
206 if is_help_arg(spec, &w) {
207 out.errors
208 .push(render_help_err(spec, &out.cmd, w.len() > 2));
209 return Ok(out);
210 }
211 }
212
213 if let Some(arg) = next_arg {
214 if arg.var {
215 let arr = out
216 .args
217 .entry(arg.clone())
218 .or_insert_with(|| ParseValue::MultiString(vec![]))
219 .try_as_multi_string_mut()
220 .unwrap();
221 arr.push(w);
222 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
223 next_arg = out.cmd.args.get(out.args.len());
224 }
225 } else {
226 if let Some(choices) = &arg.choices {
227 if !choices.choices.contains(&w) {
228 if is_help_arg(spec, &w) {
229 out.errors
230 .push(render_help_err(spec, &out.cmd, w.len() > 2));
231 return Ok(out);
232 }
233 bail!(
234 "Invalid choice for arg {}: {w}, expected one of {}",
235 arg.name,
236 choices.choices.join(", ")
237 );
238 }
239 }
240 out.args.insert(arg.clone(), ParseValue::String(w));
241 next_arg = out.cmd.args.get(out.args.len());
242 }
243 continue;
244 }
245 if is_help_arg(spec, &w) {
246 out.errors
247 .push(render_help_err(spec, &out.cmd, w.len() > 2));
248 return Ok(out);
249 }
250 bail!("unexpected word: {w}");
251 }
252
253 for arg in out.cmd.args.iter().skip(out.args.len()) {
254 if arg.required && arg.default.is_none() {
255 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
256 }
257 }
258
259 for flag in out.available_flags.values() {
260 if out.flags.contains_key(flag) {
261 continue;
262 }
263 let has_default = flag.default.is_some() || flag.arg.iter().any(|a| a.default.is_some());
264 if flag.required && !has_default {
265 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
266 }
267 }
268
269 Ok(out)
270}
271
272#[cfg(feature = "docs")]
273fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
274 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
275}
276
277#[cfg(not(feature = "docs"))]
278fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
279 UsageErr::Help("help".to_string())
280}
281
282fn is_help_arg(spec: &Spec, w: &str) -> bool {
283 spec.disable_help != Some(true)
284 && (w == "--help"
285 || w == "-h"
286 || w == "-?"
287 || (spec.cmd.subcommands.is_empty() && w == "help"))
288}
289
290impl ParseOutput {
291 pub fn as_env(&self) -> BTreeMap<String, String> {
292 let mut env = BTreeMap::new();
293 for (flag, val) in &self.flags {
294 let key = format!("usage_{}", flag.name.to_snake_case());
295 let val = match val {
296 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
297 ParseValue::String(s) => s.clone(),
298 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
299 ParseValue::MultiString(s) => shell_words::join(s),
300 };
301 env.insert(key, val);
302 }
303 for (arg, val) in &self.args {
304 let key = format!("usage_{}", arg.name.to_snake_case());
305 env.insert(key, val.to_string());
306 }
307 env
308 }
309}
310
311impl Display for ParseValue {
312 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
313 match self {
314 ParseValue::Bool(b) => write!(f, "{}", b),
315 ParseValue::String(s) => write!(f, "{}", s),
316 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
317 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
318 }
319 }
320}
321
322impl Debug for ParseOutput {
323 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
324 f.debug_struct("ParseOutput")
325 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
326 .field(
327 "args",
328 &self
329 .args
330 .iter()
331 .map(|(a, w)| format!("{}: {w}", &a.name))
332 .collect_vec(),
333 )
334 .field(
335 "available_flags",
336 &self
337 .available_flags
338 .iter()
339 .map(|(f, w)| format!("{f}: {w}"))
340 .collect_vec(),
341 )
342 .field(
343 "flags",
344 &self
345 .flags
346 .iter()
347 .map(|(f, w)| format!("{}: {w}", &f.name))
348 .collect_vec(),
349 )
350 .field("flag_awaiting_value", &self.flag_awaiting_value)
351 .field("errors", &self.errors)
352 .finish()
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_parse() {
362 let mut cmd = SpecCommand::default();
363 cmd.name = "test".to_string();
364 cmd.args = vec![SpecArg {
365 name: "arg".to_string(),
366 ..Default::default()
367 }];
368 cmd.flags = vec![SpecFlag {
369 name: "flag".to_string(),
370 long: vec!["flag".to_string()],
371 ..Default::default()
372 }];
373 let spec = Spec {
374 name: "test".to_string(),
375 bin: "test".to_string(),
376 cmd,
377 ..Default::default()
378 };
379 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
380 let parsed = parse(&spec, &input).unwrap();
381 assert_eq!(parsed.cmds.len(), 1);
382 assert_eq!(parsed.cmds[0].name, "test");
383 assert_eq!(parsed.args.len(), 1);
384 assert_eq!(parsed.flags.len(), 1);
385 assert_eq!(parsed.available_flags.len(), 1);
386 }
387
388 #[test]
389 fn test_as_env() {
390 let mut cmd = SpecCommand::default();
391 cmd.name = "test".to_string();
392 cmd.args = vec![SpecArg {
393 name: "arg".to_string(),
394 ..Default::default()
395 }];
396 cmd.flags = vec![
397 SpecFlag {
398 name: "flag".to_string(),
399 long: vec!["flag".to_string()],
400 ..Default::default()
401 },
402 SpecFlag {
403 name: "force".to_string(),
404 long: vec!["force".to_string()],
405 negate: Some("--no-force".to_string()),
406 ..Default::default()
407 },
408 ];
409 let spec = Spec {
410 name: "test".to_string(),
411 bin: "test".to_string(),
412 cmd,
413 ..Default::default()
414 };
415 let input = vec![
416 "test".to_string(),
417 "--flag".to_string(),
418 "--no-force".to_string(),
419 ];
420 let parsed = parse(&spec, &input).unwrap();
421 let env = parsed.as_env();
422 assert_eq!(env.len(), 2);
423 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
424 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
425 }
426}