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