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