1use itertools::Itertools;
2use kdl::{KdlDocument, KdlEntry, KdlNode};
3use serde::Serialize;
4use std::fmt::Display;
5use std::hash::Hash;
6use std::str::FromStr;
7
8use crate::error::UsageErr::InvalidFlag;
9use crate::error::{Result, UsageErr};
10use crate::spec::builder::SpecFlagBuilder;
11use crate::spec::context::ParsingContext;
12use crate::spec::helpers::NodeHelper;
13use crate::spec::is_false;
14use crate::{string, SpecArg, SpecChoices};
15
16#[derive(Debug, Default, Clone, Serialize)]
33pub struct SpecFlag {
34 pub name: String,
36 pub usage: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub help: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub help_long: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub help_md: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub help_first_line: Option<String>,
50 pub short: Vec<char>,
52 pub long: Vec<String>,
54 #[serde(skip_serializing_if = "is_false")]
56 pub required: bool,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub deprecated: Option<String>,
60 #[serde(skip_serializing_if = "is_false")]
62 pub var: bool,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub var_min: Option<usize>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub var_max: Option<usize>,
69 pub hide: bool,
71 pub global: bool,
73 #[serde(skip_serializing_if = "is_false")]
75 pub count: bool,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub arg: Option<SpecArg>,
79 #[serde(skip_serializing_if = "Vec::is_empty")]
81 pub default: Vec<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub negate: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub env: Option<String>,
88}
89
90impl SpecFlag {
91 pub fn builder() -> SpecFlagBuilder {
93 SpecFlagBuilder::new()
94 }
95
96 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
97 let mut flag: Self = node.arg(0)?.ensure_string()?.parse()?;
98 for (k, v) in node.props() {
99 match k {
100 "help" => flag.help = Some(v.ensure_string()?),
101 "long_help" => flag.help_long = Some(v.ensure_string()?),
102 "help_long" => flag.help_long = Some(v.ensure_string()?),
103 "help_md" => flag.help_md = Some(v.ensure_string()?),
104 "required" => flag.required = v.ensure_bool()?,
105 "var" => flag.var = v.ensure_bool()?,
106 "var_min" => flag.var_min = v.ensure_usize().map(Some)?,
107 "var_max" => flag.var_max = v.ensure_usize().map(Some)?,
108 "hide" => flag.hide = v.ensure_bool()?,
109 "deprecated" => {
110 flag.deprecated = match v.value.as_bool() {
111 Some(true) => Some("deprecated".into()),
112 Some(false) => None,
113 None => Some(v.ensure_string()?),
114 }
115 }
116 "global" => flag.global = v.ensure_bool()?,
117 "count" => flag.count = v.ensure_bool()?,
118 "default" => {
119 let default_value = match v.value.as_bool() {
121 Some(b) => b.to_string(),
122 None => v.ensure_string()?,
123 };
124 flag.default = vec![default_value];
125 }
126 "negate" => flag.negate = v.ensure_string().map(Some)?,
127 "env" => flag.env = v.ensure_string().map(Some)?,
128 k => bail_parse!(ctx, v.entry.span(), "unsupported flag key {k}"),
129 }
130 }
131 if !flag.default.is_empty() {
132 flag.required = false;
133 }
134 for child in node.children() {
135 match child.name() {
136 "arg" => flag.arg = Some(SpecArg::parse(ctx, &child)?),
137 "help" => flag.help = Some(child.arg(0)?.ensure_string()?),
138 "long_help" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
139 "help_long" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
140 "help_md" => flag.help_md = Some(child.arg(0)?.ensure_string()?),
141 "required" => flag.required = child.arg(0)?.ensure_bool()?,
142 "var" => flag.var = child.arg(0)?.ensure_bool()?,
143 "var_min" => flag.var_min = child.arg(0)?.ensure_usize().map(Some)?,
144 "var_max" => flag.var_max = child.arg(0)?.ensure_usize().map(Some)?,
145 "hide" => flag.hide = child.arg(0)?.ensure_bool()?,
146 "deprecated" => {
147 flag.deprecated = match child.arg(0)?.ensure_bool() {
148 Ok(true) => Some("deprecated".into()),
149 Ok(false) => None,
150 _ => Some(child.arg(0)?.ensure_string()?),
151 }
152 }
153 "global" => flag.global = child.arg(0)?.ensure_bool()?,
154 "count" => flag.count = child.arg(0)?.ensure_bool()?,
155 "default" => {
156 let children = child.children();
161 if children.is_empty() {
162 let arg = child.arg(0)?;
164 let default_value = match arg.value.as_bool() {
165 Some(b) => b.to_string(),
166 None => arg.ensure_string()?,
167 };
168 flag.default = vec![default_value];
169 } else {
170 flag.default = children.iter().map(|c| c.name().to_string()).collect();
173 }
174 }
175 "env" => flag.env = child.arg(0)?.ensure_string().map(Some)?,
176 "choices" => {
177 if let Some(arg) = &mut flag.arg {
178 arg.choices = Some(SpecChoices::parse(ctx, &child)?);
179 } else {
180 bail_parse!(
181 ctx,
182 child.node.name().span(),
183 "flag must have value to have choices"
184 )
185 }
186 }
187 k => bail_parse!(ctx, child.node.name().span(), "unsupported flag child {k}"),
188 }
189 }
190 flag.usage = flag.usage();
191 flag.help_first_line = flag.help.as_ref().map(|s| string::first_line(s));
192 Ok(flag)
193 }
194 pub fn usage(&self) -> String {
195 let mut parts = vec![];
196 let name = get_name_from_short_and_long(&self.short, &self.long).unwrap_or_default();
197 if name != self.name {
198 parts.push(format!("{}:", self.name));
199 }
200 if let Some(short) = self.short.first() {
201 parts.push(format!("-{short}"));
202 }
203 if let Some(long) = self.long.first() {
204 parts.push(format!("--{long}"));
205 }
206 let mut out = parts.join(" ");
207 if self.var {
208 out = format!("{out}…");
209 }
210 if let Some(arg) = &self.arg {
211 out = format!("{} {}", out, arg.usage());
212 }
213 out
214 }
215}
216
217impl From<&SpecFlag> for KdlNode {
218 fn from(flag: &SpecFlag) -> KdlNode {
219 let mut node = KdlNode::new("flag");
220 let name = flag
221 .short
222 .iter()
223 .map(|c| format!("-{c}"))
224 .chain(flag.long.iter().map(|s| format!("--{s}")))
225 .collect_vec()
226 .join(" ");
227 node.push(KdlEntry::new(name));
228 if let Some(desc) = &flag.help {
229 node.push(KdlEntry::new_prop("help", desc.clone()));
230 }
231 if let Some(desc) = &flag.help_long {
232 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
233 let mut node = KdlNode::new("long_help");
234 node.entries_mut().push(KdlEntry::new(desc.clone()));
235 children.nodes_mut().push(node);
236 }
237 if let Some(desc) = &flag.help_md {
238 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
239 let mut node = KdlNode::new("help_md");
240 node.entries_mut().push(KdlEntry::new(desc.clone()));
241 children.nodes_mut().push(node);
242 }
243 if flag.required {
244 node.push(KdlEntry::new_prop("required", true));
245 }
246 if flag.var {
247 node.push(KdlEntry::new_prop("var", true));
248 }
249 if let Some(var_min) = flag.var_min {
250 node.push(KdlEntry::new_prop("var_min", var_min as i128));
251 }
252 if let Some(var_max) = flag.var_max {
253 node.push(KdlEntry::new_prop("var_max", var_max as i128));
254 }
255 if flag.hide {
256 node.push(KdlEntry::new_prop("hide", true));
257 }
258 if flag.global {
259 node.push(KdlEntry::new_prop("global", true));
260 }
261 if flag.count {
262 node.push(KdlEntry::new_prop("count", true));
263 }
264 if let Some(negate) = &flag.negate {
265 node.push(KdlEntry::new_prop("negate", negate.clone()));
266 }
267 if let Some(env) = &flag.env {
268 node.push(KdlEntry::new_prop("env", env.clone()));
269 }
270 if let Some(deprecated) = &flag.deprecated {
271 node.push(KdlEntry::new_prop("deprecated", deprecated.clone()));
272 }
273 if !flag.default.is_empty() {
275 if flag.default.len() == 1 {
276 node.push(KdlEntry::new_prop("default", flag.default[0].clone()));
278 } else {
279 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
281 let mut default_node = KdlNode::new("default");
282 let default_children = default_node
283 .children_mut()
284 .get_or_insert_with(KdlDocument::new);
285 for val in &flag.default {
286 default_children
287 .nodes_mut()
288 .push(KdlNode::new(val.as_str()));
289 }
290 children.nodes_mut().push(default_node);
291 }
292 }
293 if let Some(arg) = &flag.arg {
294 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
295 children.nodes_mut().push(arg.into());
296 }
297 node
298 }
299}
300
301impl FromStr for SpecFlag {
302 type Err = UsageErr;
303 fn from_str(input: &str) -> Result<Self> {
304 let mut flag = Self::default();
305 let input = input.replace("...", "…").replace("…", " … ");
306 for part in input.split_whitespace() {
307 if let Some(name) = part.strip_suffix(':') {
308 flag.name = name.to_string();
309 } else if let Some(long) = part.strip_prefix("--") {
310 flag.long.push(long.to_string());
311 } else if let Some(short) = part.strip_prefix('-') {
312 if short.len() != 1 {
313 return Err(InvalidFlag {
314 token: format!("-{short}"),
315 reason: "short flags must be a single character (use -- for long flags)"
316 .to_string(),
317 span: (0, input.len()).into(),
318 input: input.to_string(),
319 });
320 }
321 flag.short.push(short.chars().next().unwrap());
322 } else if part == "…" {
323 if let Some(arg) = &mut flag.arg {
324 arg.var = true;
325 } else {
326 flag.var = true;
327 }
328 } else if part.starts_with('<') && part.ends_with('>')
329 || part.starts_with('[') && part.ends_with(']')
330 {
331 flag.arg = Some(part.to_string().parse()?);
332 } else {
333 return Err(InvalidFlag {
334 token: part.to_string(),
335 reason: "unexpected token (expected -x, --long, <arg>, or [arg])".to_string(),
336 span: (0, input.len()).into(),
337 input: input.to_string(),
338 });
339 }
340 }
341 if flag.name.is_empty() {
342 flag.name = get_name_from_short_and_long(&flag.short, &flag.long).unwrap_or_default();
343 }
344 flag.usage = flag.usage();
345 Ok(flag)
346 }
347}
348
349#[cfg(feature = "clap")]
350impl From<&clap::Arg> for SpecFlag {
351 fn from(c: &clap::Arg) -> Self {
352 let required = c.is_required_set();
353 let help = c.get_help().map(|s| s.to_string());
354 let help_long = c.get_long_help().map(|s| s.to_string());
355 let help_first_line = help.as_ref().map(|s| string::first_line(s));
356 let hide = c.is_hide_set();
357 let var = matches!(
358 c.get_action(),
359 clap::ArgAction::Count | clap::ArgAction::Append
360 );
361 let default: Vec<String> = c
362 .get_default_values()
363 .iter()
364 .map(|s| s.to_string_lossy().to_string())
365 .collect();
366 let short = c.get_short_and_visible_aliases().unwrap_or_default();
367 let long = c
368 .get_long_and_visible_aliases()
369 .unwrap_or_default()
370 .into_iter()
371 .map(|s| s.to_string())
372 .collect::<Vec<_>>();
373 let name = get_name_from_short_and_long(&short, &long).unwrap_or_default();
374 let arg = if let clap::ArgAction::Set | clap::ArgAction::Append = c.get_action() {
375 let mut arg = SpecArg::from(
376 c.get_value_names()
377 .map(|s| s.iter().map(|s| s.to_string()).join(" "))
378 .unwrap_or(name.clone())
379 .as_str(),
380 );
381
382 let choices = c
383 .get_possible_values()
384 .iter()
385 .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
386 .collect::<Vec<_>>();
387 if !choices.is_empty() {
388 arg.choices = Some(SpecChoices { choices });
389 }
390
391 Some(arg)
392 } else {
393 None
394 };
395 Self {
396 name,
397 usage: "".into(),
398 short,
399 long,
400 required,
401 help,
402 help_long,
403 help_md: None,
404 help_first_line,
405 var,
406 var_min: None,
407 var_max: None,
408 hide,
409 global: c.is_global_set(),
410 arg,
411 count: matches!(c.get_action(), clap::ArgAction::Count),
412 default,
413 deprecated: None,
414 negate: None,
415 env: None,
416 }
417 }
418}
419
420impl Display for SpecFlag {
467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 write!(f, "{}", self.usage())
469 }
470}
471impl PartialEq for SpecFlag {
472 fn eq(&self, other: &Self) -> bool {
473 self.name == other.name
474 }
475}
476impl Eq for SpecFlag {}
477impl Hash for SpecFlag {
478 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
479 self.name.hash(state);
480 }
481}
482
483fn get_name_from_short_and_long(short: &[char], long: &[String]) -> Option<String> {
484 long.first()
485 .map(|s| s.to_string())
486 .or_else(|| short.first().map(|c| c.to_string()))
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::Spec;
493 use insta::assert_snapshot;
494
495 #[test]
496 fn from_str() {
497 assert_snapshot!("-f".parse::<SpecFlag>().unwrap(), @"-f");
498 assert_snapshot!("--flag".parse::<SpecFlag>().unwrap(), @"--flag");
499 assert_snapshot!("-f --flag".parse::<SpecFlag>().unwrap(), @"-f --flag");
500 assert_snapshot!("-f --flag…".parse::<SpecFlag>().unwrap(), @"-f --flag…");
501 assert_snapshot!("-f --flag …".parse::<SpecFlag>().unwrap(), @"-f --flag…");
502 assert_snapshot!("--flag <arg>".parse::<SpecFlag>().unwrap(), @"--flag <arg>");
503 assert_snapshot!("-f --flag <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>");
504 assert_snapshot!("-f --flag… <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag… <arg>");
505 assert_snapshot!("-f --flag <arg>…".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>…");
506 assert_snapshot!("myflag: -f".parse::<SpecFlag>().unwrap(), @"myflag: -f");
507 assert_snapshot!("myflag: -f --flag <arg>".parse::<SpecFlag>().unwrap(), @"myflag: -f --flag <arg>");
508 }
509
510 #[test]
511 fn test_flag_with_env() {
512 let spec = Spec::parse(
513 &Default::default(),
514 r#"
515flag "--color" env="MYCLI_COLOR" help="Enable color output"
516flag "--verbose" env="MYCLI_VERBOSE"
517 "#,
518 )
519 .unwrap();
520
521 assert_snapshot!(spec, @r#"
522 flag --color help="Enable color output" env=MYCLI_COLOR
523 flag --verbose env=MYCLI_VERBOSE
524 "#);
525
526 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
527 assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
528
529 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
530 assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
531 }
532
533 #[test]
534 fn test_flag_with_env_child_node() {
535 let spec = Spec::parse(
536 &Default::default(),
537 r#"
538flag "--color" help="Enable color output" {
539 env "MYCLI_COLOR"
540}
541flag "--verbose" {
542 env "MYCLI_VERBOSE"
543}
544 "#,
545 )
546 .unwrap();
547
548 assert_snapshot!(spec, @r#"
549 flag --color help="Enable color output" env=MYCLI_COLOR
550 flag --verbose env=MYCLI_VERBOSE
551 "#);
552
553 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
554 assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
555
556 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
557 assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
558 }
559
560 #[test]
561 fn test_flag_with_boolean_defaults() {
562 let spec = Spec::parse(
563 &Default::default(),
564 r#"
565flag "--color" default=#true
566flag "--verbose" default=#false
567flag "--debug" default="true"
568flag "--quiet" default="false"
569 "#,
570 )
571 .unwrap();
572
573 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
574 assert_eq!(color_flag.default, vec!["true".to_string()]);
575
576 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
577 assert_eq!(verbose_flag.default, vec!["false".to_string()]);
578
579 let debug_flag = spec.cmd.flags.iter().find(|f| f.name == "debug").unwrap();
580 assert_eq!(debug_flag.default, vec!["true".to_string()]);
581
582 let quiet_flag = spec.cmd.flags.iter().find(|f| f.name == "quiet").unwrap();
583 assert_eq!(quiet_flag.default, vec!["false".to_string()]);
584 }
585
586 #[test]
587 fn test_flag_with_boolean_defaults_child_node() {
588 let spec = Spec::parse(
589 &Default::default(),
590 r#"
591flag "--color" {
592 default #true
593}
594flag "--verbose" {
595 default #false
596}
597 "#,
598 )
599 .unwrap();
600
601 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
602 assert_eq!(color_flag.default, vec!["true".to_string()]);
603
604 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
605 assert_eq!(verbose_flag.default, vec!["false".to_string()]);
606 }
607
608 #[test]
609 fn test_flag_with_single_default() {
610 let spec = Spec::parse(
611 &Default::default(),
612 r#"
613flag "--foo <foo>" var=#true default="bar"
614 "#,
615 )
616 .unwrap();
617
618 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
619 assert!(flag.var);
620 assert_eq!(flag.default, vec!["bar".to_string()]);
621 }
622
623 #[test]
624 fn test_flag_with_multiple_defaults_child_node() {
625 let spec = Spec::parse(
626 &Default::default(),
627 r#"
628flag "--foo <foo>" var=#true {
629 default {
630 "xyz"
631 "bar"
632 }
633}
634 "#,
635 )
636 .unwrap();
637
638 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
639 assert!(flag.var);
640 assert_eq!(flag.default, vec!["xyz".to_string(), "bar".to_string()]);
641 }
642
643 #[test]
644 fn test_flag_with_single_default_child_node() {
645 let spec = Spec::parse(
646 &Default::default(),
647 r#"
648flag "--foo <foo>" var=#true {
649 default "bar"
650}
651 "#,
652 )
653 .unwrap();
654
655 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
656 assert!(flag.var);
657 assert_eq!(flag.default, vec!["bar".to_string()]);
658 }
659
660 #[test]
661 fn test_flag_default_serialization_single() {
662 let spec = Spec::parse(
663 &Default::default(),
664 r#"
665flag "--foo <foo>" default="bar"
666 "#,
667 )
668 .unwrap();
669
670 let output = spec.to_string();
672 assert!(output.contains("default=bar") || output.contains(r#"default="bar""#));
673 }
674
675 #[test]
676 fn test_flag_default_serialization_multiple() {
677 let spec = Spec::parse(
678 &Default::default(),
679 r#"
680flag "--foo <foo>" var=#true {
681 default {
682 "xyz"
683 "bar"
684 }
685}
686 "#,
687 )
688 .unwrap();
689
690 let output = spec.to_string();
692 assert!(output.contains("default {"));
694 }
695}