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, SpecAdmonition, SpecAdmonitionKind, SpecArg, SpecChoices, SpecRequiredIfEq};
17
18#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
20#[serde(rename_all = "snake_case")]
21pub enum SpecFlagAction {
22 #[default]
23 Set,
24 Help,
25 HelpShort,
26 HelpLong,
27 HelpAll,
28 Version,
29}
30
31impl SpecFlagAction {
32 fn parse(value: &str) -> Option<Self> {
33 Some(match value {
34 "set" => Self::Set,
35 "help" => Self::Help,
36 "help_short" => Self::HelpShort,
37 "help_long" => Self::HelpLong,
38 "help_all" => Self::HelpAll,
39 "version" => Self::Version,
40 _ => return None,
41 })
42 }
43
44 pub fn as_str(self) -> &'static str {
45 match self {
46 Self::Set => "set",
47 Self::Help => "help",
48 Self::HelpShort => "help_short",
49 Self::HelpLong => "help_long",
50 Self::HelpAll => "help_all",
51 Self::Version => "version",
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
62pub struct SpecRequiresIf {
63 pub value: String,
65 pub requires: String,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83pub struct SpecDefaultIf {
84 pub selector: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
89 pub when: Option<String>,
90 pub value: String,
92}
93
94#[derive(Debug, Default, Clone, Serialize)]
111#[non_exhaustive]
112pub struct SpecFlag {
113 pub name: String,
115 pub usage: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub help: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub help_long: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub help_md: Option<String>,
126 #[serde(skip_serializing_if = "Vec::is_empty")]
128 pub admonitions: Vec<SpecAdmonition>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub help_first_line: Option<String>,
132 pub short: Vec<char>,
134 #[serde(skip_serializing_if = "Vec::is_empty")]
136 pub hidden_short_aliases: Vec<char>,
137 pub long: Vec<String>,
139 #[serde(skip_serializing_if = "Vec::is_empty")]
141 pub hidden_aliases: Vec<String>,
142 #[serde(skip_serializing_if = "is_false")]
144 pub required: bool,
145 #[serde(skip_serializing_if = "Vec::is_empty")]
147 pub required_if: Vec<String>,
148 #[serde(skip_serializing_if = "Vec::is_empty")]
150 pub required_if_eq: Vec<SpecRequiredIfEq>,
151 #[serde(skip_serializing_if = "Vec::is_empty")]
153 pub required_if_eq_all: Vec<SpecRequiredIfEq>,
154 #[serde(skip_serializing_if = "Vec::is_empty")]
156 pub required_unless: Vec<String>,
157 #[serde(skip_serializing_if = "Vec::is_empty")]
159 pub required_unless_all: Vec<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub deprecated: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub deprecated_warn_at: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub deprecated_remove_at: Option<String>,
169 #[serde(skip_serializing_if = "is_false")]
171 pub var: bool,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub var_min: Option<usize>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub var_max: Option<usize>,
178 pub hide: bool,
180 #[serde(skip_serializing_if = "is_false")]
182 pub hide_default_value: bool,
183 #[serde(skip_serializing_if = "is_false")]
185 pub hide_env: bool,
186 #[serde(skip_serializing_if = "is_false")]
188 pub hide_env_values: bool,
189 #[serde(skip_serializing_if = "is_false")]
191 pub hide_possible_values: bool,
192 #[serde(skip_serializing_if = "is_false")]
194 pub hide_short_help: bool,
195 #[serde(skip_serializing_if = "is_false")]
197 pub hide_long_help: bool,
198 pub global: bool,
200 #[serde(skip_serializing_if = "is_false")]
202 pub count: bool,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub arg: Option<SpecArg>,
206 #[serde(skip_serializing_if = "Vec::is_empty")]
208 pub default: Vec<String>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub negate: Option<String>,
212 #[serde(skip_serializing_if = "Vec::is_empty")]
214 pub overrides: Vec<String>,
215 #[serde(skip_serializing_if = "Vec::is_empty")]
222 pub conflicts: Vec<String>,
223 #[serde(skip_serializing_if = "Vec::is_empty")]
236 pub requires: Vec<String>,
237 #[serde(skip_serializing_if = "Vec::is_empty")]
242 pub requires_if: Vec<SpecRequiresIf>,
243 #[serde(skip_serializing_if = "Vec::is_empty")]
250 pub default_if: Vec<SpecDefaultIf>,
251 #[serde(skip_serializing_if = "is_false")]
258 pub exclusive: bool,
259 #[serde(skip_serializing_if = "is_false")]
263 pub require_equals: bool,
264 #[serde(skip_serializing_if = "is_false")]
269 pub value_optional: bool,
270 #[serde(skip_serializing_if = "is_false")]
275 pub bool_value: bool,
276 #[serde(skip_serializing_if = "Option::is_none")]
286 pub default_missing: Option<String>,
287 #[serde(skip_serializing_if = "Option::is_none")]
290 pub effect: Option<SpecCommandEffect>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub env: Option<String>,
294 #[serde(skip_serializing_if = "Vec::is_empty")]
296 pub env_fallback: Vec<String>,
297 #[serde(skip_serializing_if = "Vec::is_empty")]
299 pub deprecated_env: Vec<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
307 pub help_heading: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub surface: Option<String>,
311 #[serde(skip_serializing_if = "Vec::is_empty")]
313 pub available_if: Vec<String>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub display_order: Option<usize>,
317 #[serde(skip_serializing_if = "is_set_action")]
319 pub action: SpecFlagAction,
320 #[serde(skip_serializing_if = "is_false")]
322 pub builtin: bool,
323}
324
325fn is_set_action(action: &SpecFlagAction) -> bool {
326 *action == SpecFlagAction::Set
327}
328
329impl SpecFlag {
330 pub fn builder() -> SpecFlagBuilder {
332 SpecFlagBuilder::new()
333 }
334
335 pub fn env_names(&self) -> impl Iterator<Item = &str> {
337 self.env
338 .iter()
339 .map(String::as_str)
340 .chain(self.env_fallback.iter().map(String::as_str))
341 .chain(self.deprecated_env.iter().map(String::as_str))
342 }
343
344 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
345 let mut flag: Self = node.arg(0)?.ensure_string()?.parse()?;
346 let mut allow_hyphen_values = false;
347 let mut allow_negative_numbers = false;
348 let mut value_terminator: Option<String> = None;
349 let mut delimiter: Option<String> = None;
350 for (k, v) in node.props() {
351 match k {
352 "help" => flag.help = Some(v.ensure_string()?),
353 "long_help" => flag.help_long = Some(v.ensure_string()?),
354 "help_long" => flag.help_long = Some(v.ensure_string()?),
355 "help_md" => flag.help_md = Some(v.ensure_string()?),
356 "required" => flag.required = v.ensure_bool()?,
357 "required_if" => flag.required_if = vec![v.ensure_string()?],
358 "required_unless" => flag.required_unless = vec![v.ensure_string()?],
359 "required_unless_all" => flag.required_unless_all = vec![v.ensure_string()?],
360 "var" => flag.var = v.ensure_bool()?,
361 "var_min" => flag.var_min = v.ensure_usize().map(Some)?,
362 "var_max" => flag.var_max = v.ensure_usize().map(Some)?,
363 "hide" => flag.hide = v.ensure_bool()?,
364 "hide_default_value" => flag.hide_default_value = v.ensure_bool()?,
365 "hide_env" => flag.hide_env = v.ensure_bool()?,
366 "hide_env_values" => flag.hide_env_values = v.ensure_bool()?,
367 "hide_possible_values" => flag.hide_possible_values = v.ensure_bool()?,
368 "hide_short_help" => flag.hide_short_help = v.ensure_bool()?,
369 "hide_long_help" => flag.hide_long_help = v.ensure_bool()?,
370 "deprecated" => {
371 flag.deprecated = match v.value.as_bool() {
372 Some(true) => Some("deprecated".into()),
373 Some(false) => None,
374 None => Some(v.ensure_string()?),
375 }
376 }
377 "deprecated_warn_at" => flag.deprecated_warn_at = Some(v.ensure_string()?),
378 "deprecated_remove_at" => flag.deprecated_remove_at = Some(v.ensure_string()?),
379 "global" => flag.global = v.ensure_bool()?,
380 "count" => flag.count = v.ensure_bool()?,
381 "action" => {
382 let raw = v.ensure_string()?;
383 let Some(action) = SpecFlagAction::parse(&raw) else {
384 bail_parse!(ctx, v.entry.span(), "unsupported flag action {raw}");
385 };
386 flag.action = action;
387 }
388 "builtin" => flag.builtin = v.ensure_bool()?,
389 "allow_hyphen_values" => allow_hyphen_values = v.ensure_bool()?,
390 "allow_negative_numbers" => allow_negative_numbers = v.ensure_bool()?,
391 "value_terminator" => value_terminator = Some(v.ensure_string()?),
392 "default" => {
393 let default_value = match v.value.as_bool() {
395 Some(b) => b.to_string(),
396 None => v.ensure_string()?,
397 };
398 flag.default = vec![default_value];
399 }
400 "negate" => flag.negate = v.ensure_string().map(Some)?,
401 "overrides" => flag.overrides = vec![v.ensure_string()?],
402 "conflicts" => flag.conflicts = vec![v.ensure_string()?],
403 "requires" => flag.requires = vec![v.ensure_string()?],
404 "exclusive" => flag.exclusive = v.ensure_bool()?,
405 "require_equals" => flag.require_equals = v.ensure_bool()?,
406 "value_optional" => flag.value_optional = v.ensure_bool()?,
407 "bool_value" => flag.bool_value = v.ensure_bool()?,
408 "default_missing" => flag.default_missing = Some(v.ensure_string()?),
409 "delimiter" => delimiter = Some(v.ensure_string()?),
413 "effect" => {
414 let raw = v.ensure_string()?;
415 match raw.parse() {
416 Ok(effect) => flag.effect = Some(effect),
417 Err(_) => bail_parse!(
418 ctx,
419 v.entry.span(),
420 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
421 ),
422 }
423 }
424 "env" => flag.env = v.ensure_string().map(Some)?,
425 "env_fallback" => flag.env_fallback = vec![v.ensure_string()?],
426 "deprecated_env" => flag.deprecated_env = vec![v.ensure_string()?],
427 "help_heading" => flag.help_heading = v.ensure_string().map(Some)?,
428 "surface" => flag.surface = v.ensure_string().map(Some)?,
429 "available_if" => flag.available_if = vec![v.ensure_string()?],
430 "display_order" => flag.display_order = v.ensure_usize().map(Some)?,
431 k => bail_parse!(ctx, v.entry.span(), "unsupported flag key {k}"),
432 }
433 }
434 if !flag.default.is_empty() {
435 flag.required = false;
436 }
437 for child in node.children() {
438 match child.name() {
439 "arg" => flag.arg = Some(SpecArg::parse(ctx, &child)?),
440 "help" => flag.help = Some(child.arg(0)?.ensure_string()?),
441 "long_help" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
442 "help_long" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
443 "help_md" => flag.help_md = Some(child.arg(0)?.ensure_string()?),
444 "note" => flag
445 .admonitions
446 .push(SpecAdmonition::note(child.arg(0)?.ensure_string()?)),
447 "warning" => flag
448 .admonitions
449 .push(SpecAdmonition::warning(child.arg(0)?.ensure_string()?)),
450 "required" => flag.required = child.arg(0)?.ensure_bool()?,
451 "required_if" => {
452 flag.required_if = child
453 .ensure_arg_len(1..)?
454 .args()
455 .map(|arg| arg.ensure_string())
456 .collect::<Result<Vec<_>>>()?;
457 }
458 "required_if_eq" => {
459 child.ensure_arg_len(2..=2)?;
460 flag.required_if_eq.push(SpecRequiredIfEq {
461 selector: child.arg(0)?.ensure_string()?,
462 value: child.arg(1)?.ensure_string()?,
463 });
464 }
465 "required_if_eq_all" => {
466 let entries = child.args().collect::<Vec<_>>();
467 if entries.len() < 2 || entries.len() % 2 != 0 {
468 bail_parse!(
469 ctx,
470 child.node.name().span(),
471 "required_if_eq_all needs selector/value pairs"
472 );
473 }
474 flag.required_if_eq_all = entries
475 .chunks_exact(2)
476 .map(|pair| {
477 Ok(SpecRequiredIfEq {
478 selector: pair[0].ensure_string()?,
479 value: pair[1].ensure_string()?,
480 })
481 })
482 .collect::<Result<Vec<_>>>()?;
483 }
484 "required_unless" => {
485 flag.required_unless = child
486 .ensure_arg_len(1..)?
487 .args()
488 .map(|arg| arg.ensure_string())
489 .collect::<Result<Vec<_>>>()?;
490 }
491 "required_unless_all" => {
492 flag.required_unless_all = child
493 .ensure_arg_len(1..)?
494 .args()
495 .map(|arg| arg.ensure_string())
496 .collect::<Result<Vec<_>>>()?;
497 }
498 "var" => flag.var = child.arg(0)?.ensure_bool()?,
499 "var_min" => flag.var_min = child.arg(0)?.ensure_usize().map(Some)?,
500 "var_max" => flag.var_max = child.arg(0)?.ensure_usize().map(Some)?,
501 "hide" => flag.hide = child.arg(0)?.ensure_bool()?,
502 "hide_default_value" => flag.hide_default_value = child.arg(0)?.ensure_bool()?,
503 "hide_env" => flag.hide_env = child.arg(0)?.ensure_bool()?,
504 "hide_env_values" => flag.hide_env_values = child.arg(0)?.ensure_bool()?,
505 "hide_possible_values" => {
506 flag.hide_possible_values = child.arg(0)?.ensure_bool()?
507 }
508 "hide_short_help" => flag.hide_short_help = child.arg(0)?.ensure_bool()?,
509 "hide_long_help" => flag.hide_long_help = child.arg(0)?.ensure_bool()?,
510 "deprecated" => {
511 flag.deprecated = match child.arg(0)?.ensure_bool() {
512 Ok(true) => Some("deprecated".into()),
513 Ok(false) => None,
514 _ => Some(child.arg(0)?.ensure_string()?),
515 }
516 }
517 "deprecated_warn_at" => {
518 flag.deprecated_warn_at = Some(child.arg(0)?.ensure_string()?)
519 }
520 "deprecated_remove_at" => {
521 flag.deprecated_remove_at = Some(child.arg(0)?.ensure_string()?)
522 }
523 "global" => flag.global = child.arg(0)?.ensure_bool()?,
524 "count" => flag.count = child.arg(0)?.ensure_bool()?,
525 "action" => {
526 let arg = child.arg(0)?;
527 let raw = arg.ensure_string()?;
528 let Some(action) = SpecFlagAction::parse(&raw) else {
529 bail_parse!(ctx, arg.entry.span(), "unsupported flag action {raw}");
530 };
531 flag.action = action;
532 }
533 "builtin" => flag.builtin = child.arg(0)?.ensure_bool()?,
534 "allow_hyphen_values" => {
535 allow_hyphen_values = child.arg(0)?.ensure_bool()?;
536 }
537 "allow_negative_numbers" => {
538 allow_negative_numbers = child.arg(0)?.ensure_bool()?;
539 }
540 "value_terminator" => {
541 value_terminator = Some(child.arg(0)?.ensure_string()?);
542 }
543 "default" => {
544 let children = child.children();
549 if children.is_empty() {
550 let arg = child.arg(0)?;
552 let default_value = match arg.value.as_bool() {
553 Some(b) => b.to_string(),
554 None => arg.ensure_string()?,
555 };
556 flag.default = vec![default_value];
557 } else {
558 flag.default = children.iter().map(|c| c.name().to_string()).collect();
561 }
562 }
563 "effect" => {
564 let arg = child.arg(0)?;
565 let raw = arg.ensure_string()?;
566 match raw.parse() {
567 Ok(effect) => flag.effect = Some(effect),
568 Err(_) => bail_parse!(
569 ctx,
570 arg.entry.span(),
571 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
572 ),
573 }
574 }
575 "env" => flag.env = child.arg(0)?.ensure_string().map(Some)?,
576 "env_fallback" => {
577 flag.env_fallback = child
578 .ensure_arg_len(1..)?
579 .args()
580 .map(|entry| entry.ensure_string())
581 .collect::<Result<_>>()?;
582 }
583 "deprecated_env" => {
584 flag.deprecated_env = child
585 .ensure_arg_len(1..)?
586 .args()
587 .map(|entry| entry.ensure_string())
588 .collect::<Result<_>>()?;
589 }
590 "help_heading" => {
591 flag.help_heading = child.arg(0)?.ensure_string().map(Some)?;
592 }
593 "surface" => flag.surface = child.arg(0)?.ensure_string().map(Some)?,
594 "available_if" => {
595 flag.available_if = child
596 .ensure_arg_len(1..)?
597 .args()
598 .map(|entry| entry.ensure_string())
599 .collect::<Result<_>>()?;
600 }
601 "display_order" => {
602 flag.display_order = child.arg(0)?.ensure_usize().map(Some)?;
603 }
604 "alias" => {
605 let hide = child
606 .get("hide")
607 .map(|entry| entry.ensure_bool())
608 .unwrap_or(Ok(false))?;
609 for entry in child.ensure_arg_len(1..)?.args() {
610 let spelling = entry.ensure_string()?;
611 if let Some(long) = spelling.strip_prefix("--") {
612 if !flag.long.iter().any(|existing| existing == long) {
613 flag.long.push(long.to_string());
614 }
615 if hide && !flag.hidden_aliases.iter().any(|existing| existing == long)
616 {
617 flag.hidden_aliases.push(long.to_string());
618 }
619 } else if let Some(short) = spelling.strip_prefix('-') {
620 let mut chars = short.chars();
621 let Some(short) = chars.next().filter(|_| chars.next().is_none())
622 else {
623 bail_parse!(
624 ctx,
625 entry.entry.span(),
626 "a short flag alias must be exactly one character"
627 );
628 };
629 if !flag.short.contains(&short) {
630 flag.short.push(short);
631 }
632 if hide && !flag.hidden_short_aliases.contains(&short) {
633 flag.hidden_short_aliases.push(short);
634 }
635 } else {
636 bail_parse!(
637 ctx,
638 entry.entry.span(),
639 "flag aliases must begin with - or --"
640 );
641 }
642 }
643 }
644 "conflicts" => {
645 flag.conflicts = child
646 .ensure_arg_len(1..)?
647 .args()
648 .map(|arg| arg.ensure_string())
649 .collect::<Result<Vec<_>>>()?;
650 }
651 "overrides" => {
652 flag.overrides = child
653 .ensure_arg_len(1..)?
654 .args()
655 .map(|arg| arg.ensure_string())
656 .collect::<Result<Vec<_>>>()?;
657 }
658 "exclusive" => flag.exclusive = child.arg(0)?.ensure_bool()?,
659 "require_equals" => flag.require_equals = child.arg(0)?.ensure_bool()?,
660 "value_optional" => flag.value_optional = child.arg(0)?.ensure_bool()?,
661 "bool_value" => flag.bool_value = child.arg(0)?.ensure_bool()?,
662 "default_missing" => {
663 flag.default_missing = Some(child.arg(0)?.ensure_string()?);
664 }
665 "requires" => {
666 flag.requires = child
667 .ensure_arg_len(1..)?
668 .args()
669 .map(|arg| arg.ensure_string())
670 .collect::<Result<Vec<_>>>()?;
671 }
672 "requires_if" => {
673 child.ensure_arg_len(2..=2)?;
674 flag.requires_if.push(SpecRequiresIf {
675 value: child.arg(0)?.ensure_string()?,
676 requires: child.arg(1)?.ensure_string()?,
677 });
678 }
679 "default_if" => {
680 child.ensure_arg_len(2..=3)?;
681 let count = child.args().count();
682 flag.default_if.push(if count == 2 {
683 SpecDefaultIf {
684 selector: child.arg(0)?.ensure_string()?,
685 when: None,
686 value: child.arg(1)?.ensure_string()?,
687 }
688 } else {
689 SpecDefaultIf {
690 selector: child.arg(0)?.ensure_string()?,
691 when: Some(child.arg(1)?.ensure_string()?),
692 value: child.arg(2)?.ensure_string()?,
693 }
694 });
695 }
696 "choices" => {
697 if let Some(arg) = &mut flag.arg {
698 arg.choices = Some(SpecChoices::parse(ctx, &child)?);
699 } else {
700 bail_parse!(
701 ctx,
702 child.node.name().span(),
703 "flag must have value to have choices"
704 )
705 }
706 }
707 k => bail_parse!(ctx, child.node.name().span(), "unsupported flag child {k}"),
708 }
709 }
710 if allow_hyphen_values {
711 flag.set_allow_hyphen_values(ctx, node.node.name().span(), true)?;
712 }
713 if allow_negative_numbers {
714 let Some(arg) = flag.arg.as_mut() else {
715 bail_parse!(
716 ctx,
717 node.node.name().span(),
718 "flag must have value to allow negative numbers"
719 );
720 };
721 arg.allow_negative_numbers = true;
722 }
723 if let Some(terminator) = value_terminator {
724 let Some(arg) = flag.arg.as_mut() else {
725 bail_parse!(
726 ctx,
727 node.node.name().span(),
728 "flag must have a variadic value to have a value terminator"
729 );
730 };
731 if !arg.var {
732 bail_parse!(
733 ctx,
734 node.node.name().span(),
735 "value_terminator requires a variadic flag value"
736 );
737 }
738 if terminator.is_empty() {
739 bail_parse!(
740 ctx,
741 node.node.name().span(),
742 "value_terminator cannot be empty"
743 );
744 }
745 arg.value_terminator = Some(terminator);
746 }
747 if flag.require_equals && flag.arg.is_none() {
748 bail_parse!(
749 ctx,
750 node.node.name().span(),
751 "flag must have value to require equals"
752 );
753 }
754 if flag.value_optional && flag.arg.is_none() {
755 bail_parse!(
756 ctx,
757 node.node.name().span(),
758 "flag must have a value to make that value optional"
759 );
760 }
761 if flag.bool_value
762 && (flag.arg.is_some() || flag.count || flag.action != SpecFlagAction::Set)
763 {
764 bail_parse!(
765 ctx,
766 node.node.name().span(),
767 "bool_value is only valid on a boolean switch"
768 );
769 }
770 if flag.default_missing.is_some() && flag.arg.is_none() {
771 bail_parse!(
772 ctx,
773 node.node.name().span(),
774 "flag must have value to have a default when missing"
775 );
776 }
777 if flag.default_missing.is_some() {
780 if let Some(arg) = flag.arg.as_mut() {
781 arg.required = false;
782 }
783 }
784 if let Some(raw) = delimiter {
785 let mut chars = raw.chars();
786 let Some(delimiter) = chars.next().filter(|_| chars.next().is_none()) else {
787 bail_parse!(
788 ctx,
789 node.node.name().span(),
790 "a delimiter is one character, and {raw:?} is not"
791 );
792 };
793 if !delimiter.is_ascii() {
797 bail_parse!(
798 ctx,
799 node.node.name().span(),
800 "a delimiter is one byte, and {delimiter:?} is more than one; use an \
801 ASCII separator"
802 );
803 }
804 let Some(arg) = flag.arg.as_mut() else {
805 bail_parse!(
806 ctx,
807 node.node.name().span(),
808 "`delimiter` splits a value, and flag --{} takes none",
809 flag.name
810 );
811 };
812 arg.delimiter = Some(delimiter);
813 }
814 if flag.arg.as_ref().is_some_and(|a| a.delimiter.is_some()) && !flag.var {
819 let takes_several = flag.arg.as_ref().is_some_and(|a| a.var);
820 if !takes_several {
821 bail_parse!(
822 ctx,
823 node.node.name().span(),
824 "flag --{} has a delimiter and holds one value; add `var=#true` for \
825 the values it splits into",
826 flag.name
827 );
828 }
829 }
830 if flag.action != SpecFlagAction::Set && flag.arg.is_some() {
831 bail_parse!(
832 ctx,
833 node.node.name().span(),
834 "a help or version action does not take a value"
835 );
836 }
837 flag.usage = flag.usage();
838 flag.help_first_line = flag.help.as_ref().map(|s| string::first_line(s));
839 Ok(flag)
840 }
841 pub fn allow_hyphen_values(&self) -> bool {
842 self.arg
843 .as_ref()
844 .is_some_and(|arg| arg.double_dash == SpecDoubleDashChoices::Automatic)
845 }
846
847 pub(crate) fn set_allow_hyphen_values(
848 &mut self,
849 ctx: &ParsingContext,
850 span: miette::SourceSpan,
851 allow: bool,
852 ) -> Result<()> {
853 if let Some(arg) = &mut self.arg {
854 arg.double_dash = if allow {
855 SpecDoubleDashChoices::Automatic
856 } else if arg.double_dash == SpecDoubleDashChoices::Automatic {
857 SpecDoubleDashChoices::Optional
858 } else {
859 arg.double_dash.clone()
860 };
861 Ok(())
862 } else if allow {
863 bail_parse!(ctx, span, "flag must have value to allow hyphen values")
864 } else {
865 Ok(())
866 }
867 }
868
869 pub fn usage(&self) -> String {
870 let mut parts = vec![];
871 let name = get_name_from_short_and_long(&self.short, &self.long).unwrap_or_default();
872 let negation_only = self.short.is_empty()
876 && self.long.is_empty()
877 && self
878 .negate
879 .as_deref()
880 .is_some_and(|negate| negate.trim_start_matches('-') == self.name);
881 if negation_only {
882 parts.push(self.negate.clone().unwrap_or_default());
883 } else if name != self.name {
884 parts.push(format!("{}:", self.name));
885 }
886 if let Some(short) = self.short.first() {
887 parts.push(format!("-{short}"));
888 }
889 if let Some(long) = self.long.first() {
890 parts.push(format!("--{long}"));
891 }
892 let mut out = parts.join(" ");
893 if self.var {
894 out = format!("{out}…");
895 }
896 if let Some(arg) = &self.arg {
897 let usage = arg.usage();
898 if self.require_equals && (self.value_optional || !arg.required) {
899 out = format!("{out}{}", optional_equals_usage(&usage));
900 } else {
901 let separator = if self.require_equals { "=" } else { " " };
902 out = format!("{out}{separator}{usage}");
903 }
904 }
905 out
906 }
907}
908
909pub(crate) fn optional_equals_usage(usage: &str) -> String {
910 let (value, closing) = if let Some(value) = usage.strip_prefix('[') {
911 (value, ']')
912 } else if let Some(value) = usage.strip_prefix('<') {
913 (value, '>')
914 } else {
915 return format!("={usage}");
916 };
917 let Some(end) = value.find(closing) else {
918 return format!("={usage}");
919 };
920 format!("[={}]{}", &value[..end], &value[end + 1..])
921}
922
923impl From<&SpecFlag> for KdlNode {
924 fn from(flag: &SpecFlag) -> KdlNode {
925 let mut node = KdlNode::new("flag");
926 let visible_shorts = flag
927 .short
928 .iter()
929 .filter(|short| !flag.hidden_short_aliases.contains(short));
930 let visible_longs = flag
931 .long
932 .iter()
933 .filter(|long| !flag.hidden_aliases.contains(long));
934 let inferred_matches = visible_longs
935 .clone()
936 .next()
937 .is_some_and(|long| long == &flag.name)
938 || (visible_longs.clone().next().is_none()
939 && visible_shorts
940 .clone()
941 .next()
942 .is_some_and(|short| short.to_string() == flag.name));
943 let forms = visible_shorts
944 .map(|c| format!("-{c}"))
945 .chain(visible_longs.map(|s| format!("--{s}")))
946 .collect_vec()
947 .join(" ");
948 let declaration = if inferred_matches {
949 forms
950 } else if forms.is_empty() {
951 format!("{}:", flag.name)
952 } else {
953 format!("{}: {forms}", flag.name)
954 };
955 node.push(KdlEntry::new(declaration));
956 if let Some(desc) = &flag.help {
957 node.push(string_entry(Some("help"), desc));
958 }
959 if !flag.hidden_aliases.is_empty() || !flag.hidden_short_aliases.is_empty() {
960 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
961 let mut aliases = KdlNode::new("alias");
962 for alias in &flag.hidden_short_aliases {
963 aliases.push(string_entry(None, &format!("-{alias}")));
964 }
965 for alias in &flag.hidden_aliases {
966 aliases.push(string_entry(None, &format!("--{alias}")));
967 }
968 aliases.push(KdlEntry::new_prop("hide", true));
969 children.nodes_mut().push(aliases);
970 }
971 if let Some(desc) = &flag.help_long {
972 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
973 let mut node = KdlNode::new("long_help");
974 node.push(string_entry(None, desc));
975 children.nodes_mut().push(node);
976 }
977 if let Some(desc) = &flag.help_md {
978 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
979 let mut node = KdlNode::new("help_md");
980 node.push(string_entry(None, desc));
981 children.nodes_mut().push(node);
982 }
983 for admonition in &flag.admonitions {
984 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
985 let name = match admonition.kind {
986 SpecAdmonitionKind::Note => "note",
987 SpecAdmonitionKind::Warning => "warning",
988 };
989 let mut block = KdlNode::new(name);
990 block.push(string_entry(None, &admonition.text));
991 children.nodes_mut().push(block);
992 }
993 if flag.required {
994 node.push(KdlEntry::new_prop("required", true));
995 }
996 serialize_flag_list(&mut node, "required_if", &flag.required_if);
997 for condition in &flag.required_if_eq {
998 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
999 let mut relation = KdlNode::new("required_if_eq");
1000 relation.push(string_entry(None, &condition.selector));
1001 relation.push(string_entry(None, &condition.value));
1002 children.nodes_mut().push(relation);
1003 }
1004 if !flag.required_if_eq_all.is_empty() {
1005 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1006 let mut relation = KdlNode::new("required_if_eq_all");
1007 for condition in &flag.required_if_eq_all {
1008 relation.push(string_entry(None, &condition.selector));
1009 relation.push(string_entry(None, &condition.value));
1010 }
1011 children.nodes_mut().push(relation);
1012 }
1013 serialize_flag_list(&mut node, "required_unless", &flag.required_unless);
1014 serialize_flag_list(&mut node, "required_unless_all", &flag.required_unless_all);
1015 if flag.var {
1016 node.push(KdlEntry::new_prop("var", true));
1017 }
1018 if let Some(var_min) = flag.var_min {
1019 node.push(KdlEntry::new_prop("var_min", var_min as i128));
1020 }
1021 if let Some(var_max) = flag.var_max {
1022 node.push(KdlEntry::new_prop("var_max", var_max as i128));
1023 }
1024 if flag.hide {
1025 node.push(KdlEntry::new_prop("hide", true));
1026 }
1027 for (name, hidden) in [
1028 ("hide_default_value", flag.hide_default_value),
1029 ("hide_env", flag.hide_env),
1030 ("hide_env_values", flag.hide_env_values),
1031 ("hide_possible_values", flag.hide_possible_values),
1032 ("hide_short_help", flag.hide_short_help),
1033 ("hide_long_help", flag.hide_long_help),
1034 ] {
1035 if hidden {
1036 node.push(KdlEntry::new_prop(name, true));
1037 }
1038 }
1039 if flag.global {
1040 node.push(KdlEntry::new_prop("global", true));
1041 }
1042 if flag.count {
1043 node.push(KdlEntry::new_prop("count", true));
1044 }
1045 if flag.action != SpecFlagAction::Set {
1046 node.push(string_entry(Some("action"), flag.action.as_str()));
1047 }
1048 if flag.builtin {
1049 node.push(KdlEntry::new_prop("builtin", true));
1050 }
1051 if flag.allow_hyphen_values() {
1052 node.push(KdlEntry::new_prop("allow_hyphen_values", true));
1053 }
1054 if flag
1055 .arg
1056 .as_ref()
1057 .is_some_and(|arg| arg.allow_negative_numbers)
1058 {
1059 node.push(KdlEntry::new_prop("allow_negative_numbers", true));
1060 }
1061 if let Some(terminator) = flag
1062 .arg
1063 .as_ref()
1064 .and_then(|arg| arg.value_terminator.as_deref())
1065 {
1066 node.push(string_entry(Some("value_terminator"), terminator));
1067 }
1068 if let Some(negate) = &flag.negate {
1069 node.push(string_entry(Some("negate"), negate));
1070 }
1071 if flag.overrides.len() == 1 {
1072 node.push(string_entry(Some("overrides"), &flag.overrides[0]));
1073 } else if !flag.overrides.is_empty() {
1074 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1075 let mut overrides = KdlNode::new("overrides");
1076 for target in &flag.overrides {
1077 overrides.push(string_entry(None, target));
1078 }
1079 children.nodes_mut().push(overrides);
1080 }
1081 if flag.conflicts.len() == 1 {
1082 node.push(string_entry(Some("conflicts"), &flag.conflicts[0]));
1083 } else if !flag.conflicts.is_empty() {
1084 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1085 let mut conflicts = KdlNode::new("conflicts");
1086 for target in &flag.conflicts {
1087 conflicts.push(string_entry(None, target));
1088 }
1089 children.nodes_mut().push(conflicts);
1090 }
1091 if flag.requires.len() == 1 {
1092 node.push(string_entry(Some("requires"), &flag.requires[0]));
1093 } else if !flag.requires.is_empty() {
1094 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1095 let mut requires = KdlNode::new("requires");
1096 for target in &flag.requires {
1097 requires.push(string_entry(None, target));
1098 }
1099 children.nodes_mut().push(requires);
1100 }
1101 for condition in &flag.requires_if {
1102 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1103 let mut requires_if = KdlNode::new("requires_if");
1104 requires_if.push(string_entry(None, &condition.value));
1105 requires_if.push(string_entry(None, &condition.requires));
1106 children.nodes_mut().push(requires_if);
1107 }
1108 for condition in &flag.default_if {
1109 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1110 let mut default_if = KdlNode::new("default_if");
1111 default_if.push(string_entry(None, &condition.selector));
1112 if let Some(when) = &condition.when {
1113 default_if.push(string_entry(None, when));
1114 }
1115 default_if.push(string_entry(None, &condition.value));
1116 children.nodes_mut().push(default_if);
1117 }
1118 if flag.exclusive {
1119 node.push(KdlEntry::new_prop("exclusive", true));
1120 }
1121 if flag.require_equals {
1122 node.push(KdlEntry::new_prop("require_equals", true));
1123 }
1124 if flag.value_optional {
1125 node.push(KdlEntry::new_prop("value_optional", true));
1126 }
1127 if flag.bool_value {
1128 node.push(KdlEntry::new_prop("bool_value", true));
1129 }
1130 if let Some(missing) = &flag.default_missing {
1131 node.push(string_entry(Some("default_missing"), missing));
1132 }
1133 if let Some(env) = &flag.env {
1134 node.push(string_entry(Some("env"), env));
1135 }
1136 serialize_flag_list(&mut node, "env_fallback", &flag.env_fallback);
1137 serialize_flag_list(&mut node, "deprecated_env", &flag.deprecated_env);
1138 if let Some(help_heading) = &flag.help_heading {
1139 node.push(string_entry(Some("help_heading"), help_heading));
1140 }
1141 if let Some(surface) = &flag.surface {
1142 node.push(string_entry(Some("surface"), surface));
1143 }
1144 serialize_flag_list(&mut node, "available_if", &flag.available_if);
1145 if let Some(order) = flag.display_order {
1146 node.push(KdlEntry::new_prop("display_order", order as i128));
1147 }
1148 if let Some(effect) = &flag.effect {
1149 node.push(string_entry(Some("effect"), effect.as_str()));
1150 }
1151 if let Some(deprecated) = &flag.deprecated {
1152 node.push(string_entry(Some("deprecated"), deprecated));
1153 }
1154 if let Some(at) = &flag.deprecated_warn_at {
1155 node.push(string_entry(Some("deprecated_warn_at"), at));
1156 }
1157 if let Some(at) = &flag.deprecated_remove_at {
1158 node.push(string_entry(Some("deprecated_remove_at"), at));
1159 }
1160 if !flag.default.is_empty() {
1162 if flag.default.len() == 1 {
1163 node.push(KdlEntry::new_prop("default", flag.default[0].clone()));
1165 } else {
1166 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1168 let mut default_node = KdlNode::new("default");
1169 let default_children = default_node
1170 .children_mut()
1171 .get_or_insert_with(KdlDocument::new);
1172 for val in &flag.default {
1173 default_children
1174 .nodes_mut()
1175 .push(KdlNode::new(val.as_str()));
1176 }
1177 children.nodes_mut().push(default_node);
1178 }
1179 }
1180 if let Some(arg) = &flag.arg {
1181 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1182 if flag.allow_hyphen_values() {
1183 let mut arg = arg.clone();
1184 arg.double_dash = SpecDoubleDashChoices::Optional;
1185 children.nodes_mut().push((&arg).into());
1186 } else {
1187 children.nodes_mut().push(arg.into());
1188 }
1189 }
1190 node
1191 }
1192}
1193
1194fn serialize_flag_list(node: &mut KdlNode, name: &str, flags: &[String]) {
1195 if flags.len() == 1 {
1196 node.push(string_entry(Some(name), &flags[0]));
1197 } else if !flags.is_empty() {
1198 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1199 let mut list = KdlNode::new(name);
1200 for flag in flags {
1201 list.push(string_entry(None, flag));
1202 }
1203 children.nodes_mut().push(list);
1204 }
1205}
1206
1207impl FromStr for SpecFlag {
1208 type Err = UsageErr;
1209 fn from_str(input: &str) -> Result<Self> {
1210 let mut flag = Self::default();
1211 let input = input
1215 .replace("...", "…")
1216 .replace("…[=", "\u{e000}[=")
1217 .replace("…=", "\u{e000}=")
1218 .replace("…", " … ")
1219 .replace('\u{e000}', "…");
1220 for part in input.split_whitespace() {
1221 if let Some((form, value)) = part
1222 .strip_suffix(']')
1223 .and_then(|part| part.split_once("[="))
1224 {
1225 let (form, repeatable) = form
1226 .strip_suffix('…')
1227 .map_or((form, false), |form| (form, true));
1228 let recognized = if let Some(long) = form.strip_prefix("--") {
1229 if long.is_empty() {
1230 false
1231 } else {
1232 flag.long.push(long.to_string());
1233 true
1234 }
1235 } else if let Some(short) = form.strip_prefix('-') {
1236 if short.chars().count() != 1 {
1237 return Err(InvalidFlag {
1238 token: form.to_string(),
1239 reason:
1240 "short flags must be a single character (use -- for long flags)"
1241 .to_string(),
1242 span: (0, input.len()).into(),
1243 input: input.to_string(),
1244 });
1245 }
1246 flag.short.push(short.chars().next().unwrap());
1247 true
1248 } else {
1249 false
1250 };
1251 if recognized && !value.is_empty() {
1252 flag.var |= repeatable;
1253 flag.require_equals = true;
1254 flag.arg = Some(match flag.arg.take() {
1255 Some(existing) => format!("{} [{value}]", existing.usage()).parse()?,
1256 None => format!("[{value}]").parse()?,
1257 });
1258 continue;
1259 }
1260 }
1261 if let Some((form, value)) = part.split_once('=') {
1262 let (form, repeatable) = form
1263 .strip_suffix('…')
1264 .map_or((form, false), |form| (form, true));
1265 let recognized = if let Some(long) = form.strip_prefix("--") {
1266 if long.is_empty() {
1267 false
1268 } else {
1269 flag.long.push(long.to_string());
1270 true
1271 }
1272 } else if let Some(short) = form.strip_prefix('-') {
1273 if short.chars().count() != 1 {
1274 return Err(InvalidFlag {
1275 token: form.to_string(),
1276 reason:
1277 "short flags must be a single character (use -- for long flags)"
1278 .to_string(),
1279 span: (0, input.len()).into(),
1280 input: input.to_string(),
1281 });
1282 }
1283 flag.short.push(short.chars().next().unwrap());
1284 true
1285 } else {
1286 false
1287 };
1288 if recognized {
1289 flag.var |= repeatable;
1290 if !(value.starts_with('<') && value.ends_with('>')
1291 || value.starts_with('[') && value.ends_with(']'))
1292 {
1293 return Err(InvalidFlag {
1294 token: part.to_string(),
1295 reason: "an equals sign must attach <arg> or [arg]".to_string(),
1296 span: (0, input.len()).into(),
1297 input: input.to_string(),
1298 });
1299 }
1300 flag.require_equals = true;
1301 flag.arg = Some(match flag.arg.take() {
1302 Some(existing) => format!("{} {value}", existing.usage()).parse()?,
1303 None => value.to_string().parse()?,
1304 });
1305 continue;
1306 }
1307 }
1308 if let Some(name) = part.strip_suffix(':') {
1309 flag.name = name.to_string();
1310 } else if let Some(long) = part.strip_prefix("--") {
1311 flag.long.push(long.to_string());
1312 } else if let Some(short) = part.strip_prefix('-') {
1313 if short.chars().count() != 1 {
1314 return Err(InvalidFlag {
1315 token: format!("-{short}"),
1316 reason: "short flags must be a single character (use -- for long flags)"
1317 .to_string(),
1318 span: (0, input.len()).into(),
1319 input: input.to_string(),
1320 });
1321 }
1322 flag.short.push(short.chars().next().unwrap());
1323 } else if part == "…" {
1324 if let Some(arg) = &mut flag.arg {
1325 arg.var = true;
1326 } else {
1327 flag.var = true;
1328 }
1329 } else if part.starts_with('<') && part.ends_with('>')
1330 || part.starts_with('[') && part.ends_with(']')
1331 {
1332 flag.arg = Some(match flag.arg.take() {
1333 Some(existing) => format!("{} {part}", existing.usage()).parse()?,
1334 None => part.to_string().parse()?,
1335 });
1336 } else {
1337 return Err(InvalidFlag {
1338 token: part.to_string(),
1339 reason: "unexpected token (expected -x, --long, <arg>, or [arg])".to_string(),
1340 span: (0, input.len()).into(),
1341 input: input.to_string(),
1342 });
1343 }
1344 }
1345 if flag.name.is_empty() {
1346 flag.name = get_name_from_short_and_long(&flag.short, &flag.long).unwrap_or_default();
1347 }
1348 flag.usage = flag.usage();
1349 Ok(flag)
1350 }
1351}
1352
1353#[cfg(feature = "clap")]
1354impl From<&clap::Arg> for SpecFlag {
1355 fn from(c: &clap::Arg) -> Self {
1356 let required = c.is_required_set();
1357 let help = c.get_help().map(|s| s.to_string());
1358 let help_long = c.get_long_help().map(|s| s.to_string());
1359 let help_first_line = help.as_ref().map(|s| string::first_line(s));
1360 let hide = c.is_hide_set();
1361 let var = matches!(
1362 c.get_action(),
1363 clap::ArgAction::Count | clap::ArgAction::Append
1364 );
1365 let default: Vec<String> = crate::spec::arg::default_values(c);
1366 let mut short = c.get_short_and_visible_aliases().unwrap_or_default();
1367 let visible_short = short.clone();
1368 let hidden_short_aliases = c
1369 .get_all_short_aliases()
1370 .unwrap_or_default()
1371 .into_iter()
1372 .filter(|alias| !visible_short.contains(alias))
1373 .collect::<Vec<_>>();
1374 short.extend(hidden_short_aliases.iter().copied());
1375 let mut long = c
1376 .get_long_and_visible_aliases()
1377 .unwrap_or_default()
1378 .into_iter()
1379 .map(|s| s.to_string())
1380 .collect::<Vec<_>>();
1381 let visible_long = long.clone();
1382 let hidden_aliases = c
1383 .get_all_aliases()
1384 .unwrap_or_default()
1385 .into_iter()
1386 .filter(|alias| !visible_long.iter().any(|visible| visible == alias))
1387 .map(str::to_string)
1388 .collect::<Vec<_>>();
1389 long.extend(hidden_aliases.iter().cloned());
1390 let name = get_name_from_short_and_long(&short, &long).unwrap_or_default();
1391 let negate = if matches!(c.get_action(), clap::ArgAction::SetFalse)
1396 && short.is_empty()
1397 && long.len() == 1
1398 {
1399 Some(format!("--{}", long.remove(0)))
1400 } else {
1401 None
1402 };
1403 let arg = if let clap::ArgAction::Set | clap::ArgAction::Append = c.get_action() {
1404 let value_names = crate::spec::arg::value_names_from_clap(c);
1405 let mut arg = SpecArg::from(
1406 value_names
1407 .first()
1408 .cloned()
1409 .unwrap_or_else(|| name.clone())
1410 .as_str(),
1411 );
1412 arg.value_names = value_names;
1413
1414 arg.choices = crate::spec::arg::choices_from_clap(c);
1415
1416 if let Some(delimiter) = c.get_value_delimiter() {
1431 arg.var = true;
1432 if delimiter.is_ascii() {
1438 arg.delimiter = Some(delimiter);
1439 }
1440 } else if var || c.get_num_args().is_some_and(|n| n.max_values() > 1) {
1441 arg.var = true;
1442 }
1443 arg.allow_negative_numbers = c.is_allow_negative_numbers_set();
1444 if arg.var {
1445 if let Some(terminator) = c.get_value_terminator() {
1446 arg.value_terminator = Some(terminator.to_string());
1447 }
1448 }
1449
1450 crate::spec::arg::value_bounds(c, &mut arg, true);
1453
1454 Some(arg)
1455 } else {
1456 None
1457 };
1458 let mut flag = Self {
1459 name,
1460 usage: "".into(),
1461 short,
1462 hidden_short_aliases,
1463 long,
1464 hidden_aliases,
1465 required,
1466 required_if: vec![],
1467 required_if_eq: vec![],
1468 required_if_eq_all: vec![],
1469 required_unless: vec![],
1470 required_unless_all: vec![],
1471 deprecated_warn_at: None,
1472 deprecated_remove_at: None,
1473 conflicts: vec![],
1474 requires: vec![],
1479 requires_if: vec![],
1481 default_if: vec![],
1483 exclusive: c.is_exclusive_set(),
1485 require_equals: c.is_require_equals_set(),
1486 value_optional: arg.is_some()
1487 && c.get_num_args()
1488 .is_some_and(|n| n.min_values() == 0 && n.max_values() > 0),
1489 bool_value: false,
1491 default_missing: None,
1493 help,
1494 help_long,
1495 help_md: None,
1496 admonitions: Vec::new(),
1497 help_first_line,
1498 var,
1499 var_min: None,
1500 var_max: None,
1501 hide,
1502 hide_default_value: c.is_hide_default_value_set(),
1503 hide_env: c.is_hide_env_set(),
1504 hide_env_values: c.is_hide_env_values_set(),
1505 hide_possible_values: c.is_hide_possible_values_set(),
1506 hide_short_help: c.is_hide_short_help_set(),
1507 hide_long_help: c.is_hide_long_help_set(),
1508 global: c.is_global_set(),
1509 arg,
1510 count: matches!(c.get_action(), clap::ArgAction::Count),
1511 action: match c.get_action() {
1512 clap::ArgAction::Help => SpecFlagAction::Help,
1513 clap::ArgAction::HelpShort => SpecFlagAction::HelpShort,
1514 clap::ArgAction::HelpLong => SpecFlagAction::HelpLong,
1515 clap::ArgAction::Version => SpecFlagAction::Version,
1516 _ => SpecFlagAction::Set,
1517 },
1518 builtin: false,
1519 default,
1520 deprecated: None,
1521 negate,
1522 overrides: vec![],
1523 effect: None,
1528 env: None,
1529 env_fallback: vec![],
1530 deprecated_env: vec![],
1531 help_heading: c.get_help_heading().map(|s| s.to_string()),
1532 surface: None,
1533 available_if: Vec::new(),
1534 display_order: Some(c.get_display_order()),
1535 };
1536 if c.is_allow_hyphen_values_set() {
1537 if let Some(arg) = &mut flag.arg {
1538 arg.double_dash = SpecDoubleDashChoices::Automatic;
1539 }
1540 }
1541 flag.usage = flag.usage();
1542 flag
1543 }
1544}
1545
1546impl Display for SpecFlag {
1593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1594 write!(f, "{}", self.usage())
1595 }
1596}
1597impl PartialEq for SpecFlag {
1598 fn eq(&self, other: &Self) -> bool {
1599 self.name == other.name
1600 }
1601}
1602impl Eq for SpecFlag {}
1603impl Hash for SpecFlag {
1604 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1605 self.name.hash(state);
1606 }
1607}
1608
1609fn get_name_from_short_and_long(short: &[char], long: &[String]) -> Option<String> {
1610 long.first()
1611 .map(|s| s.to_string())
1612 .or_else(|| short.first().map(|c| c.to_string()))
1613}
1614
1615#[cfg(test)]
1616mod tests {
1617 use super::*;
1618 use crate::Spec;
1619 use insta::assert_snapshot;
1620
1621 #[test]
1622 fn from_str() {
1623 assert_snapshot!("-f".parse::<SpecFlag>().unwrap(), @"-f");
1624 assert_snapshot!("--flag".parse::<SpecFlag>().unwrap(), @"--flag");
1625 assert_snapshot!("-f --flag".parse::<SpecFlag>().unwrap(), @"-f --flag");
1626 assert_snapshot!("-f --flag…".parse::<SpecFlag>().unwrap(), @"-f --flag…");
1627 assert_snapshot!("-f --flag …".parse::<SpecFlag>().unwrap(), @"-f --flag…");
1628 assert_snapshot!("--flag <arg>".parse::<SpecFlag>().unwrap(), @"--flag <arg>");
1629 assert_snapshot!("-f --flag <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>");
1630 assert_snapshot!("-f --flag… <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag… <arg>");
1631 assert_snapshot!("-f --flag <arg>…".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>…");
1632 let range = "--range <start> <end>".parse::<SpecFlag>().unwrap();
1633 let arg = range.arg.as_ref().unwrap();
1634 assert_eq!(arg.value_names, ["start", "end"]);
1635 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1636 assert_snapshot!(range, @"--range <start> <end>");
1637 assert_snapshot!("myflag: -f".parse::<SpecFlag>().unwrap(), @"myflag: -f");
1638 assert_snapshot!("myflag: -f --flag <arg>".parse::<SpecFlag>().unwrap(), @"myflag: -f --flag <arg>");
1639 }
1640
1641 #[test]
1642 fn clap_token_boundaries_survive_the_bridge() {
1643 let command = clap::Command::new("ex")
1644 .allow_negative_numbers(true)
1645 .arg(
1646 clap::Arg::new("item")
1647 .long("item")
1648 .action(clap::ArgAction::Append)
1649 .value_terminator(";"),
1650 )
1651 .arg(clap::Arg::new("number"));
1652 let spec = Spec::from(&command);
1653 let item = spec.cmd.flags[0].arg.as_ref().unwrap();
1654
1655 assert!(item.allow_negative_numbers);
1656 assert_eq!(item.value_terminator.as_deref(), Some(";"));
1657 assert!(spec.cmd.args[0].allow_negative_numbers);
1658 }
1659
1660 #[test]
1661 fn hidden_aliases_parse_bind_and_round_trip_without_becoming_visible() {
1662 let spec: Spec =
1663 "flag \"-o --output <file>\" {\n alias \"-q\" \"--quietly\" hide=#true\n}\n"
1664 .parse()
1665 .unwrap();
1666 let flag = &spec.cmd.flags[0];
1667 assert_eq!(flag.short, ['o', 'q']);
1668 assert_eq!(flag.long, ["output", "quietly"]);
1669 assert_eq!(flag.hidden_short_aliases, ['q']);
1670 assert_eq!(flag.hidden_aliases, ["quietly"]);
1671
1672 let emitted = spec.to_string();
1673 assert!(emitted.contains("flag \"-o --output\""), "{emitted}");
1674 assert!(!emitted.contains("flag \"-o -q"), "{emitted}");
1675 assert!(
1676 emitted.contains("alias \"-q\" \"--quietly\" hide=#true"),
1677 "{emitted}"
1678 );
1679 let reparsed: Spec = emitted.parse().unwrap();
1680 assert_eq!(reparsed.cmd.flags[0].short, flag.short);
1681 assert_eq!(reparsed.cmd.flags[0].long, flag.long);
1682 assert_eq!(
1683 reparsed.cmd.flags[0].hidden_short_aliases,
1684 flag.hidden_short_aliases
1685 );
1686 assert_eq!(reparsed.cmd.flags[0].hidden_aliases, flag.hidden_aliases);
1687
1688 let cmd = clap::Command::new("ex").arg(
1689 clap::Arg::new("output")
1690 .short('o')
1691 .short_alias('q')
1692 .long("output")
1693 .visible_alias("out")
1694 .alias("quietly"),
1695 );
1696 let bridged = Spec::from(&cmd);
1697 let flag = &bridged.cmd.flags[0];
1698 assert_eq!(flag.short, ['o', 'q']);
1699 assert_eq!(flag.hidden_short_aliases, ['q']);
1700 assert_eq!(flag.long, ["output", "out", "quietly"]);
1701 assert_eq!(flag.hidden_aliases, ["quietly"]);
1702 }
1703
1704 #[test]
1705 fn conflicts_round_trip_and_come_across_from_clap() {
1706 let spec: Spec = "flag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\" {\n conflicts \"--file\" \"--url\"\n}\nflag \"--url <u>\"\n"
1709 .parse()
1710 .unwrap();
1711 assert_eq!(spec.cmd.flags[0].conflicts, vec!["--stdin".to_string()]);
1712 assert_eq!(
1713 spec.cmd.flags[1].conflicts,
1714 vec!["--file".to_string(), "--url".to_string()]
1715 );
1716
1717 let reparsed: Spec = spec.to_string().parse().unwrap();
1718 assert_eq!(reparsed.cmd.flags[1].conflicts.len(), 2, "{spec}");
1719 }
1720
1721 #[test]
1722 fn requires_round_trips_in_both_spellings() {
1723 let spec: Spec = "flag \"--out <p>\" requires=\"--format\"\nflag \"--sign\" {\n requires \"--key\" \"--identity\"\n}\nflag \"--format <f>\"\nflag \"--key <k>\"\nflag \"--identity <i>\"\n"
1726 .parse()
1727 .unwrap();
1728 assert_eq!(spec.cmd.flags[0].requires, vec!["--format".to_string()]);
1729 assert_eq!(
1730 spec.cmd.flags[1].requires,
1731 vec!["--key".to_string(), "--identity".to_string()]
1732 );
1733
1734 let reparsed: Spec = spec.to_string().parse().unwrap();
1735 assert_eq!(reparsed.cmd.flags[0].requires, vec!["--format".to_string()]);
1736 assert_eq!(reparsed.cmd.flags[1].requires.len(), 2, "{spec}");
1737 }
1738
1739 #[test]
1740 fn conditional_requirements_round_trip_in_order() {
1741 let spec: Spec = "flag \"--config <file>\" {\n requires_if \"special.toml\" \"--key\"\n requires_if \"remote.toml\" \"--token\"\n}\nflag \"--key <key>\"\nflag \"--token <token>\"\n"
1742 .parse()
1743 .unwrap();
1744 assert_eq!(
1745 spec.cmd.flags[0].requires_if,
1746 [
1747 SpecRequiresIf {
1748 value: "special.toml".into(),
1749 requires: "--key".into(),
1750 },
1751 SpecRequiresIf {
1752 value: "remote.toml".into(),
1753 requires: "--token".into(),
1754 },
1755 ]
1756 );
1757
1758 let emitted = spec.to_string();
1759 let reparsed: Spec = emitted.parse().unwrap();
1760 assert_eq!(
1761 reparsed.cmd.flags[0].requires_if,
1762 spec.cmd.flags[0].requires_if
1763 );
1764 }
1765
1766 #[test]
1767 fn conditional_defaults_round_trip_in_order_and_cannot_come_across_from_clap() {
1768 let spec: Spec = "flag \"--bin-names\" {\n default_if \"--json\" \"true\"\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--json\"\nflag \"--output <fmt>\"\n"
1769 .parse()
1770 .unwrap();
1771 assert_eq!(
1772 spec.cmd.flags[0].default_if,
1773 [
1774 SpecDefaultIf {
1775 selector: "--json".into(),
1776 when: None,
1777 value: "true".into(),
1778 },
1779 SpecDefaultIf {
1780 selector: "--output".into(),
1781 when: Some("json".into()),
1782 value: "pretty".into(),
1783 },
1784 ]
1785 );
1786
1787 let emitted = spec.to_string();
1788 let reparsed: Spec = emitted.parse().unwrap();
1789 assert_eq!(
1790 reparsed.cmd.flags[0].default_if,
1791 spec.cmd.flags[0].default_if
1792 );
1793
1794 let cmd = clap::Command::new("ex")
1796 .arg(
1797 clap::Arg::new("bin-names")
1798 .long("bin-names")
1799 .action(clap::ArgAction::SetTrue)
1800 .default_value_if("json", clap::builder::ArgPredicate::IsPresent, "true"),
1801 )
1802 .arg(
1803 clap::Arg::new("json")
1804 .long("json")
1805 .action(clap::ArgAction::SetTrue),
1806 );
1807 let spec = Spec::from(&cmd);
1808 let bin_names = spec
1809 .cmd
1810 .flags
1811 .iter()
1812 .find(|f| f.name == "bin-names")
1813 .unwrap();
1814 assert!(
1815 bin_names.default_if.is_empty(),
1816 "clap exposes no getter for `default_value_if`; if this now fails, \
1817 the bridge can carry it and `SpecFlag::default_if` should say so"
1818 );
1819 }
1820
1821 #[test]
1822 fn exclusive_round_trips_and_comes_across_from_clap() {
1823 let spec: Spec = "flag \"--dump\" exclusive=#true\nflag \"--verbose\"\n"
1824 .parse()
1825 .unwrap();
1826 assert!(spec.cmd.flags[0].exclusive);
1827 assert!(!spec.cmd.flags[1].exclusive);
1828
1829 let reparsed: Spec = spec.to_string().parse().unwrap();
1830 assert!(reparsed.cmd.flags[0].exclusive, "{spec}");
1831
1832 let cmd = clap::Command::new("ex")
1835 .arg(clap::Arg::new("dump").long("dump").exclusive(true))
1836 .arg(clap::Arg::new("verbose").long("verbose"));
1837 let spec = Spec::from(&cmd);
1838 let dump = spec.cmd.flags.iter().find(|f| f.name == "dump").unwrap();
1839 assert!(dump.exclusive);
1840 let verbose = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
1841 assert!(!verbose.exclusive);
1842 }
1843
1844 #[test]
1845 fn a_single_clap_set_false_spelling_becomes_a_negative_only_flag() {
1846 let cmd = clap::Command::new("ex").arg(
1847 clap::Arg::new("color")
1848 .long("color")
1849 .action(clap::ArgAction::SetFalse),
1850 );
1851 let spec = Spec::from(&cmd);
1852 let color = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
1853 assert!(color.long.is_empty());
1854 assert!(color.short.is_empty());
1855 assert_eq!(color.negate.as_deref(), Some("--color"));
1856 assert_eq!(color.usage(), "--color");
1861 assert_eq!(color.usage, "--color");
1862
1863 let rendered = spec.to_string();
1864 assert!(
1865 rendered.contains("flag color: negate=--color"),
1866 "{rendered}"
1867 );
1868 let reparsed: Spec = rendered.parse().expect("the bridge must emit readable KDL");
1869 assert_eq!(reparsed.cmd.flags[0].negate.as_deref(), Some("--color"));
1870 }
1871
1872 #[test]
1873 fn require_equals_round_trips_and_comes_across_from_clap() {
1874 let spec: Spec = "flag \"--inspect <PORT>\" require_equals=#true\n"
1875 .parse()
1876 .unwrap();
1877 assert!(spec.cmd.flags[0].require_equals);
1878
1879 let reparsed: Spec = spec.to_string().parse().unwrap();
1880 assert!(reparsed.cmd.flags[0].require_equals, "{spec}");
1881
1882 let cmd = clap::Command::new("ex").arg(
1883 clap::Arg::new("inspect")
1884 .long("inspect")
1885 .action(clap::ArgAction::Set)
1886 .require_equals(true),
1887 );
1888 let spec = Spec::from(&cmd);
1889 let inspect = spec.cmd.flags.iter().find(|f| f.name == "inspect").unwrap();
1890 assert!(inspect.require_equals);
1891 assert_eq!(inspect.usage(), "--inspect=<inspect>");
1892 let usage_reparsed: SpecFlag = inspect.usage().parse().unwrap();
1893 assert!(usage_reparsed.require_equals);
1894 assert_eq!(usage_reparsed.long, ["inspect"]);
1895 assert_eq!(usage_reparsed.arg.unwrap().name, "inspect");
1896 assert_eq!(
1897 crate::docs::models::SpecFlag::from(inspect).usage,
1898 "--inspect=<inspect>"
1899 );
1900
1901 let cmd = clap::Command::new("ex").arg(
1902 clap::Arg::new("color")
1903 .long("color")
1904 .action(clap::ArgAction::Set)
1905 .num_args(0..=1)
1906 .require_equals(true),
1907 );
1908 let spec = Spec::from(&cmd);
1909 let color = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
1910 assert!(color.require_equals);
1911 assert!(color.value_optional);
1912 assert!(
1913 color.arg.as_ref().unwrap().required,
1914 "clap's optional arity is flag metadata, not positional presentation"
1915 );
1916 assert_eq!(color.usage(), "--color[=color]");
1917 assert_eq!(
1918 crate::docs::models::SpecFlag::from(color).usage,
1919 "--color[=color]"
1920 );
1921
1922 let optional: SpecFlag = "--color [WHEN]".parse().unwrap();
1923 let optional = SpecFlag {
1924 require_equals: true,
1925 ..optional
1926 };
1927 assert_eq!(optional.usage(), "--color[=WHEN]");
1928 let reparsed: SpecFlag = optional.usage().parse().unwrap();
1929 assert!(reparsed.require_equals);
1930 assert!(!reparsed.arg.as_ref().unwrap().required);
1931 assert_eq!(reparsed.arg.as_ref().unwrap().name, "WHEN");
1932 assert_eq!(
1933 crate::docs::models::SpecFlag::from(&optional).usage,
1934 "--color[=WHEN]"
1935 );
1936
1937 let variadic: SpecFlag = "--color [WHEN]…".parse().unwrap();
1938 let variadic = SpecFlag {
1939 require_equals: true,
1940 ..variadic
1941 };
1942 assert_eq!(variadic.usage(), "--color[=WHEN]…");
1943 assert_eq!(
1944 crate::docs::models::SpecFlag::from(&variadic).usage,
1945 "--color[=WHEN]…"
1946 );
1947 assert_eq!(
1948 variadic.usage().parse::<SpecFlag>().unwrap().usage(),
1949 "--color[=WHEN]…"
1950 );
1951
1952 let pair: SpecFlag = "--range [START] [END]".parse().unwrap();
1953 let pair = SpecFlag {
1954 require_equals: true,
1955 ..pair
1956 };
1957 assert_eq!(pair.usage(), "--range[=START] [END]");
1958 assert_eq!(
1959 crate::docs::models::SpecFlag::from(&pair).usage,
1960 "--range[=START] [END]"
1961 );
1962 assert_eq!(
1963 pair.usage().parse::<SpecFlag>().unwrap().usage(),
1964 "--range[=START] [END]"
1965 );
1966
1967 let repeatable: SpecFlag = "--tag <TAG>".parse().unwrap();
1968 let repeatable = SpecFlag {
1969 var: true,
1970 require_equals: true,
1971 ..repeatable
1972 };
1973 assert_eq!(repeatable.usage(), "--tag…=<TAG>");
1974 let reparsed: SpecFlag = repeatable.usage().parse().unwrap();
1975 assert!(reparsed.var);
1976 assert!(reparsed.require_equals);
1977 assert!(!reparsed.arg.as_ref().unwrap().var);
1978
1979 let repeatable_optional: SpecFlag = "--color [WHEN]".parse().unwrap();
1980 let repeatable_optional = SpecFlag {
1981 var: true,
1982 require_equals: true,
1983 ..repeatable_optional
1984 };
1985 assert_eq!(repeatable_optional.usage(), "--color…[=WHEN]");
1986 let reparsed: SpecFlag = repeatable_optional.usage().parse().unwrap();
1987 assert!(reparsed.var);
1988 assert!(reparsed.require_equals);
1989 assert!(!reparsed.arg.as_ref().unwrap().var);
1990 }
1991
1992 #[test]
1993 fn optional_flag_value_policy_round_trips_separately_from_help() {
1994 let spec: Spec = "flag \"--bump [LEVEL]\" value_optional=#true\n"
1995 .parse()
1996 .unwrap();
1997 let bump = &spec.cmd.flags[0];
1998 assert!(bump.value_optional);
1999 assert!(!bump.arg.as_ref().unwrap().required);
2000
2001 let rendered = spec.to_string();
2002 assert!(rendered.contains("value_optional=#true"), "{rendered}");
2003 let reparsed: Spec = rendered.parse().unwrap();
2004 assert!(reparsed.cmd.flags[0].value_optional);
2005
2006 let presentation_only: Spec = "flag \"--bump [LEVEL]\"\n".parse().unwrap();
2007 assert!(!presentation_only.cmd.flags[0].value_optional);
2008
2009 let command = clap::Command::new("ex").arg(
2010 clap::Arg::new("bump")
2011 .long("bump")
2012 .action(clap::ArgAction::Set)
2013 .num_args(0..=1),
2014 );
2015 let bridged = Spec::from(&command);
2016 assert!(bridged.cmd.flags[0].value_optional);
2017
2018 let zero_arity = clap::Command::new("ex").arg(
2019 clap::Arg::new("plain")
2020 .long("plain")
2021 .action(clap::ArgAction::Set)
2022 .num_args(0),
2023 );
2024 assert!(!Spec::from(&zero_arity).cmd.flags[0].value_optional);
2025 }
2026
2027 #[test]
2028 fn explicit_boolean_values_round_trip() {
2029 let spec: Spec = "flag \"--color\" negate=\"--no-color\" bool_value=#true\n"
2030 .parse()
2031 .unwrap();
2032 assert!(spec.cmd.flags[0].bool_value);
2033 let rendered = spec.to_string();
2034 assert!(rendered.contains("bool_value=#true"), "{rendered}");
2035 assert!(rendered.parse::<Spec>().unwrap().cmd.flags[0].bool_value);
2036
2037 for invalid in [
2038 "flag \"--jobs <N>\" bool_value=#true\n",
2039 "flag \"--verbose\" count=#true bool_value=#true\n",
2040 ] {
2041 assert!(invalid.parse::<Spec>().is_err(), "{invalid}");
2042 }
2043 }
2044
2045 #[test]
2046 fn default_missing_round_trips_and_cannot_come_across_from_clap() {
2047 let spec: Spec = "flag \"--color <WHEN>\" default_missing=\"always\"\n"
2048 .parse()
2049 .unwrap();
2050 assert_eq!(spec.cmd.flags[0].default_missing.as_deref(), Some("always"));
2051 assert!(
2052 !spec.cmd.flags[0].arg.as_ref().unwrap().required,
2053 "a missing value is optional, so help should not demand it"
2054 );
2055 assert!(
2056 spec.cmd.flags[0].usage.contains("[WHEN]")
2057 && !spec.cmd.flags[0].usage.contains("<WHEN>"),
2058 "help should show an optional value: {}",
2059 spec.cmd.flags[0].usage
2060 );
2061
2062 let reparsed: Spec = spec.to_string().parse().unwrap();
2063 assert_eq!(
2064 reparsed.cmd.flags[0].default_missing.as_deref(),
2065 Some("always"),
2066 "{spec}"
2067 );
2068
2069 let cmd = clap::Command::new("ex").arg(
2071 clap::Arg::new("color")
2072 .long("color")
2073 .action(clap::ArgAction::Set)
2074 .num_args(0..=1)
2075 .default_missing_value("always"),
2076 );
2077 let spec = Spec::from(&cmd);
2078 let color = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2079 assert!(
2080 color.default_missing.is_none(),
2081 "clap exposes no getter for `default_missing_value`; if this now fails, \
2082 the bridge can carry it and `SpecFlag::default_missing` should say so"
2083 );
2084 }
2085
2086 #[test]
2087 fn value_count_bounds_survive_the_clap_bridge() {
2088 let cmd = clap::Command::new("ex")
2089 .arg(
2090 clap::Arg::new("pair")
2091 .long("pair")
2092 .action(clap::ArgAction::Set)
2093 .num_args(2),
2094 )
2095 .arg(
2096 clap::Arg::new("files")
2097 .value_name("FILES")
2098 .required(true)
2099 .num_args(2..=4),
2100 );
2101 let spec = Spec::from(&cmd);
2102
2103 let pair_flag = spec.cmd.flags.iter().find(|f| f.name == "pair").unwrap();
2104 assert!(!pair_flag.var, "the flag itself is not repeatable");
2105 assert_eq!(pair_flag.var_min, None);
2106 assert_eq!(pair_flag.var_max, None);
2107 let pair = pair_flag.arg.as_ref().unwrap();
2108 assert!(pair.var);
2109 assert_eq!(pair.var_min, Some(2));
2110 assert_eq!(pair.var_max, Some(2));
2111
2112 let files = spec.cmd.args.iter().find(|a| a.name == "FILES").unwrap();
2113 assert!(files.var);
2114 assert_eq!(files.var_min, Some(2));
2115 assert_eq!(files.var_max, Some(4));
2116
2117 let words = ["ex", "--pair", "a", "b", "one", "two"].map(str::to_string);
2118 crate::parse(&spec, &words).expect("both clap value-count ranges are satisfied");
2119
2120 let words = ["ex", "--pair", "a", "--", "one", "two"].map(str::to_string);
2121 let err = crate::parse(&spec, &words).unwrap_err();
2122 assert!(
2123 format!("{err:?}").contains("requires at least 2 value(s), got 1"),
2124 "{err:?}"
2125 );
2126
2127 let reparsed: Spec = spec.to_string().parse().unwrap();
2128 let pair = reparsed.cmd.flags[0].arg.as_ref().unwrap();
2129 assert_eq!((pair.var_min, pair.var_max), (Some(2), Some(2)));
2130 assert_eq!(
2131 (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
2132 (Some(2), Some(4))
2133 );
2134 }
2135
2136 #[test]
2137 fn append_value_count_bounds_are_per_occurrence() {
2138 let cmd = clap::Command::new("ex").arg(
2139 clap::Arg::new("pair")
2140 .long("pair")
2141 .action(clap::ArgAction::Append)
2142 .num_args(2),
2143 );
2144 let spec = Spec::from(&cmd);
2145 let flag = &spec.cmd.flags[0];
2146 let values = flag.arg.as_ref().unwrap();
2147 assert!(flag.var);
2148 assert_eq!((values.var_min, values.var_max), (Some(2), Some(2)));
2149
2150 crate::parse(
2151 &spec,
2152 &["ex", "--pair", "a", "b", "--pair", "c", "d"].map(str::to_string),
2153 )
2154 .expect("each occurrence satisfies the fixed cardinality");
2155
2156 let err = crate::parse(
2157 &spec,
2158 &["ex", "--pair", "a", "--pair", "c", "d"].map(str::to_string),
2159 )
2160 .unwrap_err();
2161 assert!(format!("{err:?}").contains("requires at least 2 value(s), got 1"));
2162 }
2163
2164 #[test]
2165 fn ranged_value_names_do_not_emit_invalid_fixed_arity() {
2166 let cmd = clap::Command::new("ex")
2167 .arg(
2168 clap::Arg::new("range")
2169 .long("range")
2170 .action(clap::ArgAction::Set)
2171 .num_args(2..=4)
2172 .value_names(["START", "END"]),
2173 )
2174 .arg(
2175 clap::Arg::new("files")
2176 .num_args(1..=3)
2177 .value_names(["FIRST", "REST"]),
2178 );
2179 let spec = Spec::from(&cmd);
2180 assert_eq!(
2181 spec.cmd.flags[0].arg.as_ref().unwrap().value_names,
2182 ["START"]
2183 );
2184 assert_eq!(spec.cmd.args[0].value_names, ["FIRST"]);
2185 let rendered = spec.to_string();
2186 let _: Spec = rendered.parse().expect("the generated KDL must parse back");
2187 }
2188
2189 #[test]
2190 fn delimiter_value_count_bounds_are_not_mapped() {
2191 let cmd = clap::Command::new("ex")
2192 .arg(
2193 clap::Arg::new("pairs")
2194 .long("pairs")
2195 .action(clap::ArgAction::Set)
2196 .value_delimiter(',')
2197 .num_args(2),
2198 )
2199 .arg(clap::Arg::new("items").value_delimiter(',').num_args(2..=3));
2200 let spec = Spec::from(&cmd);
2201
2202 let pairs = spec.cmd.flags[0].arg.as_ref().unwrap();
2203 assert!(pairs.var);
2204 assert_eq!(pairs.delimiter, Some(','));
2205 assert_eq!((pairs.var_min, pairs.var_max), (None, None));
2206
2207 let items = &spec.cmd.args[0];
2208 assert!(items.var);
2209 assert_eq!(items.delimiter, Some(','));
2210 assert_eq!((items.var_min, items.var_max), (None, None));
2211 }
2212
2213 #[test]
2214 fn an_optional_flag_value_carries_its_policy_and_bound() {
2215 let cmd = clap::Command::new("ex").arg(
2216 clap::Arg::new("values")
2217 .long("values")
2218 .action(clap::ArgAction::Set)
2219 .num_args(0..=3),
2220 );
2221 let spec = Spec::from(&cmd);
2222 let values = spec.cmd.flags[0].arg.as_ref().unwrap();
2223
2224 assert!(spec.cmd.flags[0].value_optional);
2225 assert_eq!(values.var_min, Some(0));
2226 assert_eq!(values.var_max, Some(3));
2227 assert_eq!(spec.cmd.flags[0].var_min, None);
2228 assert_eq!(spec.cmd.flags[0].var_max, None);
2229 }
2230
2231 #[test]
2232 fn requires_cannot_come_across_from_clap() {
2233 let cmd = clap::Command::new("ex")
2239 .arg(clap::Arg::new("out").long("out").requires("format"))
2240 .arg(clap::Arg::new("format").long("format"));
2241 let spec = Spec::from(&cmd);
2242 let out = spec.cmd.flags.iter().find(|f| f.name == "out").unwrap();
2243 assert!(
2244 out.requires.is_empty(),
2245 "clap exposes no getter for `requires`; if this now fails, the bridge can \
2246 carry it and `SpecFlag::requires` should say so"
2247 );
2248 }
2249
2250 #[cfg(feature = "clap")]
2251 #[test]
2252 fn conflicts_survive_the_clap_bridge() {
2253 let cmd = clap::Command::new("ex")
2257 .arg(clap::Arg::new("file").long("file").conflicts_with("stdin"))
2258 .arg(clap::Arg::new("stdin").long("stdin"));
2259 let spec: Spec = (&cmd).into();
2260
2261 let file = spec.cmd.flags.iter().find(|f| f.name == "file").unwrap();
2262 assert_eq!(file.conflicts, vec!["--stdin".to_string()]);
2263
2264 let stdin = spec.cmd.flags.iter().find(|f| f.name == "stdin").unwrap();
2269 assert!(stdin.conflicts.is_empty());
2270
2271 let positional = clap::Command::new("ex")
2272 .arg(
2273 clap::Arg::new("from-file")
2274 .long("from-file")
2275 .conflicts_with("value"),
2276 )
2277 .arg(clap::Arg::new("value"));
2278 let spec: Spec = (&positional).into();
2279 let from_file = spec
2280 .cmd
2281 .flags
2282 .iter()
2283 .find(|f| f.name == "from-file")
2284 .unwrap();
2285 assert_eq!(from_file.conflicts, vec!["value".to_string()]);
2286
2287 let shorts = clap::Command::new("ex")
2291 .arg(clap::Arg::new("loud").long("loud").conflicts_with("quiet"))
2292 .arg(clap::Arg::new("quiet").short('q'));
2293 let spec: Spec = (&shorts).into();
2294 let loud = spec.cmd.flags.iter().find(|f| f.name == "loud").unwrap();
2295 assert_eq!(loud.conflicts, vec!["-q".to_string()]);
2296 }
2297
2298 #[test]
2299 fn a_serialized_spec_can_always_be_read_back() {
2300 let spec: Spec = "flag \"--shell <s>\" {\n required_unless \"--jobs\" \"--color\"\n overrides \"--keep\" \"--dry-run\"\n long_help \"Colored.\\u{1b}[0m Text.\"\n}\n"
2305 .parse()
2306 .unwrap();
2307
2308 let serialized = spec.to_string();
2309 let reparsed: Spec = serialized
2310 .parse()
2311 .unwrap_or_else(|e| panic!("a serialized spec should reparse: {e}\n\n{serialized}"));
2312
2313 let flag = &reparsed.cmd.flags[0];
2314 assert_eq!(
2315 flag.required_unless,
2316 vec!["--jobs".to_string(), "--color".to_string()]
2317 );
2318 assert_eq!(
2319 flag.overrides,
2320 vec!["--keep".to_string(), "--dry-run".to_string()]
2321 );
2322 assert_eq!(flag.help_long.as_deref(), Some("Colored.\u{1b}[0m Text."));
2323 }
2324
2325 #[test]
2326 fn help_heading_round_trips() {
2327 let spec: Spec = r#"
2329flag "--filter <pattern>" help_heading="Filtering"
2330flag "--exclude <pattern>" {
2331 help_heading "Filtering"
2332}
2333arg "<file>" help_heading="Input"
2334"#
2335 .parse()
2336 .unwrap();
2337 assert_eq!(spec.cmd.flags[0].help_heading.as_deref(), Some("Filtering"));
2338 assert_eq!(spec.cmd.flags[1].help_heading.as_deref(), Some("Filtering"));
2339 assert_eq!(spec.cmd.args[0].help_heading.as_deref(), Some("Input"));
2340
2341 let reparsed: Spec = spec.to_string().parse().unwrap();
2343 assert_eq!(
2344 reparsed.cmd.flags[0].help_heading.as_deref(),
2345 Some("Filtering")
2346 );
2347 assert_eq!(reparsed.cmd.args[0].help_heading.as_deref(), Some("Input"));
2348 }
2349
2350 #[cfg(feature = "clap")]
2351 #[test]
2352 fn help_heading_comes_across_from_clap() {
2353 let cmd = clap::Command::new("ex")
2356 .arg(
2357 clap::Arg::new("filter")
2358 .long("filter")
2359 .help_heading("Filtering"),
2360 )
2361 .arg(clap::Arg::new("plain").long("plain"));
2362 let spec: Spec = (&cmd).into();
2363
2364 let filter = spec
2365 .cmd
2366 .flags
2367 .iter()
2368 .find(|f| f.name == "filter")
2369 .expect("--filter should be in the spec");
2370 assert_eq!(filter.help_heading.as_deref(), Some("Filtering"));
2371
2372 let plain = spec
2373 .cmd
2374 .flags
2375 .iter()
2376 .find(|f| f.name == "plain")
2377 .expect("--plain should be in the spec");
2378 assert_eq!(plain.help_heading, None);
2379 }
2380
2381 #[test]
2382 fn test_flag_with_env() {
2383 let spec = Spec::parse(
2384 &Default::default(),
2385 r#"
2386flag "--color" env="MYCLI_COLOR" help="Enable color output"
2387flag "--verbose" env="MYCLI_VERBOSE"
2388 "#,
2389 )
2390 .unwrap();
2391
2392 assert_snapshot!(spec, @r#"
2393 flag --color help="Enable color output" env=MYCLI_COLOR
2394 flag --verbose env=MYCLI_VERBOSE
2395 "#);
2396
2397 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2398 assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
2399
2400 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2401 assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
2402 }
2403
2404 #[test]
2405 fn test_flag_with_env_child_node() {
2406 let spec = Spec::parse(
2407 &Default::default(),
2408 r#"
2409flag "--color" help="Enable color output" {
2410 env "MYCLI_COLOR"
2411}
2412flag "--verbose" {
2413 env "MYCLI_VERBOSE"
2414}
2415 "#,
2416 )
2417 .unwrap();
2418
2419 assert_snapshot!(spec, @r#"
2420 flag --color help="Enable color output" env=MYCLI_COLOR
2421 flag --verbose env=MYCLI_VERBOSE
2422 "#);
2423
2424 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2425 assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
2426
2427 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2428 assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
2429 }
2430
2431 #[test]
2432 fn test_flag_with_overrides() {
2433 let spec = Spec::parse(
2434 &Default::default(),
2435 r#"
2436flag "--file <file>" overrides="--stdin"
2437flag "--format <format>" {
2438 overrides "--json" "--yaml"
2439}
2440 "#,
2441 )
2442 .unwrap();
2443
2444 assert_eq!(spec.cmd.flags[0].overrides, ["--stdin"]);
2445 assert_eq!(spec.cmd.flags[1].overrides, ["--json", "--yaml"]);
2446
2447 let reparsed: Spec = spec.to_string().parse().unwrap();
2448 assert_eq!(reparsed.cmd.flags[0].overrides, ["--stdin"]);
2449 assert_eq!(reparsed.cmd.flags[1].overrides, ["--json", "--yaml"]);
2450 }
2451
2452 #[test]
2453 fn test_flag_with_conditional_requirements() {
2454 let spec = Spec::parse(
2455 &Default::default(),
2456 r#"
2457flag "--file <file>" required_if="--dir"
2458flag "--output <output>" {
2459 required_unless "--stdout" "--check"
2460}
2461 "#,
2462 )
2463 .unwrap();
2464
2465 assert_eq!(spec.cmd.flags[0].required_if, ["--dir"]);
2466 assert_eq!(spec.cmd.flags[1].required_unless, ["--stdout", "--check"]);
2467
2468 let reparsed: Spec = spec.to_string().parse().unwrap();
2469 assert_eq!(reparsed.cmd.flags[0].required_if, ["--dir"]);
2470 assert_eq!(
2471 reparsed.cmd.flags[1].required_unless,
2472 ["--stdout", "--check"]
2473 );
2474 }
2475
2476 #[test]
2477 fn test_flag_with_boolean_defaults() {
2478 let spec = Spec::parse(
2479 &Default::default(),
2480 r#"
2481flag "--color" default=#true
2482flag "--verbose" default=#false
2483flag "--debug" default="true"
2484flag "--quiet" default="false"
2485 "#,
2486 )
2487 .unwrap();
2488
2489 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2490 assert_eq!(color_flag.default, vec!["true".to_string()]);
2491
2492 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2493 assert_eq!(verbose_flag.default, vec!["false".to_string()]);
2494
2495 let debug_flag = spec.cmd.flags.iter().find(|f| f.name == "debug").unwrap();
2496 assert_eq!(debug_flag.default, vec!["true".to_string()]);
2497
2498 let quiet_flag = spec.cmd.flags.iter().find(|f| f.name == "quiet").unwrap();
2499 assert_eq!(quiet_flag.default, vec!["false".to_string()]);
2500 }
2501
2502 #[test]
2503 fn test_flag_with_boolean_defaults_child_node() {
2504 let spec = Spec::parse(
2505 &Default::default(),
2506 r#"
2507flag "--color" {
2508 default #true
2509}
2510flag "--verbose" {
2511 default #false
2512}
2513 "#,
2514 )
2515 .unwrap();
2516
2517 let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
2518 assert_eq!(color_flag.default, vec!["true".to_string()]);
2519
2520 let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
2521 assert_eq!(verbose_flag.default, vec!["false".to_string()]);
2522 }
2523
2524 #[test]
2525 fn test_flag_with_single_default() {
2526 let spec = Spec::parse(
2527 &Default::default(),
2528 r#"
2529flag "--foo <foo>" var=#true default="bar"
2530 "#,
2531 )
2532 .unwrap();
2533
2534 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
2535 assert!(flag.var);
2536 assert_eq!(flag.default, vec!["bar".to_string()]);
2537 }
2538
2539 #[test]
2540 fn test_flag_with_multiple_defaults_child_node() {
2541 let spec = Spec::parse(
2542 &Default::default(),
2543 r#"
2544flag "--foo <foo>" var=#true {
2545 default {
2546 "xyz"
2547 "bar"
2548 }
2549}
2550 "#,
2551 )
2552 .unwrap();
2553
2554 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
2555 assert!(flag.var);
2556 assert_eq!(flag.default, vec!["xyz".to_string(), "bar".to_string()]);
2557 }
2558
2559 #[test]
2560 fn test_flag_with_single_default_child_node() {
2561 let spec = Spec::parse(
2562 &Default::default(),
2563 r#"
2564flag "--foo <foo>" var=#true {
2565 default "bar"
2566}
2567 "#,
2568 )
2569 .unwrap();
2570
2571 let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
2572 assert!(flag.var);
2573 assert_eq!(flag.default, vec!["bar".to_string()]);
2574 }
2575
2576 #[test]
2577 fn test_flag_default_serialization_single() {
2578 let spec = Spec::parse(
2579 &Default::default(),
2580 r#"
2581flag "--foo <foo>" default="bar"
2582 "#,
2583 )
2584 .unwrap();
2585
2586 let output = spec.to_string();
2588 assert!(output.contains("default=bar") || output.contains(r#"default="bar""#));
2589 }
2590
2591 #[test]
2592 fn test_flag_default_serialization_multiple() {
2593 let spec = Spec::parse(
2594 &Default::default(),
2595 r#"
2596flag "--foo <foo>" var=#true {
2597 default {
2598 "xyz"
2599 "bar"
2600 }
2601}
2602 "#,
2603 )
2604 .unwrap();
2605
2606 let output = spec.to_string();
2608 assert!(output.contains("default {"));
2610 }
2611}