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