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