1use crate::kdl::{KdlDocument, KdlEntry, KdlNode};
2use crate::miette;
3use serde::Serialize;
4use std::fmt::Display;
5use std::hash::Hash;
6use std::str::FromStr;
7
8use crate::error::UsageErr;
9use crate::spec::builder::SpecArgBuilder;
10use crate::spec::context::ParsingContext;
11use crate::spec::effect::{EFFECT_VALUES, SpecCommandEffect};
12use crate::spec::helpers::{NodeHelper, string_entry};
13use crate::spec::is_false;
14use crate::{SpecAdmonition, SpecAdmonitionKind, SpecChoices, SpecComplete, string};
15#[cfg(feature = "clap")]
16use crate::{SpecChoice, SpecChoiceAlias};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20pub struct SpecRequiredIfEq {
21 pub selector: String,
22 pub value: String,
23}
24
25#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)]
26pub enum SpecDoubleDashChoices {
27 Automatic,
29 #[default]
31 Optional,
32 Required,
34 Preserve,
36}
37
38impl_string_enum!(SpecDoubleDashChoices {
39 SpecDoubleDashChoices::Automatic => "automatic",
40 SpecDoubleDashChoices::Optional => "optional",
41 SpecDoubleDashChoices::Required => "required",
42 SpecDoubleDashChoices::Preserve => "preserve",
43});
44
45#[derive(Debug, Default, Clone, Serialize)]
62#[non_exhaustive]
63pub struct SpecArg {
64 pub name: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub sigil: Option<String>,
69 #[serde(skip_serializing_if = "Vec::is_empty")]
72 pub value_names: Vec<String>,
73 pub usage: String,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub help: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub help_long: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub help_md: Option<String>,
84 #[serde(skip_serializing_if = "Vec::is_empty")]
86 pub admonitions: Vec<SpecAdmonition>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub help_first_line: Option<String>,
90 pub required: bool,
92 pub double_dash: SpecDoubleDashChoices,
94 #[serde(skip_serializing_if = "is_false")]
96 pub var: bool,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub var_min: Option<usize>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub var_max: Option<usize>,
103 #[serde(skip_serializing_if = "Option::is_none")]
110 pub delimiter: Option<char>,
111 #[serde(skip_serializing_if = "is_false")]
113 pub allow_negative_numbers: bool,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub value_terminator: Option<String>,
117 pub hide: bool,
119 #[serde(skip_serializing_if = "is_false")]
121 pub hide_default_value: bool,
122 #[serde(skip_serializing_if = "is_false")]
124 pub hide_env: bool,
125 #[serde(skip_serializing_if = "is_false")]
127 pub hide_env_values: bool,
128 #[serde(skip_serializing_if = "is_false")]
130 pub hide_possible_values: bool,
131 #[serde(skip_serializing_if = "is_false")]
133 pub hide_short_help: bool,
134 #[serde(skip_serializing_if = "is_false")]
136 pub hide_long_help: bool,
137 #[serde(skip_serializing_if = "Vec::is_empty")]
142 pub conflicts: Vec<String>,
143 #[serde(skip_serializing_if = "Vec::is_empty")]
145 pub requires: Vec<String>,
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 = "Vec::is_empty")]
163 pub default: Vec<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub choices: Option<SpecChoices>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub validate: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub validate_error: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
176 pub effect: Option<SpecCommandEffect>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub env: Option<String>,
180 #[serde(skip_serializing_if = "Vec::is_empty")]
182 pub env_fallback: Vec<String>,
183 #[serde(skip_serializing_if = "Vec::is_empty")]
185 pub deprecated_env: Vec<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
189 pub help_heading: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
194 pub surface: Option<String>,
195 #[serde(skip_serializing_if = "Vec::is_empty")]
199 pub available_if: Vec<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub display_order: Option<usize>,
203 #[serde(skip_serializing_if = "Option::is_none")]
209 pub complete: Option<SpecComplete>,
210}
211
212impl SpecArg {
213 pub fn builder() -> SpecArgBuilder {
215 SpecArgBuilder::new()
216 }
217
218 pub fn env_names(&self) -> impl Iterator<Item = &str> {
220 self.env
221 .iter()
222 .map(String::as_str)
223 .chain(self.env_fallback.iter().map(String::as_str))
224 .chain(self.deprecated_env.iter().map(String::as_str))
225 }
226
227 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
228 let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
229 for (k, v) in node.props() {
230 match k {
231 "help" => arg.help = Some(v.ensure_string()?),
232 "sigil" => arg.sigil = v.ensure_string().map(Some)?,
233 "long_help" => arg.help_long = Some(v.ensure_string()?),
234 "help_long" => arg.help_long = Some(v.ensure_string()?),
235 "help_md" => arg.help_md = Some(v.ensure_string()?),
236 "required" => arg.required = v.ensure_bool()?,
237 "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
238 "var" => arg.var = v.ensure_bool()?,
239 "delimiter" => {
240 let raw = v.ensure_string()?;
241 let mut chars = raw.chars();
242 match (chars.next(), chars.next()) {
243 (Some(c), None) if c.is_ascii() => arg.delimiter = Some(c),
249 (Some(c), None) => bail_parse!(
250 ctx,
251 v.entry.span(),
252 "a delimiter is one byte, and {c:?} is more than one; use an \
253 ASCII separator"
254 ),
255 _ => bail_parse!(
256 ctx,
257 v.entry.span(),
258 "a delimiter is one character, and {raw:?} is not"
259 ),
260 }
261 }
262 "allow_negative_numbers" => arg.allow_negative_numbers = v.ensure_bool()?,
263 "value_terminator" => arg.value_terminator = v.ensure_string().map(Some)?,
264 "hide" => arg.hide = v.ensure_bool()?,
265 "hide_default_value" => arg.hide_default_value = v.ensure_bool()?,
266 "hide_env" => arg.hide_env = v.ensure_bool()?,
267 "hide_env_values" => arg.hide_env_values = v.ensure_bool()?,
268 "hide_possible_values" => arg.hide_possible_values = v.ensure_bool()?,
269 "hide_short_help" => arg.hide_short_help = v.ensure_bool()?,
270 "hide_long_help" => arg.hide_long_help = v.ensure_bool()?,
271 "conflicts" => arg.conflicts = vec![v.ensure_string()?],
272 "requires" => arg.requires = vec![v.ensure_string()?],
273 "required_if" => arg.required_if = vec![v.ensure_string()?],
274 "required_unless" => arg.required_unless = vec![v.ensure_string()?],
275 "required_unless_all" => arg.required_unless_all = vec![v.ensure_string()?],
276 "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
277 "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
278 "default" => arg.default = vec![v.ensure_string()?],
279 "effect" => {
280 let raw = v.ensure_string()?;
281 match raw.parse() {
282 Ok(effect) => arg.effect = Some(effect),
283 Err(_) => bail_parse!(
284 ctx,
285 v.entry.span(),
286 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
287 ),
288 }
289 }
290 "env" => arg.env = v.ensure_string().map(Some)?,
291 "env_fallback" => arg.env_fallback = vec![v.ensure_string()?],
292 "deprecated_env" => arg.deprecated_env = vec![v.ensure_string()?],
293 "validate" => arg.validate = v.ensure_string().map(Some)?,
294 "validate_error" => arg.validate_error = v.ensure_string().map(Some)?,
295 "help_heading" => arg.help_heading = v.ensure_string().map(Some)?,
296 "surface" => arg.surface = v.ensure_string().map(Some)?,
297 "available_if" => arg.available_if = vec![v.ensure_string()?],
298 "display_order" => arg.display_order = v.ensure_usize().map(Some)?,
299 k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
300 }
301 }
302 if !arg.default.is_empty() {
303 arg.required = false;
304 }
305 for child in node.children() {
306 match child.name() {
307 "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
308 "effect" => {
309 let a = child.arg(0)?;
310 let raw = a.ensure_string()?;
311 match raw.parse() {
312 Ok(effect) => arg.effect = Some(effect),
313 Err(_) => bail_parse!(
314 ctx,
315 a.entry.span(),
316 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
317 ),
318 }
319 }
320 "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
321 "env_fallback" => arg.env_fallback = string_args(&child)?,
322 "deprecated_env" => arg.deprecated_env = string_args(&child)?,
323 "validate" => arg.validate = child.arg(0)?.ensure_string().map(Some)?,
324 "validate_error" => {
325 arg.validate_error = child.arg(0)?.ensure_string().map(Some)?;
326 }
327 "help_heading" => {
328 arg.help_heading = child.arg(0)?.ensure_string().map(Some)?;
329 }
330 "surface" => arg.surface = child.arg(0)?.ensure_string().map(Some)?,
331 "available_if" => arg.available_if = string_args(&child)?,
332 "display_order" => {
333 arg.display_order = child.arg(0)?.ensure_usize().map(Some)?;
334 }
335 "default" => {
336 let children = child.children();
340 if children.is_empty() {
341 arg.default = vec![child.arg(0)?.ensure_string()?];
343 } else {
344 arg.default = children.iter().map(|c| c.name().to_string()).collect();
347 }
348 }
349 "help" => arg.help = Some(child.arg(0)?.ensure_string()?),
350 "sigil" => arg.sigil = child.arg(0)?.ensure_string().map(Some)?,
351 "long_help" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
352 "help_long" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
353 "help_md" => arg.help_md = Some(child.arg(0)?.ensure_string()?),
354 "note" => arg
355 .admonitions
356 .push(SpecAdmonition::note(child.arg(0)?.ensure_string()?)),
357 "warning" => arg
358 .admonitions
359 .push(SpecAdmonition::warning(child.arg(0)?.ensure_string()?)),
360 "required" => arg.required = child.arg(0)?.ensure_bool()?,
361 "var" => arg.var = child.arg(0)?.ensure_bool()?,
362 "var_min" => arg.var_min = child.arg(0)?.ensure_usize().map(Some)?,
363 "var_max" => arg.var_max = child.arg(0)?.ensure_usize().map(Some)?,
364 "value_names" => {
365 arg.value_names = child
366 .ensure_arg_len(1..)?
367 .args()
368 .map(|entry| entry.ensure_string())
369 .collect::<Result<Vec<_>, _>>()?;
370 }
371 "allow_negative_numbers" => {
372 arg.allow_negative_numbers = child.arg(0)?.ensure_bool()?;
373 }
374 "value_terminator" => {
375 arg.value_terminator = child.arg(0)?.ensure_string().map(Some)?;
376 }
377 "hide" => arg.hide = child.arg(0)?.ensure_bool()?,
378 "hide_default_value" => arg.hide_default_value = child.arg(0)?.ensure_bool()?,
379 "hide_env" => arg.hide_env = child.arg(0)?.ensure_bool()?,
380 "hide_env_values" => arg.hide_env_values = child.arg(0)?.ensure_bool()?,
381 "hide_possible_values" => arg.hide_possible_values = child.arg(0)?.ensure_bool()?,
382 "hide_short_help" => arg.hide_short_help = child.arg(0)?.ensure_bool()?,
383 "hide_long_help" => arg.hide_long_help = child.arg(0)?.ensure_bool()?,
384 "conflicts" => {
385 arg.conflicts = child
386 .ensure_arg_len(1..)?
387 .args()
388 .map(|entry| entry.ensure_string())
389 .collect::<Result<Vec<_>, _>>()?;
390 }
391 "requires" => arg.requires = string_args(&child)?,
392 "required_if" => arg.required_if = string_args(&child)?,
393 "required_if_eq" => arg.required_if_eq.push(required_if_eq(&child)?),
394 "required_if_eq_all" => {
395 let len = child.args().count();
396 if len < 2 || len % 2 != 0 {
397 bail_parse!(
398 ctx,
399 child.node.name().span(),
400 "required_if_eq_all needs selector/value pairs"
401 );
402 }
403 arg.required_if_eq_all = required_if_eq_pairs(&child)?;
404 }
405 "required_unless" => arg.required_unless = string_args(&child)?,
406 "required_unless_all" => arg.required_unless_all = string_args(&child)?,
407 "double_dash" => arg.double_dash = child.arg(0)?.ensure_string()?.parse()?,
408 "complete" => {
409 if arg.complete.is_some() {
410 bail_parse!(
411 ctx,
412 child.node.name().span(),
413 "an arg may have only one complete"
414 );
415 }
416 arg.complete = Some(SpecComplete::parse_inline(ctx, &child)?);
417 }
418 k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
419 }
420 }
421 if let Some(sigil) = &arg.sigil
422 && let Some(name) = arg.name.strip_prefix(sigil)
423 {
424 arg.name = name.to_string();
425 }
426 if let Some(first) = arg.value_names.first() {
427 arg.name.clone_from(first);
428 }
429 if let Some(complete) = &mut arg.complete {
431 complete.name = arg.name.to_lowercase();
432 }
433 if arg.value_names.len() > 1 {
434 let arity = arg.value_names.len();
435 match (arg.var_min, arg.var_max) {
436 (None, None) => {
437 arg.var_min = Some(arity);
438 arg.var_max = Some(arity);
439 }
440 (Some(min), Some(max)) if min == arity && max == arity => {}
441 _ => bail_parse!(
442 ctx,
443 node.node.name().span(),
444 "{arity} value names require var_min={arity} and var_max={arity}"
445 ),
446 }
447 arg.var = true;
448 }
449 if arg.validate_error.is_some() && arg.validate.is_none() {
450 bail_parse!(
451 ctx,
452 node.node.name().span(),
453 "validate_error requires a validate expression"
454 );
455 }
456 if arg.value_terminator.as_deref() == Some("") {
457 bail_parse!(
458 ctx,
459 node.node.name().span(),
460 "value_terminator cannot be empty"
461 );
462 }
463 if arg.value_terminator.is_some() && !arg.var {
464 bail_parse!(
465 ctx,
466 node.node.name().span(),
467 "value_terminator requires a variadic argument"
468 );
469 }
470 if let Some(sigil) = &arg.sigil {
471 if sigil.is_empty() {
472 bail_parse!(ctx, node.node.name().span(), "sigil cannot be empty");
473 }
474 if sigil.starts_with('-') {
475 bail_parse!(
476 ctx,
477 node.node.name().span(),
478 "sigil cannot start with `-`; that namespace belongs to flags"
479 );
480 }
481 if sigil.chars().any(char::is_whitespace) {
482 bail_parse!(
483 ctx,
484 node.node.name().span(),
485 "sigil cannot contain whitespace"
486 );
487 }
488 if arg.double_dash != SpecDoubleDashChoices::Optional {
489 bail_parse!(
490 ctx,
491 node.node.name().span(),
492 "sigil arguments cannot declare non-optional double_dash behavior"
493 );
494 }
495 if arg.value_terminator.is_some() {
496 bail_parse!(
497 ctx,
498 node.node.name().span(),
499 "sigil arguments cannot declare value_terminator"
500 );
501 }
502 if arg.var_min.is_some() || arg.var_max.is_some() {
503 bail_parse!(
504 ctx,
505 node.node.name().span(),
506 "sigil arguments cannot declare var_min or var_max"
507 );
508 }
509 }
510 #[cfg(feature = "validation")]
511 if let Some(expression) = &arg.validate
512 && let Err(error) = usage_validation::check(expression)
513 {
514 bail_parse!(
515 ctx,
516 node.node.name().span(),
517 "invalid validation expression: {error}"
518 );
519 }
520 arg.usage = arg.usage();
521 if let Some(help) = &arg.help {
522 arg.help_first_line = Some(string::first_line(help));
523 }
524 Ok(arg)
525 }
526}
527
528impl SpecArg {
529 pub fn usage(&self) -> String {
530 let exact_arity = self.var.then_some(()).and_then(|()| {
531 self.var_min
532 .zip(self.var_max)
533 .filter(|(min, max)| min == max && *min > 1)
534 .map(|(arity, _)| arity)
535 });
536 if self.value_names.len() > 1 || exact_arity.is_some() {
537 let labels = if self.value_names.len() > 1 {
538 self.value_names.clone()
539 } else {
540 vec![
541 self.value_names
542 .first()
543 .cloned()
544 .unwrap_or_else(|| self.name.clone());
545 exact_arity.expect("branch checked")
546 ]
547 };
548 let placeholders = labels
549 .iter()
550 .map(|name| {
551 if self.required {
552 format!("<{name}>")
553 } else {
554 format!("[{name}]")
555 }
556 })
557 .collect::<Vec<_>>()
558 .join(" ");
559 return if self.double_dash == SpecDoubleDashChoices::Required {
560 format!("-- {placeholders}")
561 } else {
562 placeholders
563 };
564 }
565 let name = if let Some(sigil) = &self.sigil {
566 format!("{sigil}{}", self.name)
567 } else if self.double_dash == SpecDoubleDashChoices::Required {
568 format!("-- {}", self.name)
569 } else {
570 self.name.clone()
571 };
572 let mut name = if self.required {
573 format!("<{name}>")
574 } else {
575 format!("[{name}]")
576 };
577 if self.var {
578 name = format!("{name}…");
579 }
580 name
581 }
582}
583
584impl From<&SpecArg> for KdlNode {
585 fn from(arg: &SpecArg) -> Self {
586 let mut node = KdlNode::new("arg");
587 node.push(KdlEntry::new(arg.usage()));
588 if let Some(sigil) = &arg.sigil {
589 node.push(string_entry(Some("sigil"), sigil));
590 }
591 if let Some(desc) = &arg.help {
592 node.push(string_entry(Some("help"), desc));
593 }
594 if let Some(desc) = &arg.help_long {
595 node.push(string_entry(Some("help_long"), desc));
596 }
597 if let Some(desc) = &arg.help_md {
598 node.push(string_entry(Some("help_md"), desc));
599 }
600 for admonition in &arg.admonitions {
601 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
602 let name = match admonition.kind {
603 SpecAdmonitionKind::Note => "note",
604 SpecAdmonitionKind::Warning => "warning",
605 };
606 let mut block = KdlNode::new(name);
607 block.push(string_entry(None, &admonition.text));
608 children.nodes_mut().push(block);
609 }
610 if !arg.required {
611 node.push(KdlEntry::new_prop("required", false));
612 }
613 if arg.double_dash == SpecDoubleDashChoices::Automatic
614 || arg.double_dash == SpecDoubleDashChoices::Preserve
615 {
616 node.push(KdlEntry::new_prop(
617 "double_dash",
618 arg.double_dash.to_string(),
619 ));
620 }
621 if arg.var {
622 node.push(KdlEntry::new_prop("var", true));
623 }
624 if let Some(min) = arg.var_min {
625 node.push(KdlEntry::new_prop("var_min", min as i128));
626 }
627 if let Some(max) = arg.var_max {
628 node.push(KdlEntry::new_prop("var_max", max as i128));
629 }
630 if let Some(delimiter) = arg.delimiter {
631 node.push(string_entry(Some("delimiter"), &delimiter.to_string()));
632 }
633 if arg.allow_negative_numbers {
634 node.push(KdlEntry::new_prop("allow_negative_numbers", true));
635 }
636 if let Some(terminator) = &arg.value_terminator {
637 node.push(string_entry(Some("value_terminator"), terminator));
638 }
639 if arg.hide {
640 node.push(KdlEntry::new_prop("hide", true));
641 }
642 for (name, hidden) in [
643 ("hide_default_value", arg.hide_default_value),
644 ("hide_env", arg.hide_env),
645 ("hide_env_values", arg.hide_env_values),
646 ("hide_possible_values", arg.hide_possible_values),
647 ("hide_short_help", arg.hide_short_help),
648 ("hide_long_help", arg.hide_long_help),
649 ] {
650 if hidden {
651 node.push(KdlEntry::new_prop(name, true));
652 }
653 }
654 if arg.conflicts.len() == 1 {
655 node.push(string_entry(Some("conflicts"), &arg.conflicts[0]));
656 } else if !arg.conflicts.is_empty() {
657 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
658 let mut conflicts = KdlNode::new("conflicts");
659 for target in &arg.conflicts {
660 conflicts.push(string_entry(None, target));
661 }
662 children.nodes_mut().push(conflicts);
663 }
664 serialize_selector_list(&mut node, "requires", &arg.requires);
665 serialize_selector_list(&mut node, "required_if", &arg.required_if);
666 serialize_required_if_eq(&mut node, "required_if_eq", &arg.required_if_eq);
667 if !arg.required_if_eq_all.is_empty() {
668 serialize_required_if_eq(&mut node, "required_if_eq_all", &arg.required_if_eq_all);
669 }
670 serialize_selector_list(&mut node, "required_unless", &arg.required_unless);
671 serialize_selector_list(&mut node, "required_unless_all", &arg.required_unless_all);
672 if !arg.default.is_empty() {
674 if arg.default.len() == 1 {
675 node.push(string_entry(Some("default"), &arg.default[0]));
677 } else {
678 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
680 let mut default_node = KdlNode::new("default");
681 let default_children = default_node
682 .children_mut()
683 .get_or_insert_with(KdlDocument::new);
684 for val in &arg.default {
685 default_children
686 .nodes_mut()
687 .push(KdlNode::new(val.as_str()));
688 }
689 children.nodes_mut().push(default_node);
690 }
691 }
692 if let Some(env) = &arg.env {
693 node.push(string_entry(Some("env"), env));
694 }
695 serialize_selector_list(&mut node, "env_fallback", &arg.env_fallback);
696 serialize_selector_list(&mut node, "deprecated_env", &arg.deprecated_env);
697 if let Some(validate) = &arg.validate {
698 node.push(string_entry(Some("validate"), validate));
699 }
700 if arg.validate.is_some()
701 && let Some(error) = &arg.validate_error
702 {
703 node.push(string_entry(Some("validate_error"), error));
704 }
705 if let Some(help_heading) = &arg.help_heading {
706 node.push(string_entry(Some("help_heading"), help_heading));
707 }
708 if let Some(surface) = &arg.surface {
709 node.push(string_entry(Some("surface"), surface));
710 }
711 serialize_selector_list(&mut node, "available_if", &arg.available_if);
712 if let Some(order) = arg.display_order {
713 node.push(KdlEntry::new_prop("display_order", order as i128));
714 }
715 if let Some(effect) = &arg.effect {
716 node.push(string_entry(Some("effect"), effect.as_str()));
717 }
718 if let Some(choices) = &arg.choices {
719 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
720 children.nodes_mut().push(choices.into());
721 }
722 if let Some(complete) = &arg.complete {
723 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
724 children.nodes_mut().push(complete.to_inline_node());
725 }
726 node
727 }
728}
729
730fn string_args(node: &NodeHelper<'_>) -> Result<Vec<String>, UsageErr> {
731 node.ensure_arg_len(1..)?
732 .args()
733 .map(|entry| entry.ensure_string())
734 .collect()
735}
736
737fn required_if_eq(node: &NodeHelper<'_>) -> Result<SpecRequiredIfEq, UsageErr> {
738 node.ensure_arg_len(2..=2)?;
739 Ok(SpecRequiredIfEq {
740 selector: node.arg(0)?.ensure_string()?,
741 value: node.arg(1)?.ensure_string()?,
742 })
743}
744
745fn required_if_eq_pairs(node: &NodeHelper<'_>) -> Result<Vec<SpecRequiredIfEq>, UsageErr> {
746 let entries = node.args().collect::<Vec<_>>();
747 entries
748 .as_chunks::<2>()
749 .0
750 .iter()
751 .map(|pair| {
752 Ok(SpecRequiredIfEq {
753 selector: pair[0].ensure_string()?,
754 value: pair[1].ensure_string()?,
755 })
756 })
757 .collect()
758}
759
760fn serialize_selector_list(node: &mut KdlNode, name: &str, selectors: &[String]) {
761 if selectors.len() == 1 {
762 node.push(string_entry(Some(name), &selectors[0]));
763 } else if !selectors.is_empty() {
764 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
765 let mut relation = KdlNode::new(name);
766 for selector in selectors {
767 relation.push(string_entry(None, selector));
768 }
769 children.nodes_mut().push(relation);
770 }
771}
772
773fn serialize_required_if_eq(node: &mut KdlNode, name: &str, conditions: &[SpecRequiredIfEq]) {
774 if conditions.is_empty() {
775 return;
776 }
777 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
778 if name == "required_if_eq_all" {
779 let mut relation = KdlNode::new(name);
780 for condition in conditions {
781 relation.push(string_entry(None, &condition.selector));
782 relation.push(string_entry(None, &condition.value));
783 }
784 children.nodes_mut().push(relation);
785 } else {
786 for condition in conditions {
787 let mut relation = KdlNode::new(name);
788 relation.push(string_entry(None, &condition.selector));
789 relation.push(string_entry(None, &condition.value));
790 children.nodes_mut().push(relation);
791 }
792 }
793}
794
795impl From<&str> for SpecArg {
796 fn from(input: &str) -> Self {
797 let (input, after_double_dash) = input
798 .strip_prefix("-- ")
799 .map_or((input, false), |rest| (rest, true));
800 if let Some(placeholders) = fixed_placeholders(input) {
801 let required = placeholders
802 .iter()
803 .all(|placeholder| placeholder.starts_with('<'));
804 let value_names = placeholders
805 .iter()
806 .map(|placeholder| placeholder[1..placeholder.len() - 1].to_string())
807 .collect::<Vec<_>>();
808 let mut arg = SpecArg {
809 name: value_names[0].clone(),
810 value_names,
811 required,
812 var: true,
813 var_min: Some(placeholders.len()),
814 var_max: Some(placeholders.len()),
815 double_dash: if after_double_dash {
816 SpecDoubleDashChoices::Required
817 } else {
818 SpecDoubleDashChoices::Optional
819 },
820 ..Default::default()
821 };
822 arg.usage = arg.usage();
823 return arg;
824 }
825 let mut arg = SpecArg {
826 name: input.to_string(),
827 required: true,
828 double_dash: if after_double_dash {
829 SpecDoubleDashChoices::Required
830 } else {
831 SpecDoubleDashChoices::Optional
832 },
833 ..Default::default()
834 };
835 if let Some(name) = arg
837 .name
838 .strip_suffix("...")
839 .or_else(|| arg.name.strip_suffix("…"))
840 {
841 arg.var = true;
842 arg.name = name.to_string();
843 }
844 let first = arg.name.chars().next().unwrap_or_default();
845 let last = arg.name.chars().last().unwrap_or_default();
846 match (first, last) {
847 ('[', ']') => {
848 arg.name = arg.name[1..arg.name.len() - 1].to_string();
849 arg.required = false;
850 }
851 ('<', '>') => {
852 arg.name = arg.name[1..arg.name.len() - 1].to_string();
853 }
854 _ => {}
855 }
856 if let Some(name) = arg.name.strip_prefix("-- ") {
860 arg.double_dash = SpecDoubleDashChoices::Required;
861 arg.name = name.to_string();
862 }
863 if !arg.var
865 && let Some(name) = arg
866 .name
867 .strip_suffix("...")
868 .or_else(|| arg.name.strip_suffix("…"))
869 {
870 arg.var = true;
871 arg.name = name.to_string();
872 }
873 arg.usage = arg.usage();
878 arg
879 }
880}
881impl FromStr for SpecArg {
882 type Err = UsageErr;
883 fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
884 if fixed_placeholders(input.strip_prefix("-- ").unwrap_or(input)).is_some_and(
885 |placeholders| {
886 placeholders
887 .windows(2)
888 .any(|pair| pair[0].starts_with('<') != pair[1].starts_with('<'))
889 },
890 ) {
891 let message =
892 "fixed-arity placeholders must be either all required or all optional".to_string();
893 return Err(UsageErr::InvalidInput(
894 message,
895 (0, input.len()).into(),
896 miette::NamedSource::new("argument", input.to_string()),
897 ));
898 }
899 Ok(input.into())
900 }
901}
902
903fn fixed_placeholders(input: &str) -> Option<Vec<&str>> {
906 if !input.bytes().any(|byte| byte.is_ascii_whitespace()) {
907 return None;
908 }
909 let placeholders: Vec<_> = input.split_whitespace().collect();
910 (placeholders.len() > 1
911 && placeholders.iter().all(|placeholder| {
912 matches!(
913 (placeholder.chars().next(), placeholder.chars().last()),
914 (Some('<'), Some('>')) | (Some('['), Some(']'))
915 )
916 }))
917 .then_some(placeholders)
918}
919
920#[cfg(feature = "clap")]
928pub(crate) fn default_values(arg: &clap::Arg) -> Vec<String> {
929 let raw = arg
930 .get_default_values()
931 .iter()
932 .map(|v| v.to_string_lossy().to_string());
933 match arg.get_value_delimiter() {
934 Some(delimiter) => raw
935 .flat_map(|v| {
936 v.split(delimiter)
937 .map(|part| part.to_string())
938 .collect::<Vec<_>>()
939 })
940 .collect(),
941 None => raw.collect(),
942 }
943}
944
945#[cfg(feature = "clap")]
952pub(crate) fn value_bounds(source: &clap::Arg, target: &mut SpecArg, zero_values_supported: bool) {
953 if source.get_value_delimiter().is_some() {
958 return;
959 }
960
961 let Some(range) = source.get_num_args() else {
962 if target.value_names.len() > 1 {
963 let arity = target.value_names.len();
964 target.var = true;
965 target.var_min = Some(arity);
966 target.var_max = Some(arity);
967 }
968 return;
969 };
970 let min = range.min_values();
971 let max = range.max_values();
972 if max <= 1 || min == 0 && !zero_values_supported {
973 return;
974 }
975
976 target.var = true;
977 target.var_min = Some(min);
978 target.var_max = (max != usize::MAX).then_some(max);
979}
980
981#[cfg(feature = "clap")]
987pub(crate) fn value_names_from_clap(source: &clap::Arg) -> Vec<String> {
988 let names: Vec<String> = source
989 .get_value_names()
990 .unwrap_or_default()
991 .iter()
992 .map(ToString::to_string)
993 .collect();
994 if names.len() <= 1 {
995 return names;
996 }
997 let mismatched_range = source.get_num_args().is_some_and(|range| {
998 range.min_values() != names.len() || range.max_values() != names.len()
999 });
1000 if source.get_value_delimiter().is_some() || mismatched_range {
1001 names.into_iter().take(1).collect()
1002 } else {
1003 names
1004 }
1005}
1006
1007#[cfg(feature = "clap")]
1009pub(crate) fn value_hint_type(hint: clap::ValueHint) -> Option<&'static str> {
1010 use clap::ValueHint;
1011
1012 match hint {
1013 ValueHint::Unknown => None,
1014 ValueHint::Other => Some("none"),
1015 ValueHint::AnyPath | ValueHint::FilePath => Some("path"),
1016 ValueHint::DirPath => Some("dir"),
1017 ValueHint::ExecutablePath => Some("executable"),
1018 ValueHint::CommandName | ValueHint::CommandString => Some("command"),
1019 ValueHint::CommandWithArguments => Some("command_args"),
1020 ValueHint::Username => Some("username"),
1021 ValueHint::Hostname => Some("hostname"),
1022 ValueHint::Url => Some("url"),
1023 ValueHint::EmailAddress => Some("email"),
1024 _ => None,
1025 }
1026}
1027
1028#[cfg(feature = "clap")]
1029pub(crate) fn choices_from_clap(arg: &clap::Arg) -> Option<SpecChoices> {
1030 let possible = arg.get_possible_values();
1031 if possible.is_empty() {
1032 return None;
1033 }
1034 let choices = possible
1035 .iter()
1036 .map(|value| value.get_name().to_string())
1037 .collect();
1038 let details = possible
1039 .iter()
1040 .filter_map(|value| {
1041 let aliases: Vec<_> = value
1042 .get_name_and_aliases()
1043 .skip(1)
1044 .map(|alias| SpecChoiceAlias {
1045 value: alias.to_string(),
1046 hide: true,
1048 })
1049 .collect();
1050 let detail = SpecChoice {
1051 value: value.get_name().to_string(),
1052 help: value.get_help().map(ToString::to_string),
1053 hide: value.is_hide_set(),
1054 aliases,
1055 };
1056 (detail.help.is_some() || detail.hide || !detail.aliases.is_empty()).then_some(detail)
1057 })
1058 .collect();
1059 Some(SpecChoices {
1060 choices,
1061 details,
1062 ignore_case: arg.is_ignore_case_set(),
1063 ..Default::default()
1064 })
1065}
1066
1067#[cfg(feature = "clap")]
1068impl From<&clap::Arg> for SpecArg {
1069 fn from(arg: &clap::Arg) -> Self {
1070 let source = arg;
1071 let required = arg.is_required_set();
1072 let help = arg.get_help().map(|s| s.to_string());
1073 let help_long = arg.get_long_help().map(|s| s.to_string());
1074 let help_first_line = help.as_ref().map(|s| string::first_line(s));
1075 let hide = arg.is_hide_set();
1076 let delimiter = arg.get_value_delimiter();
1080 let recorded_delimiter = delimiter.filter(char::is_ascii);
1081 let value_terminator = arg.get_value_terminator().map(ToString::to_string);
1082 let var = matches!(
1083 arg.get_action(),
1084 clap::ArgAction::Count | clap::ArgAction::Append
1085 ) || delimiter.is_some();
1086 let choices = choices_from_clap(arg);
1087 let value_names = value_names_from_clap(arg);
1088 let mut arg = Self {
1089 name: value_names
1090 .first()
1091 .cloned()
1092 .unwrap_or_else(|| source.get_id().to_string()),
1093 sigil: None,
1094 value_names,
1095 usage: "".into(),
1096 required,
1097 double_dash: if arg.is_last_set() {
1098 SpecDoubleDashChoices::Required
1099 } else if arg.is_trailing_var_arg_set() {
1100 SpecDoubleDashChoices::Automatic
1101 } else {
1102 SpecDoubleDashChoices::Optional
1103 },
1104 help,
1105 help_long,
1106 help_md: None,
1107 admonitions: Vec::new(),
1108 help_first_line,
1109 var,
1110 var_max: None,
1111 var_min: None,
1112 delimiter: recorded_delimiter,
1115 allow_negative_numbers: arg.is_allow_negative_numbers_set(),
1116 value_terminator: None,
1117 hide,
1118 hide_default_value: arg.is_hide_default_value_set(),
1119 hide_env: arg.is_hide_env_set(),
1120 hide_env_values: arg.is_hide_env_values_set(),
1121 hide_possible_values: arg.is_hide_possible_values_set(),
1122 hide_short_help: arg.is_hide_short_help_set(),
1123 hide_long_help: arg.is_hide_long_help_set(),
1124 conflicts: Vec::new(),
1125 requires: Vec::new(),
1126 required_if: Vec::new(),
1127 required_if_eq: Vec::new(),
1128 required_if_eq_all: Vec::new(),
1129 required_unless: Vec::new(),
1130 required_unless_all: Vec::new(),
1131 default: default_values(arg),
1132 choices: None,
1133 validate: None,
1134 validate_error: None,
1135 effect: None,
1136 env: None,
1137 env_fallback: Vec::new(),
1138 deprecated_env: Vec::new(),
1139 help_heading: arg.get_help_heading().map(|s| s.to_string()),
1140 surface: None,
1141 available_if: Vec::new(),
1142 display_order: Some(arg.get_display_order()),
1143 complete: None,
1146 };
1147 arg.choices = choices;
1148
1149 value_bounds(source, &mut arg, true);
1150 if arg.var {
1151 arg.value_terminator = value_terminator;
1152 }
1153
1154 arg
1155 }
1156}
1157
1158impl Display for SpecArg {
1159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1160 write!(f, "{}", self.usage())
1161 }
1162}
1163impl PartialEq for SpecArg {
1164 fn eq(&self, other: &Self) -> bool {
1165 self.name == other.name
1166 }
1167}
1168impl Eq for SpecArg {}
1169impl Hash for SpecArg {
1170 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1171 self.name.hash(state);
1172 }
1173}
1174
1175#[cfg(all(test, feature = "validation"))]
1176mod validation_tests {
1177 use std::collections::HashMap;
1178
1179 use crate::{Spec, parse, parse::Parser};
1180
1181 fn spec() -> Spec {
1182 r#"
1183name "ex"
1184bin "ex"
1185arg "<port>" validate="int(value) >= 1 && int(value) <= 65535" validate_error="must be a valid port"
1186 "#
1187 .parse()
1188 .unwrap()
1189 }
1190
1191 #[test]
1192 fn validation_round_trips_through_kdl() {
1193 let spec = spec();
1194 let kdl = spec.to_string();
1195 let reparsed: Spec = kdl.parse().unwrap();
1196 let arg = &reparsed.cmd.args[0];
1197 assert_eq!(
1198 arg.validate.as_deref(),
1199 Some("int(value) >= 1 && int(value) <= 65535")
1200 );
1201 assert_eq!(arg.validate_error.as_deref(), Some("must be a valid port"));
1202 }
1203
1204 #[test]
1205 fn invalid_validation_declarations_are_rejected_with_the_spec() {
1206 let missing_expression = r#"name "demo"
1207bin "demo"
1208arg "<port>" validate_error="must be a port"
1209"#;
1210 assert!(missing_expression.parse::<Spec>().is_err());
1211
1212 let invalid_expression = r#"name "demo"
1213bin "demo"
1214arg "<port>" validate="int(value) >"
1215"#;
1216 assert!(invalid_expression.parse::<Spec>().is_err());
1217 }
1218
1219 #[test]
1220 fn reference_parser_validates_each_raw_value() {
1221 parse(&spec(), &["ex".to_string(), "9229".to_string()]).unwrap();
1222
1223 let error = parse(&spec(), &["ex".to_string(), "0".to_string()]).unwrap_err();
1224 assert!(
1225 error.to_string().contains("must be a valid port"),
1226 "{error:?}"
1227 );
1228
1229 let variadic: Spec = r#"
1230name "ex"
1231bin "ex"
1232arg "<port>" var=#true validate="int(value) > 0" validate_error="port must be positive"
1233 "#
1234 .parse()
1235 .unwrap();
1236 let error = parse(
1237 &variadic,
1238 &["ex".to_string(), "0".to_string(), "-1".to_string()],
1239 )
1240 .unwrap_err()
1241 .to_string();
1242 assert_eq!(error.matches("port must be positive").count(), 1, "{error}");
1243 }
1244
1245 #[test]
1246 fn reference_parser_validates_environment_and_default_fallbacks() {
1247 let spec: Spec = r#"
1248name "ex"
1249bin "ex"
1250arg "[port]" env="PORT" validate="int(value) > 0" validate_error="port must be positive"
1251flag "--mode" default="bad" {
1252 arg "<mode>" validate="value == 'good'" validate_error="mode must be good"
1253}
1254arg "[ports]..." env="PORTS" var=#true var_max=1 delimiter="," validate="int(value) > 0" validate_error="all ports must be positive"
1255flag "--levels" env="LEVELS" {
1256 arg "<level>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all levels must be good"
1257}
1258flag "--modes" default="good,bad" {
1259 arg "<mode>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all modes must be good"
1260}
1261flag "--conditional" {
1262 default_if "--trigger" "good,bad"
1263 arg "<conditional>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all conditional values must be good"
1264}
1265flag "--repeats <repeat>" env="REPEATS" var=#true var_max=1 delimiter=","
1266flag "--trigger"
1267 "#
1268 .parse()
1269 .unwrap();
1270 let env = HashMap::from([
1271 ("PORT".to_string(), "0".to_string()),
1272 ("PORTS".to_string(), "1,0".to_string()),
1273 ("LEVELS".to_string(), "good,bad".to_string()),
1274 ("REPEATS".to_string(), "one,two".to_string()),
1275 ]);
1276 let error = Parser::new(&spec)
1277 .with_env(env)
1278 .parse(&["ex".to_string(), "--trigger".to_string()])
1279 .unwrap_err();
1280 let error = error.to_string();
1281 assert!(error.contains("port must be positive"), "{error}");
1282 assert!(error.contains("mode must be good"), "{error}");
1283 assert!(error.contains("all ports must be positive"), "{error}");
1284 assert!(error.contains("all levels must be good"), "{error}");
1285 assert!(error.contains("all modes must be good"), "{error}");
1286 assert!(
1287 error.contains("all conditional values must be good"),
1288 "{error}"
1289 );
1290 assert!(
1291 error.contains("Variadic argument <ports> accepts at most 1 value(s), got 2"),
1292 "{error}"
1293 );
1294 for flag in ["levels", "modes", "conditional", "repeats"] {
1295 assert!(
1296 error.contains(&format!(
1297 "Variadic flag --{flag} accepts at most 1 value(s), got 2"
1298 )),
1299 "{error}"
1300 );
1301 }
1302 }
1303}
1304
1305#[cfg(test)]
1306mod sigil_tests {
1307 use crate::Spec;
1308
1309 #[test]
1310 fn sigil_arguments_reject_value_bounds() {
1311 for bound in ["var_min=1", "var_max=2"] {
1312 let spec = format!("arg \"[tools]...\" sigil=\"+\" {bound}\n");
1313 let error = spec.parse::<Spec>().unwrap_err();
1314 assert!(
1315 format!("{error:?}").contains("cannot declare var_min or var_max"),
1316 "{error:?}"
1317 );
1318 }
1319 }
1320}
1321
1322#[cfg(test)]
1323mod delimiter_tests {
1324 use crate::Spec;
1325
1326 #[test]
1327 fn a_delimiter_has_to_be_one_byte() {
1328 for spec in [
1333 "flag \"--tags <tag>\" var=#true delimiter=\"§\"\n",
1334 "arg \"[tags]...\" var=#true delimiter=\"、\"\n",
1335 ] {
1336 let err = spec.parse::<Spec>().unwrap_err();
1337 assert!(format!("{err:?}").contains("one byte"), "{err:?}");
1338 }
1339
1340 let cmd = clap::Command::new("ex").arg(
1344 clap::Arg::new("tags")
1345 .long("tags")
1346 .value_delimiter('、')
1347 .action(clap::ArgAction::Set),
1348 );
1349 let spec = Spec::from(&cmd);
1350 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1351 assert_eq!(
1352 arg.delimiter, None,
1353 "a separator it cannot write is not recorded"
1354 );
1355 assert!(arg.var, "clap still splits, so the values still arrive");
1356 spec.to_string()
1357 .parse::<Spec>()
1358 .expect("what the bridge produces has to parse back");
1359 }
1360
1361 #[test]
1362 fn a_delimiter_round_trips_and_comes_across_from_clap() {
1363 let spec: Spec = "flag \"--tags <tag>\" var=#true delimiter=\",\"\n"
1364 .parse()
1365 .unwrap();
1366 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1367 assert_eq!(arg.delimiter, Some(','));
1368
1369 let reparsed: Spec = spec.to_string().parse().unwrap();
1370 let arg = reparsed.cmd.flags[0].arg.as_ref().unwrap();
1371 assert_eq!(arg.delimiter, Some(','), "{spec}");
1372
1373 let cmd = clap::Command::new("ex").arg(
1376 clap::Arg::new("tags")
1377 .long("tags")
1378 .value_delimiter(',')
1379 .num_args(1..)
1380 .default_value("a,b"),
1381 );
1382 let spec = Spec::from(&cmd);
1383 let flag = &spec.cmd.flags[0];
1384 assert_eq!(flag.arg.as_ref().unwrap().delimiter, Some(','));
1385 assert_eq!(flag.default, vec!["a", "b"]);
1388 }
1389
1390 #[test]
1391 fn a_single_valued_clap_arg_keeps_its_delimiter() {
1392 let cmd = clap::Command::new("ex").arg(
1397 clap::Arg::new("tags")
1398 .long("tags")
1399 .action(clap::ArgAction::Set)
1400 .value_delimiter(','),
1401 );
1402 let spec = Spec::from(&cmd);
1403 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1404 assert_eq!(arg.delimiter, Some(','));
1405 assert!(arg.var, "a delimiter brings `var` with it");
1408 let _: Spec = spec.to_string().parse().expect("{spec}");
1409 }
1410
1411 #[test]
1412 fn a_single_valued_clap_positional_splits_into_stored_values() {
1413 let cmd = clap::Command::new("ex").arg(
1417 clap::Arg::new("tags")
1418 .action(clap::ArgAction::Set)
1419 .value_delimiter(',')
1420 .value_parser(["a", "b"]),
1421 );
1422 let spec = Spec::from(&cmd);
1423 let arg = &spec.cmd.args[0];
1424 assert!(arg.var, "a positional delimiter brings `var` with it");
1425 assert_eq!(arg.delimiter, Some(','));
1426
1427 let input = ["ex", "a,b"].map(str::to_string);
1428 let parsed = crate::parse(&spec, &input).expect("both split values are choices");
1429 let value = parsed
1430 .args
1431 .values()
1432 .next()
1433 .expect("the positional was stored");
1434 assert!(matches!(
1435 value,
1436 crate::parse::ParseValue::MultiString(values)
1437 if values == &["a".to_string(), "b".to_string()]
1438 ));
1439 }
1440
1441 #[test]
1442 fn a_delimiter_needs_somewhere_to_put_what_it_splits() {
1443 let err = "flag \"--tags <tag>\" delimiter=\",\"\n"
1445 .parse::<Spec>()
1446 .unwrap_err();
1447 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1448
1449 let err = "arg \"[tags]\" delimiter=\",\"\n"
1450 .parse::<Spec>()
1451 .unwrap_err();
1452 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1453
1454 let err = "flag \"--quiet\" delimiter=\",\"\n"
1456 .parse::<Spec>()
1457 .unwrap_err();
1458 assert!(format!("{err:?}").contains("takes none"), "{err:?}");
1459
1460 let err = "flag \"--tags <tag>\" var=#true delimiter=\"::\"\n"
1462 .parse::<Spec>()
1463 .unwrap_err();
1464 assert!(format!("{err:?}").contains("one character"), "{err:?}");
1465 }
1466}
1467
1468#[cfg(test)]
1469mod possible_value_tests {
1470 use clap::builder::PossibleValue;
1471
1472 #[test]
1473 fn clap_possible_value_metadata_survives_the_bridge() {
1474 let command = clap::Command::new("ex").arg(
1475 clap::Arg::new("color").ignore_case(true).value_parser([
1476 PossibleValue::new("always")
1477 .help("Always use color")
1478 .alias("yes"),
1479 PossibleValue::new("never").hide(true),
1480 ]),
1481 );
1482 let spec = crate::Spec::from(&command);
1483 let choices = spec.cmd.args[0].choices.as_ref().unwrap();
1484 assert_eq!(choices.choices, ["always", "never"]);
1485 assert!(choices.ignore_case);
1486 assert!(choices.matches("YES"));
1487 assert_eq!(choices.values(), ["always"]);
1488 assert_eq!(choices.details[0].help.as_deref(), Some("Always use color"));
1489 assert!(choices.details[0].aliases[0].hide);
1490 assert!(choices.details[1].hide);
1491 }
1492}
1493
1494#[cfg(test)]
1495mod tests {
1496 use crate::{Spec, SpecArg};
1497 use insta::assert_snapshot;
1498
1499 #[test]
1500 fn test_arg_with_env() {
1501 let spec = Spec::parse(
1502 &Default::default(),
1503 r#"
1504arg "<input>" env="MY_INPUT" help="Input file"
1505arg "<output>" env="MY_OUTPUT"
1506 "#,
1507 )
1508 .unwrap();
1509
1510 assert_snapshot!(spec, @r#"
1511 arg <input> help="Input file" env=MY_INPUT
1512 arg <output> env=MY_OUTPUT
1513 "#);
1514
1515 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1516 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1517
1518 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1519 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1520 }
1521
1522 #[test]
1523 fn test_arg_with_env_child_node() {
1524 let spec = Spec::parse(
1525 &Default::default(),
1526 r#"
1527arg "<input>" help="Input file" {
1528 env "MY_INPUT"
1529}
1530arg "<output>" {
1531 env "MY_OUTPUT"
1532}
1533 "#,
1534 )
1535 .unwrap();
1536
1537 assert_snapshot!(spec, @r#"
1538 arg <input> help="Input file" env=MY_INPUT
1539 arg <output> env=MY_OUTPUT
1540 "#);
1541
1542 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1543 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1544
1545 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1546 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1547 }
1548
1549 #[test]
1550 fn test_arg_variadic_syntax() {
1551 use crate::SpecArg;
1552
1553 let arg: SpecArg = "<files>...".into();
1555 assert_eq!(arg.name, "files");
1556 assert!(arg.var);
1557 assert!(arg.required);
1558
1559 let arg: SpecArg = "[files]...".into();
1561 assert_eq!(arg.name, "files");
1562 assert!(arg.var);
1563 assert!(!arg.required);
1564
1565 let arg: SpecArg = "<files>…".into();
1567 assert_eq!(arg.name, "files");
1568 assert!(arg.var);
1569
1570 let arg: SpecArg = "[files]…".into();
1571 assert_eq!(arg.name, "files");
1572 assert!(arg.var);
1573 assert!(!arg.required);
1574
1575 let arg: SpecArg = "[args...]".into();
1577 assert_eq!(arg.name, "args");
1578 assert!(arg.var);
1579 assert!(!arg.required);
1580
1581 let arg: SpecArg = "<args...>".into();
1582 assert_eq!(arg.name, "args");
1583 assert!(arg.var);
1584 assert!(arg.required);
1585
1586 let arg: SpecArg = "[args…]".into();
1588 assert_eq!(arg.name, "args");
1589 assert!(arg.var);
1590 assert!(!arg.required);
1591 }
1592
1593 #[test]
1594 fn fixed_arity_placeholders_round_trip() {
1595 let spec: Spec = "arg \"<START> <END>\"\n".parse().unwrap();
1596 let arg = &spec.cmd.args[0];
1597 assert_eq!(arg.value_names, ["START", "END"]);
1598 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1599 assert_eq!(arg.usage, "<START> <END>");
1600
1601 let reparsed: Spec = spec.to_string().parse().unwrap();
1602 assert_eq!(reparsed.cmd.args[0].value_names, ["START", "END"]);
1603 }
1604
1605 #[test]
1606 fn fixed_arity_placeholders_reject_mismatched_bounds() {
1607 let error = "arg \"<START> <END>\" var_min=1 var_max=2\n"
1608 .parse::<Spec>()
1609 .unwrap_err();
1610 assert!(
1611 format!("{error:?}").contains("require var_min=2 and var_max=2"),
1612 "{error:?}"
1613 );
1614 }
1615
1616 #[test]
1617 fn a_single_value_name_replaces_the_display_name() {
1618 let spec: Spec = "arg \"<input>\" { value_names \"INPUT\" }\n"
1619 .parse()
1620 .unwrap();
1621 let arg = &spec.cmd.args[0];
1622 assert_eq!(arg.name, "INPUT");
1623 assert_eq!(arg.usage, "<INPUT>");
1624
1625 let built = SpecArg::builder()
1626 .name("input")
1627 .required(true)
1628 .value_names(["INPUT"])
1629 .build();
1630 assert_eq!(built.name, "INPUT");
1631 assert_eq!(built.usage, "<INPUT>");
1632 }
1633
1634 #[test]
1635 fn builder_fixed_arity_survives_later_bound_setters() {
1636 let after = SpecArg::builder()
1637 .value_names(["START", "END"])
1638 .var(false)
1639 .var_min(1)
1640 .var_max(4)
1641 .build();
1642 let before = SpecArg::builder()
1643 .var(false)
1644 .var_min(1)
1645 .var_max(4)
1646 .value_names(["START", "END"])
1647 .build();
1648 for arg in [after, before] {
1649 assert!(arg.var);
1650 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1651 assert_eq!(arg.usage, "[START] [END]");
1652 }
1653 }
1654
1655 #[test]
1656 fn one_label_with_exact_bounds_renders_each_value_slot() {
1657 let spec: Spec = "arg \"<item>…\" var_min=2 var_max=2 { value_names \"ITEM\" }\n"
1658 .parse()
1659 .unwrap();
1660 assert_eq!(spec.cmd.args[0].usage, "<ITEM> <ITEM>");
1661 let reparsed: Spec = spec.to_string().parse().unwrap();
1662 assert_eq!(reparsed.cmd.args[0].value_names, ["ITEM", "ITEM"]);
1663 assert_eq!(
1664 (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
1665 (Some(2), Some(2))
1666 );
1667
1668 let built = SpecArg::builder()
1669 .value_names(["ITEM"])
1670 .required(true)
1671 .var(true)
1672 .var_min(2)
1673 .var_max(2)
1674 .build();
1675 assert_eq!(built.usage, "<ITEM> <ITEM>");
1676 }
1677
1678 #[test]
1679 fn fixed_arity_placeholders_reject_mixed_requiredness() {
1680 let error = "arg \"<START> [END]\"\n".parse::<Spec>().unwrap_err();
1681 assert!(
1682 format!("{error:?}")
1683 .contains("fixed-arity placeholders must be either all required or all optional"),
1684 "{error:?}"
1685 );
1686 }
1687
1688 #[test]
1689 fn test_arg_child_nodes() {
1690 let spec = Spec::parse(
1691 &Default::default(),
1692 r#"
1693arg "<environment>" {
1694 help "Deployment environment"
1695 choices "dev" "staging" "prod"
1696}
1697arg "[services]" {
1698 help "Services to deploy"
1699 var #true
1700 var_min 0
1701}
1702 "#,
1703 )
1704 .unwrap();
1705
1706 let env_arg = spec
1707 .cmd
1708 .args
1709 .iter()
1710 .find(|a| a.name == "environment")
1711 .unwrap();
1712 assert_eq!(env_arg.help, Some("Deployment environment".to_string()));
1713 assert!(env_arg.choices.is_some());
1714
1715 let svc_arg = spec.cmd.args.iter().find(|a| a.name == "services").unwrap();
1716 assert_eq!(svc_arg.help, Some("Services to deploy".to_string()));
1717 assert!(svc_arg.var);
1718 assert_eq!(svc_arg.var_min, Some(0));
1719 }
1720
1721 #[test]
1722 fn test_arg_long_help_child_node() {
1723 let spec = Spec::parse(
1724 &Default::default(),
1725 r#"
1726arg "<input>" {
1727 help "Input file"
1728 long_help "Extended help text for input"
1729}
1730 "#,
1731 )
1732 .unwrap();
1733
1734 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1735 assert_eq!(input_arg.help, Some("Input file".to_string()));
1736 assert_eq!(
1737 input_arg.help_long,
1738 Some("Extended help text for input".to_string())
1739 );
1740 }
1741
1742 #[test]
1743 fn positional_conflicts_round_trip_without_dropping_members() {
1744 let spec: Spec = "arg \"[VALUE]\" { conflicts \"--from-file\" \"--stdin\" }\n"
1745 .parse()
1746 .unwrap();
1747 assert_eq!(
1748 spec.cmd.args[0].conflicts,
1749 vec!["--from-file".to_string(), "--stdin".to_string()]
1750 );
1751
1752 let rendered = spec.to_string();
1753 let reparsed: Spec = rendered.parse().unwrap();
1754 assert_eq!(reparsed.cmd.args[0].conflicts, spec.cmd.args[0].conflicts);
1755 }
1756
1757 #[test]
1758 fn a_complete_inside_an_arg_is_attached_to_it() {
1759 let spec: Spec = r#"
1760complete "file" run="echo named"
1761arg "<FILE>" {
1762 complete run="ls" descriptions=#true
1763}
1764arg "[other]"
1765flag "--color <when>" {
1766 arg "<when>" {
1767 complete type="none"
1768 }
1769}
1770flag "--out <path>" {
1771 complete type="path"
1772}
1773cmd "sub" {
1774 complete "other" run="echo cmd"
1775}
1776"#
1777 .parse()
1778 .unwrap();
1779
1780 let file = &spec.cmd.args[0];
1781 let complete = file.complete.as_ref().expect("the arg's own completer");
1782 assert_eq!(complete.name, "file");
1783 assert_eq!(complete.run.as_deref(), Some("ls"));
1784 assert!(complete.descriptions);
1785 assert_eq!(
1787 spec.completer(&spec.cmd, file).unwrap().run.as_deref(),
1788 Some("ls")
1789 );
1790 let other = &spec.cmd.args[1];
1792 assert!(other.complete.is_none());
1793 assert!(spec.completer(&spec.cmd, other).is_none());
1794 let sub = &spec.cmd.subcommands["sub"];
1795 assert_eq!(
1796 spec.completer(sub, other).unwrap().run.as_deref(),
1797 Some("echo cmd")
1798 );
1799
1800 let color = spec.cmd.flags[0].arg.as_ref().unwrap();
1801 assert_eq!(color.complete.as_ref().unwrap().name, "when");
1802 assert_eq!(
1803 color.complete.as_ref().unwrap().type_.as_deref(),
1804 Some("none")
1805 );
1806 let out = spec.cmd.flags[1].arg.as_ref().unwrap();
1808 assert_eq!(out.complete.as_ref().unwrap().name, "path");
1809 assert_eq!(
1810 out.complete.as_ref().unwrap().type_.as_deref(),
1811 Some("path")
1812 );
1813
1814 assert_snapshot!(spec, @r#"
1815 flag --color {
1816 arg <when> {
1817 complete type=none
1818 }
1819 }
1820 flag --out {
1821 arg <path> {
1822 complete type=path
1823 }
1824 }
1825 arg <FILE> {
1826 complete run=ls descriptions=#true
1827 }
1828 arg "[other]" required=#false
1829 complete file run="echo named"
1830 cmd sub {
1831 complete other run="echo cmd"
1832 }
1833 "#);
1834 let reparsed: Spec = spec.to_string().parse().unwrap();
1835 assert_eq!(reparsed.to_string(), spec.to_string());
1836 assert_eq!(
1837 reparsed.cmd.args[0]
1838 .complete
1839 .as_ref()
1840 .unwrap()
1841 .run
1842 .as_deref(),
1843 Some("ls")
1844 );
1845 }
1846
1847 #[test]
1848 fn a_complete_inside_an_arg_takes_no_name_and_appears_once() {
1849 let err = |kdl: &str| {
1850 let err = kdl.parse::<Spec>().unwrap_err();
1851 format!("{:?}", crate::miette::Error::from(err))
1852 };
1853 assert!(err(r#"arg "<file>" { complete "file" run="ls"; }"#).contains("takes no name"));
1854 assert!(
1855 err(r#"arg "<file>" { complete run="ls"; complete run="pwd"; }"#)
1856 .contains("only one complete")
1857 );
1858 assert!(
1859 err(r#"arg "<file>" { complete run="ls" type="file"; }"#)
1860 .contains("only one of run, type or delegate")
1861 );
1862 assert!(err(r#"flag "--force" { complete run="ls"; }"#).contains("must have value"));
1863 assert!(err(
1864 r#"flag "--out <path>" { complete run="ls"; arg "<path>" { complete run="pwd"; }; }"#
1865 )
1866 .contains("only one complete"));
1867 }
1868
1869 #[test]
1870 fn a_flag_complete_written_before_its_arg_is_kept() {
1871 let spec: Spec = r#"flag "--out <path>" { complete run="ls"; arg "<target>"; }"#
1872 .parse()
1873 .unwrap();
1874 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1875 assert_eq!(arg.name, "target");
1876 let complete = arg.complete.as_ref().unwrap();
1877 assert_eq!(complete.run.as_deref(), Some("ls"));
1878 assert_eq!(complete.name, "target");
1879 }
1880
1881 #[test]
1882 fn a_built_arg_names_its_completer_after_itself() {
1883 let arg = SpecArg::builder()
1884 .name("FILE")
1885 .complete(crate::SpecComplete::default().run("ls"))
1886 .build();
1887 assert_eq!(arg.complete.unwrap().name, "file");
1888 }
1889
1890 #[test]
1891 fn a_built_arg_normalizes_a_named_completer_the_way_kdl_does() {
1892 let mut spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
1895 spec.cmd.args.push(
1896 SpecArg::builder()
1897 .name("FILE")
1898 .complete(crate::SpecComplete::new("FILE").run("ls"))
1899 .build(),
1900 );
1901 assert_eq!(spec.cmd.args[0].complete.as_ref().unwrap().name, "file");
1902 let reparsed: Spec = spec.to_string().parse().unwrap();
1903 assert_eq!(
1904 reparsed.cmd.args[0].complete.as_ref().unwrap().name,
1905 spec.cmd.args[0].complete.as_ref().unwrap().name
1906 );
1907 }
1908}