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