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::arg::SpecDoubleDashChoices;
11use crate::spec::builder::SpecFlagBuilder;
12use crate::spec::context::ParsingContext;
13use crate::spec::effect::{SpecCommandEffect, EFFECT_VALUES};
14use crate::spec::helpers::{string_entry, NodeHelper};
15use crate::spec::is_false;
16use crate::{string, SpecArg, SpecChoices};
17
18#[derive(Debug, Default, Clone, Serialize)]
35#[non_exhaustive]
36pub struct SpecFlag {
37 pub name: String,
39 pub usage: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub help: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub help_long: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub help_md: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub help_first_line: Option<String>,
53 pub short: Vec<char>,
55 pub long: Vec<String>,
57 #[serde(skip_serializing_if = "is_false")]
59 pub required: bool,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub deprecated: Option<String>,
63 #[serde(skip_serializing_if = "is_false")]
65 pub var: bool,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub var_min: Option<usize>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub var_max: Option<usize>,
72 pub hide: bool,
74 pub global: bool,
76 #[serde(skip_serializing_if = "is_false")]
78 pub count: bool,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub arg: Option<SpecArg>,
82 #[serde(skip_serializing_if = "Vec::is_empty")]
84 pub default: Vec<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub negate: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
91 pub effect: Option<SpecCommandEffect>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub env: Option<String>,
95}
96
97impl SpecFlag {
98 pub fn builder() -> SpecFlagBuilder {
100 SpecFlagBuilder::new()
101 }
102
103 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
104 let mut flag: Self = node.arg(0)?.ensure_string()?.parse()?;
105 let mut allow_hyphen_values = false;
106 for (k, v) in node.props() {
107 match k {
108 "help" => flag.help = Some(v.ensure_string()?),
109 "long_help" => flag.help_long = Some(v.ensure_string()?),
110 "help_long" => flag.help_long = Some(v.ensure_string()?),
111 "help_md" => flag.help_md = Some(v.ensure_string()?),
112 "required" => flag.required = v.ensure_bool()?,
113 "var" => flag.var = v.ensure_bool()?,
114 "var_min" => flag.var_min = v.ensure_usize().map(Some)?,
115 "var_max" => flag.var_max = v.ensure_usize().map(Some)?,
116 "hide" => flag.hide = v.ensure_bool()?,
117 "deprecated" => {
118 flag.deprecated = match v.value.as_bool() {
119 Some(true) => Some("deprecated".into()),
120 Some(false) => None,
121 None => Some(v.ensure_string()?),
122 }
123 }
124 "global" => flag.global = v.ensure_bool()?,
125 "count" => flag.count = v.ensure_bool()?,
126 "allow_hyphen_values" => allow_hyphen_values = v.ensure_bool()?,
127 "default" => {
128 let default_value = match v.value.as_bool() {
130 Some(b) => b.to_string(),
131 None => v.ensure_string()?,
132 };
133 flag.default = vec![default_value];
134 }
135 "negate" => flag.negate = v.ensure_string().map(Some)?,
136 "effect" => {
137 let raw = v.ensure_string()?;
138 match raw.parse() {
139 Ok(effect) => flag.effect = Some(effect),
140 Err(_) => bail_parse!(
141 ctx,
142 v.entry.span(),
143 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
144 ),
145 }
146 }
147 "env" => flag.env = v.ensure_string().map(Some)?,
148 k => bail_parse!(ctx, v.entry.span(), "unsupported flag key {k}"),
149 }
150 }
151 if !flag.default.is_empty() {
152 flag.required = false;
153 }
154 for child in node.children() {
155 match child.name() {
156 "arg" => flag.arg = Some(SpecArg::parse(ctx, &child)?),
157 "help" => flag.help = Some(child.arg(0)?.ensure_string()?),
158 "long_help" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
159 "help_long" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
160 "help_md" => flag.help_md = Some(child.arg(0)?.ensure_string()?),
161 "required" => flag.required = child.arg(0)?.ensure_bool()?,
162 "var" => flag.var = child.arg(0)?.ensure_bool()?,
163 "var_min" => flag.var_min = child.arg(0)?.ensure_usize().map(Some)?,
164 "var_max" => flag.var_max = child.arg(0)?.ensure_usize().map(Some)?,
165 "hide" => flag.hide = child.arg(0)?.ensure_bool()?,
166 "deprecated" => {
167 flag.deprecated = match child.arg(0)?.ensure_bool() {
168 Ok(true) => Some("deprecated".into()),
169 Ok(false) => None,
170 _ => Some(child.arg(0)?.ensure_string()?),
171 }
172 }
173 "global" => flag.global = child.arg(0)?.ensure_bool()?,
174 "count" => flag.count = child.arg(0)?.ensure_bool()?,
175 "allow_hyphen_values" => {
176 allow_hyphen_values = child.arg(0)?.ensure_bool()?;
177 }
178 "default" => {
179 let children = child.children();
184 if children.is_empty() {
185 let arg = child.arg(0)?;
187 let default_value = match arg.value.as_bool() {
188 Some(b) => b.to_string(),
189 None => arg.ensure_string()?,
190 };
191 flag.default = vec![default_value];
192 } else {
193 flag.default = children.iter().map(|c| c.name().to_string()).collect();
196 }
197 }
198 "effect" => {
199 let arg = child.arg(0)?;
200 let raw = arg.ensure_string()?;
201 match raw.parse() {
202 Ok(effect) => flag.effect = Some(effect),
203 Err(_) => bail_parse!(
204 ctx,
205 arg.entry.span(),
206 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
207 ),
208 }
209 }
210 "env" => flag.env = child.arg(0)?.ensure_string().map(Some)?,
211 "choices" => {
212 if let Some(arg) = &mut flag.arg {
213 arg.choices = Some(SpecChoices::parse(ctx, &child)?);
214 } else {
215 bail_parse!(
216 ctx,
217 child.node.name().span(),
218 "flag must have value to have choices"
219 )
220 }
221 }
222 k => bail_parse!(ctx, child.node.name().span(), "unsupported flag child {k}"),
223 }
224 }
225 if allow_hyphen_values {
226 flag.set_allow_hyphen_values(ctx, node.node.name().span(), true)?;
227 }
228 flag.usage = flag.usage();
229 flag.help_first_line = flag.help.as_ref().map(|s| string::first_line(s));
230 Ok(flag)
231 }
232 pub fn allow_hyphen_values(&self) -> bool {
233 self.arg
234 .as_ref()
235 .is_some_and(|arg| arg.double_dash == SpecDoubleDashChoices::Automatic)
236 }
237
238 pub(crate) fn set_allow_hyphen_values(
239 &mut self,
240 ctx: &ParsingContext,
241 span: miette::SourceSpan,
242 allow: bool,
243 ) -> Result<()> {
244 if let Some(arg) = &mut self.arg {
245 arg.double_dash = if allow {
246 SpecDoubleDashChoices::Automatic
247 } else if arg.double_dash == SpecDoubleDashChoices::Automatic {
248 SpecDoubleDashChoices::Optional
249 } else {
250 arg.double_dash.clone()
251 };
252 Ok(())
253 } else if allow {
254 bail_parse!(ctx, span, "flag must have value to allow hyphen values")
255 } else {
256 Ok(())
257 }
258 }
259
260 pub fn usage(&self) -> String {
261 let mut parts = vec![];
262 let name = get_name_from_short_and_long(&self.short, &self.long).unwrap_or_default();
263 if name != self.name {
264 parts.push(format!("{}:", self.name));
265 }
266 if let Some(short) = self.short.first() {
267 parts.push(format!("-{short}"));
268 }
269 if let Some(long) = self.long.first() {
270 parts.push(format!("--{long}"));
271 }
272 let mut out = parts.join(" ");
273 if self.var {
274 out = format!("{out}…");
275 }
276 if let Some(arg) = &self.arg {
277 out = format!("{} {}", out, arg.usage());
278 }
279 out
280 }
281}
282
283impl From<&SpecFlag> for KdlNode {
284 fn from(flag: &SpecFlag) -> KdlNode {
285 let mut node = KdlNode::new("flag");
286 let name = flag
287 .short
288 .iter()
289 .map(|c| format!("-{c}"))
290 .chain(flag.long.iter().map(|s| format!("--{s}")))
291 .collect_vec()
292 .join(" ");
293 node.push(KdlEntry::new(name));
294 if let Some(desc) = &flag.help {
295 node.push(string_entry(Some("help"), desc));
296 }
297 if let Some(desc) = &flag.help_long {
298 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
299 let mut node = KdlNode::new("long_help");
300 node.push(string_entry(None, desc));
301 children.nodes_mut().push(node);
302 }
303 if let Some(desc) = &flag.help_md {
304 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
305 let mut node = KdlNode::new("help_md");
306 node.push(string_entry(None, desc));
307 children.nodes_mut().push(node);
308 }
309 if flag.required {
310 node.push(KdlEntry::new_prop("required", true));
311 }
312 if flag.var {
313 node.push(KdlEntry::new_prop("var", true));
314 }
315 if let Some(var_min) = flag.var_min {
316 node.push(KdlEntry::new_prop("var_min", var_min as i128));
317 }
318 if let Some(var_max) = flag.var_max {
319 node.push(KdlEntry::new_prop("var_max", var_max as i128));
320 }
321 if flag.hide {
322 node.push(KdlEntry::new_prop("hide", true));
323 }
324 if flag.global {
325 node.push(KdlEntry::new_prop("global", true));
326 }
327 if flag.count {
328 node.push(KdlEntry::new_prop("count", true));
329 }
330 if flag.allow_hyphen_values() {
331 node.push(KdlEntry::new_prop("allow_hyphen_values", true));
332 }
333 if let Some(negate) = &flag.negate {
334 node.push(string_entry(Some("negate"), negate));
335 }
336 if let Some(env) = &flag.env {
337 node.push(string_entry(Some("env"), env));
338 }
339 if let Some(effect) = &flag.effect {
340 node.push(string_entry(Some("effect"), effect.as_str()));
341 }
342 if let Some(deprecated) = &flag.deprecated {
343 node.push(string_entry(Some("deprecated"), deprecated));
344 }
345 if !flag.default.is_empty() {
347 if flag.default.len() == 1 {
348 node.push(KdlEntry::new_prop("default", flag.default[0].clone()));
350 } else {
351 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
353 let mut default_node = KdlNode::new("default");
354 let default_children = default_node
355 .children_mut()
356 .get_or_insert_with(KdlDocument::new);
357 for val in &flag.default {
358 default_children
359 .nodes_mut()
360 .push(KdlNode::new(val.as_str()));
361 }
362 children.nodes_mut().push(default_node);
363 }
364 }
365 if let Some(arg) = &flag.arg {
366 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
367 if flag.allow_hyphen_values() {
368 let mut arg = arg.clone();
369 arg.double_dash = SpecDoubleDashChoices::Optional;
370 children.nodes_mut().push((&arg).into());
371 } else {
372 children.nodes_mut().push(arg.into());
373 }
374 }
375 node
376 }
377}
378
379impl FromStr for SpecFlag {
380 type Err = UsageErr;
381 fn from_str(input: &str) -> Result<Self> {
382 let mut flag = Self::default();
383 let input = input.replace("...", "…").replace("…", " … ");
384 for part in input.split_whitespace() {
385 if let Some(name) = part.strip_suffix(':') {
386 flag.name = name.to_string();
387 } else if let Some(long) = part.strip_prefix("--") {
388 flag.long.push(long.to_string());
389 } else if let Some(short) = part.strip_prefix('-') {
390 if short.len() != 1 {
391 return Err(InvalidFlag {
392 token: format!("-{short}"),
393 reason: "short flags must be a single character (use -- for long flags)"
394 .to_string(),
395 span: (0, input.len()).into(),
396 input: input.to_string(),
397 });
398 }
399 flag.short.push(short.chars().next().unwrap());
400 } else if part == "…" {
401 if let Some(arg) = &mut flag.arg {
402 arg.var = true;
403 } else {
404 flag.var = true;
405 }
406 } else if part.starts_with('<') && part.ends_with('>')
407 || part.starts_with('[') && part.ends_with(']')
408 {
409 flag.arg = Some(part.to_string().parse()?);
410 } else {
411 return Err(InvalidFlag {
412 token: part.to_string(),
413 reason: "unexpected token (expected -x, --long, <arg>, or [arg])".to_string(),
414 span: (0, input.len()).into(),
415 input: input.to_string(),
416 });
417 }
418 }
419 if flag.name.is_empty() {
420 flag.name = get_name_from_short_and_long(&flag.short, &flag.long).unwrap_or_default();
421 }
422 flag.usage = flag.usage();
423 Ok(flag)
424 }
425}
426
427#[cfg(feature = "clap")]
428impl From<&clap::Arg> for SpecFlag {
429 fn from(c: &clap::Arg) -> Self {
430 let required = c.is_required_set();
431 let help = c.get_help().map(|s| s.to_string());
432 let help_long = c.get_long_help().map(|s| s.to_string());
433 let help_first_line = help.as_ref().map(|s| string::first_line(s));
434 let hide = c.is_hide_set();
435 let var = matches!(
436 c.get_action(),
437 clap::ArgAction::Count | clap::ArgAction::Append
438 );
439 let default: Vec<String> = c
440 .get_default_values()
441 .iter()
442 .map(|s| s.to_string_lossy().to_string())
443 .collect();
444 let short = c.get_short_and_visible_aliases().unwrap_or_default();
445 let long = c
446 .get_long_and_visible_aliases()
447 .unwrap_or_default()
448 .into_iter()
449 .map(|s| s.to_string())
450 .collect::<Vec<_>>();
451 let name = get_name_from_short_and_long(&short, &long).unwrap_or_default();
452 let arg = if let clap::ArgAction::Set | clap::ArgAction::Append = c.get_action() {
453 let mut arg = SpecArg::from(
454 c.get_value_names()
455 .map(|s| s.iter().map(|s| s.to_string()).join(" "))
456 .unwrap_or(name.clone())
457 .as_str(),
458 );
459
460 let choices = c
461 .get_possible_values()
462 .iter()
463 .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
464 .collect::<Vec<_>>();
465 if !choices.is_empty() {
466 arg.choices = Some(SpecChoices {
467 choices,
468 ..Default::default()
469 });
470 }
471
472 Some(arg)
473 } else {
474 None
475 };
476 let mut flag = Self {
477 name,
478 usage: "".into(),
479 short,
480 long,
481 required,
482 help,
483 help_long,
484 help_md: None,
485 help_first_line,
486 var,
487 var_min: None,
488 var_max: None,
489 hide,
490 global: c.is_global_set(),
491 arg,
492 count: matches!(c.get_action(), clap::ArgAction::Count),
493 default,
494 deprecated: None,
495 negate: None,
496 effect: None,
499 env: None,
500 };
501 if c.is_allow_hyphen_values_set() {
502 if let Some(arg) = &mut flag.arg {
503 arg.double_dash = SpecDoubleDashChoices::Automatic;
504 }
505 }
506 flag
507 }
508}
509
510impl Display for SpecFlag {
557 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558 write!(f, "{}", self.usage())
559 }
560}
561impl PartialEq for SpecFlag {
562 fn eq(&self, other: &Self) -> bool {
563 self.name == other.name
564 }
565}
566impl Eq for SpecFlag {}
567impl Hash for SpecFlag {
568 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
569 self.name.hash(state);
570 }
571}
572
573fn get_name_from_short_and_long(short: &[char], long: &[String]) -> Option<String> {
574 long.first()
575 .map(|s| s.to_string())
576 .or_else(|| short.first().map(|c| c.to_string()))
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::Spec;
583 use insta::assert_snapshot;
584
585 #[test]
586 fn from_str() {
587 assert_snapshot!("-f".parse::<SpecFlag>().unwrap(), @"-f");
588 assert_snapshot!("--flag".parse::<SpecFlag>().unwrap(), @"--flag");
589 assert_snapshot!("-f --flag".parse::<SpecFlag>().unwrap(), @"-f --flag");
590 assert_snapshot!("-f --flag…".parse::<SpecFlag>().unwrap(), @"-f --flag…");
591 assert_snapshot!("-f --flag …".parse::<SpecFlag>().unwrap(), @"-f --flag…");
592 assert_snapshot!("--flag <arg>".parse::<SpecFlag>().unwrap(), @"--flag <arg>");
593 assert_snapshot!("-f --flag <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>");
594 assert_snapshot!("-f --flag… <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag… <arg>");
595 assert_snapshot!("-f --flag <arg>…".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>…");
596 assert_snapshot!("myflag: -f".parse::<SpecFlag>().unwrap(), @"myflag: -f");
597 assert_snapshot!("myflag: -f --flag <arg>".parse::<SpecFlag>().unwrap(), @"myflag: -f --flag <arg>");
598 }
599
600 #[test]
601 fn test_flag_with_env() {
602 let spec = Spec::parse(
603 &Default::default(),
604 r#"
605flag "--color" env="MYCLI_COLOR" help="Enable color output"
606flag "--verbose" env="MYCLI_VERBOSE"
607 "#,
608 )
609 .unwrap();
610
611 assert_snapshot!(spec, @r#"
612 flag --color help="Enable color output" env=MYCLI_COLOR
613 flag --verbose env=MYCLI_VERBOSE
614 "#);
615
616 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
617 assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
618
619 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
620 assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
621 }
622
623 #[test]
624 fn test_flag_with_env_child_node() {
625 let spec = Spec::parse(
626 &Default::default(),
627 r#"
628flag "--color" help="Enable color output" {
629 env "MYCLI_COLOR"
630}
631flag "--verbose" {
632 env "MYCLI_VERBOSE"
633}
634 "#,
635 )
636 .unwrap();
637
638 assert_snapshot!(spec, @r#"
639 flag --color help="Enable color output" env=MYCLI_COLOR
640 flag --verbose env=MYCLI_VERBOSE
641 "#);
642
643 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
644 assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
645
646 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
647 assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
648 }
649
650 #[test]
651 fn test_flag_with_boolean_defaults() {
652 let spec = Spec::parse(
653 &Default::default(),
654 r#"
655flag "--color" default=#true
656flag "--verbose" default=#false
657flag "--debug" default="true"
658flag "--quiet" default="false"
659 "#,
660 )
661 .unwrap();
662
663 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
664 assert_eq!(color_flag.default, vec!["true".to_string()]);
665
666 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
667 assert_eq!(verbose_flag.default, vec!["false".to_string()]);
668
669 let debug_flag = spec.cmd.flags.iter().find(|f| f.name == "debug").unwrap();
670 assert_eq!(debug_flag.default, vec!["true".to_string()]);
671
672 let quiet_flag = spec.cmd.flags.iter().find(|f| f.name == "quiet").unwrap();
673 assert_eq!(quiet_flag.default, vec!["false".to_string()]);
674 }
675
676 #[test]
677 fn test_flag_with_boolean_defaults_child_node() {
678 let spec = Spec::parse(
679 &Default::default(),
680 r#"
681flag "--color" {
682 default #true
683}
684flag "--verbose" {
685 default #false
686}
687 "#,
688 )
689 .unwrap();
690
691 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
692 assert_eq!(color_flag.default, vec!["true".to_string()]);
693
694 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
695 assert_eq!(verbose_flag.default, vec!["false".to_string()]);
696 }
697
698 #[test]
699 fn test_flag_with_single_default() {
700 let spec = Spec::parse(
701 &Default::default(),
702 r#"
703flag "--foo <foo>" var=#true default="bar"
704 "#,
705 )
706 .unwrap();
707
708 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
709 assert!(flag.var);
710 assert_eq!(flag.default, vec!["bar".to_string()]);
711 }
712
713 #[test]
714 fn test_flag_with_multiple_defaults_child_node() {
715 let spec = Spec::parse(
716 &Default::default(),
717 r#"
718flag "--foo <foo>" var=#true {
719 default {
720 "xyz"
721 "bar"
722 }
723}
724 "#,
725 )
726 .unwrap();
727
728 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
729 assert!(flag.var);
730 assert_eq!(flag.default, vec!["xyz".to_string(), "bar".to_string()]);
731 }
732
733 #[test]
734 fn test_flag_with_single_default_child_node() {
735 let spec = Spec::parse(
736 &Default::default(),
737 r#"
738flag "--foo <foo>" var=#true {
739 default "bar"
740}
741 "#,
742 )
743 .unwrap();
744
745 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
746 assert!(flag.var);
747 assert_eq!(flag.default, vec!["bar".to_string()]);
748 }
749
750 #[test]
751 fn test_flag_default_serialization_single() {
752 let spec = Spec::parse(
753 &Default::default(),
754 r#"
755flag "--foo <foo>" default="bar"
756 "#,
757 )
758 .unwrap();
759
760 let output = spec.to_string();
762 assert!(output.contains("default=bar") || output.contains(r#"default="bar""#));
763 }
764
765 #[test]
766 fn test_flag_default_serialization_multiple() {
767 let spec = Spec::parse(
768 &Default::default(),
769 r#"
770flag "--foo <foo>" var=#true {
771 default {
772 "xyz"
773 "bar"
774 }
775}
776 "#,
777 )
778 .unwrap();
779
780 let output = spec.to_string();
782 assert!(output.contains("default {"));
784 }
785}