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