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::{SpecCommandEffect, EFFECT_VALUES};
12use crate::spec::helpers::{string_entry, NodeHelper};
13use crate::spec::is_false;
14use crate::{string, SpecAdmonition, SpecAdmonitionKind, SpecChoices};
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}
204
205impl SpecArg {
206 pub fn builder() -> SpecArgBuilder {
208 SpecArgBuilder::new()
209 }
210
211 pub fn env_names(&self) -> impl Iterator<Item = &str> {
213 self.env
214 .iter()
215 .map(String::as_str)
216 .chain(self.env_fallback.iter().map(String::as_str))
217 .chain(self.deprecated_env.iter().map(String::as_str))
218 }
219
220 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
221 let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
222 for (k, v) in node.props() {
223 match k {
224 "help" => arg.help = Some(v.ensure_string()?),
225 "sigil" => arg.sigil = v.ensure_string().map(Some)?,
226 "long_help" => arg.help_long = Some(v.ensure_string()?),
227 "help_long" => arg.help_long = Some(v.ensure_string()?),
228 "help_md" => arg.help_md = Some(v.ensure_string()?),
229 "required" => arg.required = v.ensure_bool()?,
230 "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
231 "var" => arg.var = v.ensure_bool()?,
232 "delimiter" => {
233 let raw = v.ensure_string()?;
234 let mut chars = raw.chars();
235 match (chars.next(), chars.next()) {
236 (Some(c), None) if c.is_ascii() => arg.delimiter = Some(c),
242 (Some(c), None) => bail_parse!(
243 ctx,
244 v.entry.span(),
245 "a delimiter is one byte, and {c:?} is more than one; use an \
246 ASCII separator"
247 ),
248 _ => bail_parse!(
249 ctx,
250 v.entry.span(),
251 "a delimiter is one character, and {raw:?} is not"
252 ),
253 }
254 }
255 "allow_negative_numbers" => arg.allow_negative_numbers = v.ensure_bool()?,
256 "value_terminator" => arg.value_terminator = v.ensure_string().map(Some)?,
257 "hide" => arg.hide = v.ensure_bool()?,
258 "hide_default_value" => arg.hide_default_value = v.ensure_bool()?,
259 "hide_env" => arg.hide_env = v.ensure_bool()?,
260 "hide_env_values" => arg.hide_env_values = v.ensure_bool()?,
261 "hide_possible_values" => arg.hide_possible_values = v.ensure_bool()?,
262 "hide_short_help" => arg.hide_short_help = v.ensure_bool()?,
263 "hide_long_help" => arg.hide_long_help = v.ensure_bool()?,
264 "conflicts" => arg.conflicts = vec![v.ensure_string()?],
265 "requires" => arg.requires = vec![v.ensure_string()?],
266 "required_if" => arg.required_if = vec![v.ensure_string()?],
267 "required_unless" => arg.required_unless = vec![v.ensure_string()?],
268 "required_unless_all" => arg.required_unless_all = vec![v.ensure_string()?],
269 "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
270 "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
271 "default" => arg.default = vec![v.ensure_string()?],
272 "effect" => {
273 let raw = v.ensure_string()?;
274 match raw.parse() {
275 Ok(effect) => arg.effect = Some(effect),
276 Err(_) => bail_parse!(
277 ctx,
278 v.entry.span(),
279 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
280 ),
281 }
282 }
283 "env" => arg.env = v.ensure_string().map(Some)?,
284 "env_fallback" => arg.env_fallback = vec![v.ensure_string()?],
285 "deprecated_env" => arg.deprecated_env = vec![v.ensure_string()?],
286 "validate" => arg.validate = v.ensure_string().map(Some)?,
287 "validate_error" => arg.validate_error = v.ensure_string().map(Some)?,
288 "help_heading" => arg.help_heading = v.ensure_string().map(Some)?,
289 "surface" => arg.surface = v.ensure_string().map(Some)?,
290 "available_if" => arg.available_if = vec![v.ensure_string()?],
291 "display_order" => arg.display_order = v.ensure_usize().map(Some)?,
292 k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
293 }
294 }
295 if !arg.default.is_empty() {
296 arg.required = false;
297 }
298 for child in node.children() {
299 match child.name() {
300 "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
301 "effect" => {
302 let a = child.arg(0)?;
303 let raw = a.ensure_string()?;
304 match raw.parse() {
305 Ok(effect) => arg.effect = Some(effect),
306 Err(_) => bail_parse!(
307 ctx,
308 a.entry.span(),
309 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
310 ),
311 }
312 }
313 "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
314 "env_fallback" => arg.env_fallback = string_args(&child)?,
315 "deprecated_env" => arg.deprecated_env = string_args(&child)?,
316 "validate" => arg.validate = child.arg(0)?.ensure_string().map(Some)?,
317 "validate_error" => {
318 arg.validate_error = child.arg(0)?.ensure_string().map(Some)?;
319 }
320 "help_heading" => {
321 arg.help_heading = child.arg(0)?.ensure_string().map(Some)?;
322 }
323 "surface" => arg.surface = child.arg(0)?.ensure_string().map(Some)?,
324 "available_if" => arg.available_if = string_args(&child)?,
325 "display_order" => {
326 arg.display_order = child.arg(0)?.ensure_usize().map(Some)?;
327 }
328 "default" => {
329 let children = child.children();
333 if children.is_empty() {
334 arg.default = vec![child.arg(0)?.ensure_string()?];
336 } else {
337 arg.default = children.iter().map(|c| c.name().to_string()).collect();
340 }
341 }
342 "help" => arg.help = Some(child.arg(0)?.ensure_string()?),
343 "sigil" => arg.sigil = child.arg(0)?.ensure_string().map(Some)?,
344 "long_help" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
345 "help_long" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
346 "help_md" => arg.help_md = Some(child.arg(0)?.ensure_string()?),
347 "note" => arg
348 .admonitions
349 .push(SpecAdmonition::note(child.arg(0)?.ensure_string()?)),
350 "warning" => arg
351 .admonitions
352 .push(SpecAdmonition::warning(child.arg(0)?.ensure_string()?)),
353 "required" => arg.required = child.arg(0)?.ensure_bool()?,
354 "var" => arg.var = child.arg(0)?.ensure_bool()?,
355 "var_min" => arg.var_min = child.arg(0)?.ensure_usize().map(Some)?,
356 "var_max" => arg.var_max = child.arg(0)?.ensure_usize().map(Some)?,
357 "value_names" => {
358 arg.value_names = child
359 .ensure_arg_len(1..)?
360 .args()
361 .map(|entry| entry.ensure_string())
362 .collect::<Result<Vec<_>, _>>()?;
363 }
364 "allow_negative_numbers" => {
365 arg.allow_negative_numbers = child.arg(0)?.ensure_bool()?;
366 }
367 "value_terminator" => {
368 arg.value_terminator = child.arg(0)?.ensure_string().map(Some)?;
369 }
370 "hide" => arg.hide = child.arg(0)?.ensure_bool()?,
371 "hide_default_value" => arg.hide_default_value = child.arg(0)?.ensure_bool()?,
372 "hide_env" => arg.hide_env = child.arg(0)?.ensure_bool()?,
373 "hide_env_values" => arg.hide_env_values = child.arg(0)?.ensure_bool()?,
374 "hide_possible_values" => arg.hide_possible_values = child.arg(0)?.ensure_bool()?,
375 "hide_short_help" => arg.hide_short_help = child.arg(0)?.ensure_bool()?,
376 "hide_long_help" => arg.hide_long_help = child.arg(0)?.ensure_bool()?,
377 "conflicts" => {
378 arg.conflicts = child
379 .ensure_arg_len(1..)?
380 .args()
381 .map(|entry| entry.ensure_string())
382 .collect::<Result<Vec<_>, _>>()?;
383 }
384 "requires" => arg.requires = string_args(&child)?,
385 "required_if" => arg.required_if = string_args(&child)?,
386 "required_if_eq" => arg.required_if_eq.push(required_if_eq(&child)?),
387 "required_if_eq_all" => {
388 let len = child.args().count();
389 if len < 2 || len % 2 != 0 {
390 bail_parse!(
391 ctx,
392 child.node.name().span(),
393 "required_if_eq_all needs selector/value pairs"
394 );
395 }
396 arg.required_if_eq_all = required_if_eq_pairs(&child)?;
397 }
398 "required_unless" => arg.required_unless = string_args(&child)?,
399 "required_unless_all" => arg.required_unless_all = string_args(&child)?,
400 "double_dash" => arg.double_dash = child.arg(0)?.ensure_string()?.parse()?,
401 k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
402 }
403 }
404 if let Some(sigil) = &arg.sigil {
405 if let Some(name) = arg.name.strip_prefix(sigil) {
406 arg.name = name.to_string();
407 }
408 }
409 if let Some(first) = arg.value_names.first() {
410 arg.name.clone_from(first);
411 }
412 if arg.value_names.len() > 1 {
413 let arity = arg.value_names.len();
414 match (arg.var_min, arg.var_max) {
415 (None, None) => {
416 arg.var_min = Some(arity);
417 arg.var_max = Some(arity);
418 }
419 (Some(min), Some(max)) if min == arity && max == arity => {}
420 _ => bail_parse!(
421 ctx,
422 node.node.name().span(),
423 "{arity} value names require var_min={arity} and var_max={arity}"
424 ),
425 }
426 arg.var = true;
427 }
428 if arg.validate_error.is_some() && arg.validate.is_none() {
429 bail_parse!(
430 ctx,
431 node.node.name().span(),
432 "validate_error requires a validate expression"
433 );
434 }
435 if arg.value_terminator.as_deref() == Some("") {
436 bail_parse!(
437 ctx,
438 node.node.name().span(),
439 "value_terminator cannot be empty"
440 );
441 }
442 if arg.value_terminator.is_some() && !arg.var {
443 bail_parse!(
444 ctx,
445 node.node.name().span(),
446 "value_terminator requires a variadic argument"
447 );
448 }
449 if let Some(sigil) = &arg.sigil {
450 if sigil.is_empty() {
451 bail_parse!(ctx, node.node.name().span(), "sigil cannot be empty");
452 }
453 if sigil.starts_with('-') {
454 bail_parse!(
455 ctx,
456 node.node.name().span(),
457 "sigil cannot start with `-`; that namespace belongs to flags"
458 );
459 }
460 if sigil.chars().any(char::is_whitespace) {
461 bail_parse!(
462 ctx,
463 node.node.name().span(),
464 "sigil cannot contain whitespace"
465 );
466 }
467 if arg.double_dash != SpecDoubleDashChoices::Optional {
468 bail_parse!(
469 ctx,
470 node.node.name().span(),
471 "sigil arguments cannot declare non-optional double_dash behavior"
472 );
473 }
474 if arg.value_terminator.is_some() {
475 bail_parse!(
476 ctx,
477 node.node.name().span(),
478 "sigil arguments cannot declare value_terminator"
479 );
480 }
481 if arg.var_min.is_some() || arg.var_max.is_some() {
482 bail_parse!(
483 ctx,
484 node.node.name().span(),
485 "sigil arguments cannot declare var_min or var_max"
486 );
487 }
488 }
489 #[cfg(feature = "validation")]
490 if let Some(expression) = &arg.validate {
491 if let Err(error) = usage_validation::check(expression) {
492 bail_parse!(
493 ctx,
494 node.node.name().span(),
495 "invalid validation expression: {error}"
496 );
497 }
498 }
499 arg.usage = arg.usage();
500 if let Some(help) = &arg.help {
501 arg.help_first_line = Some(string::first_line(help));
502 }
503 Ok(arg)
504 }
505}
506
507impl SpecArg {
508 pub fn usage(&self) -> String {
509 let exact_arity = self.var.then_some(()).and_then(|()| {
510 self.var_min
511 .zip(self.var_max)
512 .filter(|(min, max)| min == max && *min > 1)
513 .map(|(arity, _)| arity)
514 });
515 if self.value_names.len() > 1 || exact_arity.is_some() {
516 let labels = if self.value_names.len() > 1 {
517 self.value_names.clone()
518 } else {
519 vec![
520 self.value_names
521 .first()
522 .cloned()
523 .unwrap_or_else(|| self.name.clone());
524 exact_arity.expect("branch checked")
525 ]
526 };
527 let placeholders = labels
528 .iter()
529 .map(|name| {
530 if self.required {
531 format!("<{name}>")
532 } else {
533 format!("[{name}]")
534 }
535 })
536 .collect::<Vec<_>>()
537 .join(" ");
538 return if self.double_dash == SpecDoubleDashChoices::Required {
539 format!("-- {placeholders}")
540 } else {
541 placeholders
542 };
543 }
544 let name = if let Some(sigil) = &self.sigil {
545 format!("{sigil}{}", self.name)
546 } else if self.double_dash == SpecDoubleDashChoices::Required {
547 format!("-- {}", self.name)
548 } else {
549 self.name.clone()
550 };
551 let mut name = if self.required {
552 format!("<{name}>")
553 } else {
554 format!("[{name}]")
555 };
556 if self.var {
557 name = format!("{name}…");
558 }
559 name
560 }
561}
562
563impl From<&SpecArg> for KdlNode {
564 fn from(arg: &SpecArg) -> Self {
565 let mut node = KdlNode::new("arg");
566 node.push(KdlEntry::new(arg.usage()));
567 if let Some(sigil) = &arg.sigil {
568 node.push(string_entry(Some("sigil"), sigil));
569 }
570 if let Some(desc) = &arg.help {
571 node.push(string_entry(Some("help"), desc));
572 }
573 if let Some(desc) = &arg.help_long {
574 node.push(string_entry(Some("help_long"), desc));
575 }
576 if let Some(desc) = &arg.help_md {
577 node.push(string_entry(Some("help_md"), desc));
578 }
579 for admonition in &arg.admonitions {
580 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
581 let name = match admonition.kind {
582 SpecAdmonitionKind::Note => "note",
583 SpecAdmonitionKind::Warning => "warning",
584 };
585 let mut block = KdlNode::new(name);
586 block.push(string_entry(None, &admonition.text));
587 children.nodes_mut().push(block);
588 }
589 if !arg.required {
590 node.push(KdlEntry::new_prop("required", false));
591 }
592 if arg.double_dash == SpecDoubleDashChoices::Automatic
593 || arg.double_dash == SpecDoubleDashChoices::Preserve
594 {
595 node.push(KdlEntry::new_prop(
596 "double_dash",
597 arg.double_dash.to_string(),
598 ));
599 }
600 if arg.var {
601 node.push(KdlEntry::new_prop("var", true));
602 }
603 if let Some(min) = arg.var_min {
604 node.push(KdlEntry::new_prop("var_min", min as i128));
605 }
606 if let Some(max) = arg.var_max {
607 node.push(KdlEntry::new_prop("var_max", max as i128));
608 }
609 if let Some(delimiter) = arg.delimiter {
610 node.push(string_entry(Some("delimiter"), &delimiter.to_string()));
611 }
612 if arg.allow_negative_numbers {
613 node.push(KdlEntry::new_prop("allow_negative_numbers", true));
614 }
615 if let Some(terminator) = &arg.value_terminator {
616 node.push(string_entry(Some("value_terminator"), terminator));
617 }
618 if arg.hide {
619 node.push(KdlEntry::new_prop("hide", true));
620 }
621 for (name, hidden) in [
622 ("hide_default_value", arg.hide_default_value),
623 ("hide_env", arg.hide_env),
624 ("hide_env_values", arg.hide_env_values),
625 ("hide_possible_values", arg.hide_possible_values),
626 ("hide_short_help", arg.hide_short_help),
627 ("hide_long_help", arg.hide_long_help),
628 ] {
629 if hidden {
630 node.push(KdlEntry::new_prop(name, true));
631 }
632 }
633 if arg.conflicts.len() == 1 {
634 node.push(string_entry(Some("conflicts"), &arg.conflicts[0]));
635 } else if !arg.conflicts.is_empty() {
636 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
637 let mut conflicts = KdlNode::new("conflicts");
638 for target in &arg.conflicts {
639 conflicts.push(string_entry(None, target));
640 }
641 children.nodes_mut().push(conflicts);
642 }
643 serialize_selector_list(&mut node, "requires", &arg.requires);
644 serialize_selector_list(&mut node, "required_if", &arg.required_if);
645 serialize_required_if_eq(&mut node, "required_if_eq", &arg.required_if_eq);
646 if !arg.required_if_eq_all.is_empty() {
647 serialize_required_if_eq(&mut node, "required_if_eq_all", &arg.required_if_eq_all);
648 }
649 serialize_selector_list(&mut node, "required_unless", &arg.required_unless);
650 serialize_selector_list(&mut node, "required_unless_all", &arg.required_unless_all);
651 if !arg.default.is_empty() {
653 if arg.default.len() == 1 {
654 node.push(string_entry(Some("default"), &arg.default[0]));
656 } else {
657 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
659 let mut default_node = KdlNode::new("default");
660 let default_children = default_node
661 .children_mut()
662 .get_or_insert_with(KdlDocument::new);
663 for val in &arg.default {
664 default_children
665 .nodes_mut()
666 .push(KdlNode::new(val.as_str()));
667 }
668 children.nodes_mut().push(default_node);
669 }
670 }
671 if let Some(env) = &arg.env {
672 node.push(string_entry(Some("env"), env));
673 }
674 serialize_selector_list(&mut node, "env_fallback", &arg.env_fallback);
675 serialize_selector_list(&mut node, "deprecated_env", &arg.deprecated_env);
676 if let Some(validate) = &arg.validate {
677 node.push(string_entry(Some("validate"), validate));
678 }
679 if arg.validate.is_some() {
680 if let Some(error) = &arg.validate_error {
681 node.push(string_entry(Some("validate_error"), error));
682 }
683 }
684 if let Some(help_heading) = &arg.help_heading {
685 node.push(string_entry(Some("help_heading"), help_heading));
686 }
687 if let Some(surface) = &arg.surface {
688 node.push(string_entry(Some("surface"), surface));
689 }
690 serialize_selector_list(&mut node, "available_if", &arg.available_if);
691 if let Some(order) = arg.display_order {
692 node.push(KdlEntry::new_prop("display_order", order as i128));
693 }
694 if let Some(effect) = &arg.effect {
695 node.push(string_entry(Some("effect"), effect.as_str()));
696 }
697 if let Some(choices) = &arg.choices {
698 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
699 children.nodes_mut().push(choices.into());
700 }
701 node
702 }
703}
704
705fn string_args(node: &NodeHelper<'_>) -> Result<Vec<String>, UsageErr> {
706 node.ensure_arg_len(1..)?
707 .args()
708 .map(|entry| entry.ensure_string())
709 .collect()
710}
711
712fn required_if_eq(node: &NodeHelper<'_>) -> Result<SpecRequiredIfEq, UsageErr> {
713 node.ensure_arg_len(2..=2)?;
714 Ok(SpecRequiredIfEq {
715 selector: node.arg(0)?.ensure_string()?,
716 value: node.arg(1)?.ensure_string()?,
717 })
718}
719
720fn required_if_eq_pairs(node: &NodeHelper<'_>) -> Result<Vec<SpecRequiredIfEq>, UsageErr> {
721 let entries = node.args().collect::<Vec<_>>();
722 entries
723 .as_chunks::<2>()
724 .0
725 .iter()
726 .map(|pair| {
727 Ok(SpecRequiredIfEq {
728 selector: pair[0].ensure_string()?,
729 value: pair[1].ensure_string()?,
730 })
731 })
732 .collect()
733}
734
735fn serialize_selector_list(node: &mut KdlNode, name: &str, selectors: &[String]) {
736 if selectors.len() == 1 {
737 node.push(string_entry(Some(name), &selectors[0]));
738 } else if !selectors.is_empty() {
739 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
740 let mut relation = KdlNode::new(name);
741 for selector in selectors {
742 relation.push(string_entry(None, selector));
743 }
744 children.nodes_mut().push(relation);
745 }
746}
747
748fn serialize_required_if_eq(node: &mut KdlNode, name: &str, conditions: &[SpecRequiredIfEq]) {
749 if conditions.is_empty() {
750 return;
751 }
752 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
753 if name == "required_if_eq_all" {
754 let mut relation = KdlNode::new(name);
755 for condition in conditions {
756 relation.push(string_entry(None, &condition.selector));
757 relation.push(string_entry(None, &condition.value));
758 }
759 children.nodes_mut().push(relation);
760 } else {
761 for condition in conditions {
762 let mut relation = KdlNode::new(name);
763 relation.push(string_entry(None, &condition.selector));
764 relation.push(string_entry(None, &condition.value));
765 children.nodes_mut().push(relation);
766 }
767 }
768}
769
770impl From<&str> for SpecArg {
771 fn from(input: &str) -> Self {
772 let (input, after_double_dash) = input
773 .strip_prefix("-- ")
774 .map_or((input, false), |rest| (rest, true));
775 if let Some(placeholders) = fixed_placeholders(input) {
776 let required = placeholders
777 .iter()
778 .all(|placeholder| placeholder.starts_with('<'));
779 let value_names = placeholders
780 .iter()
781 .map(|placeholder| placeholder[1..placeholder.len() - 1].to_string())
782 .collect::<Vec<_>>();
783 let mut arg = SpecArg {
784 name: value_names[0].clone(),
785 value_names,
786 required,
787 var: true,
788 var_min: Some(placeholders.len()),
789 var_max: Some(placeholders.len()),
790 double_dash: if after_double_dash {
791 SpecDoubleDashChoices::Required
792 } else {
793 SpecDoubleDashChoices::Optional
794 },
795 ..Default::default()
796 };
797 arg.usage = arg.usage();
798 return arg;
799 }
800 let mut arg = SpecArg {
801 name: input.to_string(),
802 required: true,
803 double_dash: if after_double_dash {
804 SpecDoubleDashChoices::Required
805 } else {
806 SpecDoubleDashChoices::Optional
807 },
808 ..Default::default()
809 };
810 if let Some(name) = arg
812 .name
813 .strip_suffix("...")
814 .or_else(|| arg.name.strip_suffix("…"))
815 {
816 arg.var = true;
817 arg.name = name.to_string();
818 }
819 let first = arg.name.chars().next().unwrap_or_default();
820 let last = arg.name.chars().last().unwrap_or_default();
821 match (first, last) {
822 ('[', ']') => {
823 arg.name = arg.name[1..arg.name.len() - 1].to_string();
824 arg.required = false;
825 }
826 ('<', '>') => {
827 arg.name = arg.name[1..arg.name.len() - 1].to_string();
828 }
829 _ => {}
830 }
831 if let Some(name) = arg.name.strip_prefix("-- ") {
835 arg.double_dash = SpecDoubleDashChoices::Required;
836 arg.name = name.to_string();
837 }
838 if !arg.var {
840 if let Some(name) = arg
841 .name
842 .strip_suffix("...")
843 .or_else(|| arg.name.strip_suffix("…"))
844 {
845 arg.var = true;
846 arg.name = name.to_string();
847 }
848 }
849 arg.usage = arg.usage();
854 arg
855 }
856}
857impl FromStr for SpecArg {
858 type Err = UsageErr;
859 fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
860 if fixed_placeholders(input.strip_prefix("-- ").unwrap_or(input)).is_some_and(
861 |placeholders| {
862 placeholders
863 .windows(2)
864 .any(|pair| pair[0].starts_with('<') != pair[1].starts_with('<'))
865 },
866 ) {
867 let message =
868 "fixed-arity placeholders must be either all required or all optional".to_string();
869 return Err(UsageErr::InvalidInput(
870 message,
871 (0, input.len()).into(),
872 miette::NamedSource::new("argument", input.to_string()),
873 ));
874 }
875 Ok(input.into())
876 }
877}
878
879fn fixed_placeholders(input: &str) -> Option<Vec<&str>> {
882 if !input.bytes().any(|byte| byte.is_ascii_whitespace()) {
883 return None;
884 }
885 let placeholders: Vec<_> = input.split_whitespace().collect();
886 (placeholders.len() > 1
887 && placeholders.iter().all(|placeholder| {
888 matches!(
889 (placeholder.chars().next(), placeholder.chars().last()),
890 (Some('<'), Some('>')) | (Some('['), Some(']'))
891 )
892 }))
893 .then_some(placeholders)
894}
895
896#[cfg(feature = "clap")]
904pub(crate) fn default_values(arg: &clap::Arg) -> Vec<String> {
905 let raw = arg
906 .get_default_values()
907 .iter()
908 .map(|v| v.to_string_lossy().to_string());
909 match arg.get_value_delimiter() {
910 Some(delimiter) => raw
911 .flat_map(|v| {
912 v.split(delimiter)
913 .map(|part| part.to_string())
914 .collect::<Vec<_>>()
915 })
916 .collect(),
917 None => raw.collect(),
918 }
919}
920
921#[cfg(feature = "clap")]
928pub(crate) fn value_bounds(source: &clap::Arg, target: &mut SpecArg, zero_values_supported: bool) {
929 if source.get_value_delimiter().is_some() {
934 return;
935 }
936
937 let Some(range) = source.get_num_args() else {
938 if target.value_names.len() > 1 {
939 let arity = target.value_names.len();
940 target.var = true;
941 target.var_min = Some(arity);
942 target.var_max = Some(arity);
943 }
944 return;
945 };
946 let min = range.min_values();
947 let max = range.max_values();
948 if max <= 1 || min == 0 && !zero_values_supported {
949 return;
950 }
951
952 target.var = true;
953 target.var_min = Some(min);
954 target.var_max = (max != usize::MAX).then_some(max);
955}
956
957#[cfg(feature = "clap")]
963pub(crate) fn value_names_from_clap(source: &clap::Arg) -> Vec<String> {
964 let names: Vec<String> = source
965 .get_value_names()
966 .unwrap_or_default()
967 .iter()
968 .map(ToString::to_string)
969 .collect();
970 if names.len() <= 1 {
971 return names;
972 }
973 let mismatched_range = source.get_num_args().is_some_and(|range| {
974 range.min_values() != names.len() || range.max_values() != names.len()
975 });
976 if source.get_value_delimiter().is_some() || mismatched_range {
977 names.into_iter().take(1).collect()
978 } else {
979 names
980 }
981}
982
983#[cfg(feature = "clap")]
985pub(crate) fn value_hint_type(hint: clap::ValueHint) -> Option<&'static str> {
986 use clap::ValueHint;
987
988 match hint {
989 ValueHint::Unknown => None,
990 ValueHint::Other => Some("none"),
991 ValueHint::AnyPath | ValueHint::FilePath => Some("path"),
992 ValueHint::DirPath => Some("dir"),
993 ValueHint::ExecutablePath => Some("executable"),
994 ValueHint::CommandName | ValueHint::CommandString => Some("command"),
995 ValueHint::CommandWithArguments => Some("command_args"),
996 ValueHint::Username => Some("username"),
997 ValueHint::Hostname => Some("hostname"),
998 ValueHint::Url => Some("url"),
999 ValueHint::EmailAddress => Some("email"),
1000 _ => None,
1001 }
1002}
1003
1004#[cfg(feature = "clap")]
1005pub(crate) fn choices_from_clap(arg: &clap::Arg) -> Option<SpecChoices> {
1006 let possible = arg.get_possible_values();
1007 if possible.is_empty() {
1008 return None;
1009 }
1010 let choices = possible
1011 .iter()
1012 .map(|value| value.get_name().to_string())
1013 .collect();
1014 let details = possible
1015 .iter()
1016 .filter_map(|value| {
1017 let aliases: Vec<_> = value
1018 .get_name_and_aliases()
1019 .skip(1)
1020 .map(|alias| SpecChoiceAlias {
1021 value: alias.to_string(),
1022 hide: true,
1024 })
1025 .collect();
1026 let detail = SpecChoice {
1027 value: value.get_name().to_string(),
1028 help: value.get_help().map(ToString::to_string),
1029 hide: value.is_hide_set(),
1030 aliases,
1031 };
1032 (detail.help.is_some() || detail.hide || !detail.aliases.is_empty()).then_some(detail)
1033 })
1034 .collect();
1035 Some(SpecChoices {
1036 choices,
1037 details,
1038 ignore_case: arg.is_ignore_case_set(),
1039 ..Default::default()
1040 })
1041}
1042
1043#[cfg(feature = "clap")]
1044impl From<&clap::Arg> for SpecArg {
1045 fn from(arg: &clap::Arg) -> Self {
1046 let source = arg;
1047 let required = arg.is_required_set();
1048 let help = arg.get_help().map(|s| s.to_string());
1049 let help_long = arg.get_long_help().map(|s| s.to_string());
1050 let help_first_line = help.as_ref().map(|s| string::first_line(s));
1051 let hide = arg.is_hide_set();
1052 let delimiter = arg.get_value_delimiter();
1056 let recorded_delimiter = delimiter.filter(char::is_ascii);
1057 let value_terminator = arg.get_value_terminator().map(ToString::to_string);
1058 let var = matches!(
1059 arg.get_action(),
1060 clap::ArgAction::Count | clap::ArgAction::Append
1061 ) || delimiter.is_some();
1062 let choices = choices_from_clap(arg);
1063 let value_names = value_names_from_clap(arg);
1064 let mut arg = Self {
1065 name: value_names
1066 .first()
1067 .cloned()
1068 .unwrap_or_else(|| source.get_id().to_string()),
1069 sigil: None,
1070 value_names,
1071 usage: "".into(),
1072 required,
1073 double_dash: if arg.is_last_set() {
1074 SpecDoubleDashChoices::Required
1075 } else if arg.is_trailing_var_arg_set() {
1076 SpecDoubleDashChoices::Automatic
1077 } else {
1078 SpecDoubleDashChoices::Optional
1079 },
1080 help,
1081 help_long,
1082 help_md: None,
1083 admonitions: Vec::new(),
1084 help_first_line,
1085 var,
1086 var_max: None,
1087 var_min: None,
1088 delimiter: recorded_delimiter,
1091 allow_negative_numbers: arg.is_allow_negative_numbers_set(),
1092 value_terminator: None,
1093 hide,
1094 hide_default_value: arg.is_hide_default_value_set(),
1095 hide_env: arg.is_hide_env_set(),
1096 hide_env_values: arg.is_hide_env_values_set(),
1097 hide_possible_values: arg.is_hide_possible_values_set(),
1098 hide_short_help: arg.is_hide_short_help_set(),
1099 hide_long_help: arg.is_hide_long_help_set(),
1100 conflicts: Vec::new(),
1101 requires: Vec::new(),
1102 required_if: Vec::new(),
1103 required_if_eq: Vec::new(),
1104 required_if_eq_all: Vec::new(),
1105 required_unless: Vec::new(),
1106 required_unless_all: Vec::new(),
1107 default: default_values(arg),
1108 choices: None,
1109 validate: None,
1110 validate_error: None,
1111 effect: None,
1112 env: None,
1113 env_fallback: Vec::new(),
1114 deprecated_env: Vec::new(),
1115 help_heading: arg.get_help_heading().map(|s| s.to_string()),
1116 surface: None,
1117 available_if: Vec::new(),
1118 display_order: Some(arg.get_display_order()),
1119 };
1120 arg.choices = choices;
1121
1122 value_bounds(source, &mut arg, true);
1123 if arg.var {
1124 arg.value_terminator = value_terminator;
1125 }
1126
1127 arg
1128 }
1129}
1130
1131impl Display for SpecArg {
1132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1133 write!(f, "{}", self.usage())
1134 }
1135}
1136impl PartialEq for SpecArg {
1137 fn eq(&self, other: &Self) -> bool {
1138 self.name == other.name
1139 }
1140}
1141impl Eq for SpecArg {}
1142impl Hash for SpecArg {
1143 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1144 self.name.hash(state);
1145 }
1146}
1147
1148#[cfg(all(test, feature = "validation"))]
1149mod validation_tests {
1150 use std::collections::HashMap;
1151
1152 use crate::{parse, parse::Parser, Spec};
1153
1154 fn spec() -> Spec {
1155 r#"
1156name "ex"
1157bin "ex"
1158arg "<port>" validate="int(value) >= 1 && int(value) <= 65535" validate_error="must be a valid port"
1159 "#
1160 .parse()
1161 .unwrap()
1162 }
1163
1164 #[test]
1165 fn validation_round_trips_through_kdl() {
1166 let spec = spec();
1167 let kdl = spec.to_string();
1168 let reparsed: Spec = kdl.parse().unwrap();
1169 let arg = &reparsed.cmd.args[0];
1170 assert_eq!(
1171 arg.validate.as_deref(),
1172 Some("int(value) >= 1 && int(value) <= 65535")
1173 );
1174 assert_eq!(arg.validate_error.as_deref(), Some("must be a valid port"));
1175 }
1176
1177 #[test]
1178 fn invalid_validation_declarations_are_rejected_with_the_spec() {
1179 let missing_expression = r#"name "demo"
1180bin "demo"
1181arg "<port>" validate_error="must be a port"
1182"#;
1183 assert!(missing_expression.parse::<Spec>().is_err());
1184
1185 let invalid_expression = r#"name "demo"
1186bin "demo"
1187arg "<port>" validate="int(value) >"
1188"#;
1189 assert!(invalid_expression.parse::<Spec>().is_err());
1190 }
1191
1192 #[test]
1193 fn reference_parser_validates_each_raw_value() {
1194 parse(&spec(), &["ex".to_string(), "9229".to_string()]).unwrap();
1195
1196 let error = parse(&spec(), &["ex".to_string(), "0".to_string()]).unwrap_err();
1197 assert!(
1198 error.to_string().contains("must be a valid port"),
1199 "{error:?}"
1200 );
1201
1202 let variadic: Spec = r#"
1203name "ex"
1204bin "ex"
1205arg "<port>" var=#true validate="int(value) > 0" validate_error="port must be positive"
1206 "#
1207 .parse()
1208 .unwrap();
1209 let error = parse(
1210 &variadic,
1211 &["ex".to_string(), "0".to_string(), "-1".to_string()],
1212 )
1213 .unwrap_err()
1214 .to_string();
1215 assert_eq!(error.matches("port must be positive").count(), 1, "{error}");
1216 }
1217
1218 #[test]
1219 fn reference_parser_validates_environment_and_default_fallbacks() {
1220 let spec: Spec = r#"
1221name "ex"
1222bin "ex"
1223arg "[port]" env="PORT" validate="int(value) > 0" validate_error="port must be positive"
1224flag "--mode" default="bad" {
1225 arg "<mode>" validate="value == 'good'" validate_error="mode must be good"
1226}
1227arg "[ports]..." env="PORTS" var=#true var_max=1 delimiter="," validate="int(value) > 0" validate_error="all ports must be positive"
1228flag "--levels" env="LEVELS" {
1229 arg "<level>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all levels must be good"
1230}
1231flag "--modes" default="good,bad" {
1232 arg "<mode>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all modes must be good"
1233}
1234flag "--conditional" {
1235 default_if "--trigger" "good,bad"
1236 arg "<conditional>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all conditional values must be good"
1237}
1238flag "--repeats <repeat>" env="REPEATS" var=#true var_max=1 delimiter=","
1239flag "--trigger"
1240 "#
1241 .parse()
1242 .unwrap();
1243 let env = HashMap::from([
1244 ("PORT".to_string(), "0".to_string()),
1245 ("PORTS".to_string(), "1,0".to_string()),
1246 ("LEVELS".to_string(), "good,bad".to_string()),
1247 ("REPEATS".to_string(), "one,two".to_string()),
1248 ]);
1249 let error = Parser::new(&spec)
1250 .with_env(env)
1251 .parse(&["ex".to_string(), "--trigger".to_string()])
1252 .unwrap_err();
1253 let error = error.to_string();
1254 assert!(error.contains("port must be positive"), "{error}");
1255 assert!(error.contains("mode must be good"), "{error}");
1256 assert!(error.contains("all ports must be positive"), "{error}");
1257 assert!(error.contains("all levels must be good"), "{error}");
1258 assert!(error.contains("all modes must be good"), "{error}");
1259 assert!(
1260 error.contains("all conditional values must be good"),
1261 "{error}"
1262 );
1263 assert!(
1264 error.contains("Variadic argument <ports> accepts at most 1 value(s), got 2"),
1265 "{error}"
1266 );
1267 for flag in ["levels", "modes", "conditional", "repeats"] {
1268 assert!(
1269 error.contains(&format!(
1270 "Variadic flag --{flag} accepts at most 1 value(s), got 2"
1271 )),
1272 "{error}"
1273 );
1274 }
1275 }
1276}
1277
1278#[cfg(test)]
1279mod sigil_tests {
1280 use crate::Spec;
1281
1282 #[test]
1283 fn sigil_arguments_reject_value_bounds() {
1284 for bound in ["var_min=1", "var_max=2"] {
1285 let spec = format!("arg \"[tools]...\" sigil=\"+\" {bound}\n");
1286 let error = spec.parse::<Spec>().unwrap_err();
1287 assert!(
1288 format!("{error:?}").contains("cannot declare var_min or var_max"),
1289 "{error:?}"
1290 );
1291 }
1292 }
1293}
1294
1295#[cfg(test)]
1296mod delimiter_tests {
1297 use crate::Spec;
1298
1299 #[test]
1300 fn a_delimiter_has_to_be_one_byte() {
1301 for spec in [
1306 "flag \"--tags <tag>\" var=#true delimiter=\"§\"\n",
1307 "arg \"[tags]...\" var=#true delimiter=\"、\"\n",
1308 ] {
1309 let err = spec.parse::<Spec>().unwrap_err();
1310 assert!(format!("{err:?}").contains("one byte"), "{err:?}");
1311 }
1312
1313 let cmd = clap::Command::new("ex").arg(
1317 clap::Arg::new("tags")
1318 .long("tags")
1319 .value_delimiter('、')
1320 .action(clap::ArgAction::Set),
1321 );
1322 let spec = Spec::from(&cmd);
1323 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1324 assert_eq!(
1325 arg.delimiter, None,
1326 "a separator it cannot write is not recorded"
1327 );
1328 assert!(arg.var, "clap still splits, so the values still arrive");
1329 spec.to_string()
1330 .parse::<Spec>()
1331 .expect("what the bridge produces has to parse back");
1332 }
1333
1334 #[test]
1335 fn a_delimiter_round_trips_and_comes_across_from_clap() {
1336 let spec: Spec = "flag \"--tags <tag>\" var=#true delimiter=\",\"\n"
1337 .parse()
1338 .unwrap();
1339 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1340 assert_eq!(arg.delimiter, Some(','));
1341
1342 let reparsed: Spec = spec.to_string().parse().unwrap();
1343 let arg = reparsed.cmd.flags[0].arg.as_ref().unwrap();
1344 assert_eq!(arg.delimiter, Some(','), "{spec}");
1345
1346 let cmd = clap::Command::new("ex").arg(
1349 clap::Arg::new("tags")
1350 .long("tags")
1351 .value_delimiter(',')
1352 .num_args(1..)
1353 .default_value("a,b"),
1354 );
1355 let spec = Spec::from(&cmd);
1356 let flag = &spec.cmd.flags[0];
1357 assert_eq!(flag.arg.as_ref().unwrap().delimiter, Some(','));
1358 assert_eq!(flag.default, vec!["a", "b"]);
1361 }
1362
1363 #[test]
1364 fn a_single_valued_clap_arg_keeps_its_delimiter() {
1365 let cmd = clap::Command::new("ex").arg(
1370 clap::Arg::new("tags")
1371 .long("tags")
1372 .action(clap::ArgAction::Set)
1373 .value_delimiter(','),
1374 );
1375 let spec = Spec::from(&cmd);
1376 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1377 assert_eq!(arg.delimiter, Some(','));
1378 assert!(arg.var, "a delimiter brings `var` with it");
1381 let _: Spec = spec.to_string().parse().expect("{spec}");
1382 }
1383
1384 #[test]
1385 fn a_single_valued_clap_positional_splits_into_stored_values() {
1386 let cmd = clap::Command::new("ex").arg(
1390 clap::Arg::new("tags")
1391 .action(clap::ArgAction::Set)
1392 .value_delimiter(',')
1393 .value_parser(["a", "b"]),
1394 );
1395 let spec = Spec::from(&cmd);
1396 let arg = &spec.cmd.args[0];
1397 assert!(arg.var, "a positional delimiter brings `var` with it");
1398 assert_eq!(arg.delimiter, Some(','));
1399
1400 let input = ["ex", "a,b"].map(str::to_string);
1401 let parsed = crate::parse(&spec, &input).expect("both split values are choices");
1402 let value = parsed
1403 .args
1404 .values()
1405 .next()
1406 .expect("the positional was stored");
1407 assert!(matches!(
1408 value,
1409 crate::parse::ParseValue::MultiString(values)
1410 if values == &["a".to_string(), "b".to_string()]
1411 ));
1412 }
1413
1414 #[test]
1415 fn a_delimiter_needs_somewhere_to_put_what_it_splits() {
1416 let err = "flag \"--tags <tag>\" delimiter=\",\"\n"
1418 .parse::<Spec>()
1419 .unwrap_err();
1420 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1421
1422 let err = "arg \"[tags]\" delimiter=\",\"\n"
1423 .parse::<Spec>()
1424 .unwrap_err();
1425 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1426
1427 let err = "flag \"--quiet\" delimiter=\",\"\n"
1429 .parse::<Spec>()
1430 .unwrap_err();
1431 assert!(format!("{err:?}").contains("takes none"), "{err:?}");
1432
1433 let err = "flag \"--tags <tag>\" var=#true delimiter=\"::\"\n"
1435 .parse::<Spec>()
1436 .unwrap_err();
1437 assert!(format!("{err:?}").contains("one character"), "{err:?}");
1438 }
1439}
1440
1441#[cfg(test)]
1442mod possible_value_tests {
1443 use clap::builder::PossibleValue;
1444
1445 #[test]
1446 fn clap_possible_value_metadata_survives_the_bridge() {
1447 let command = clap::Command::new("ex").arg(
1448 clap::Arg::new("color").ignore_case(true).value_parser([
1449 PossibleValue::new("always")
1450 .help("Always use color")
1451 .alias("yes"),
1452 PossibleValue::new("never").hide(true),
1453 ]),
1454 );
1455 let spec = crate::Spec::from(&command);
1456 let choices = spec.cmd.args[0].choices.as_ref().unwrap();
1457 assert_eq!(choices.choices, ["always", "never"]);
1458 assert!(choices.ignore_case);
1459 assert!(choices.matches("YES"));
1460 assert_eq!(choices.values(), ["always"]);
1461 assert_eq!(choices.details[0].help.as_deref(), Some("Always use color"));
1462 assert!(choices.details[0].aliases[0].hide);
1463 assert!(choices.details[1].hide);
1464 }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469 use crate::{Spec, SpecArg};
1470 use insta::assert_snapshot;
1471
1472 #[test]
1473 fn test_arg_with_env() {
1474 let spec = Spec::parse(
1475 &Default::default(),
1476 r#"
1477arg "<input>" env="MY_INPUT" help="Input file"
1478arg "<output>" env="MY_OUTPUT"
1479 "#,
1480 )
1481 .unwrap();
1482
1483 assert_snapshot!(spec, @r#"
1484 arg <input> help="Input file" env=MY_INPUT
1485 arg <output> env=MY_OUTPUT
1486 "#);
1487
1488 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1489 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1490
1491 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1492 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1493 }
1494
1495 #[test]
1496 fn test_arg_with_env_child_node() {
1497 let spec = Spec::parse(
1498 &Default::default(),
1499 r#"
1500arg "<input>" help="Input file" {
1501 env "MY_INPUT"
1502}
1503arg "<output>" {
1504 env "MY_OUTPUT"
1505}
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_variadic_syntax() {
1524 use crate::SpecArg;
1525
1526 let arg: SpecArg = "<files>...".into();
1528 assert_eq!(arg.name, "files");
1529 assert!(arg.var);
1530 assert!(arg.required);
1531
1532 let arg: SpecArg = "[files]...".into();
1534 assert_eq!(arg.name, "files");
1535 assert!(arg.var);
1536 assert!(!arg.required);
1537
1538 let arg: SpecArg = "<files>…".into();
1540 assert_eq!(arg.name, "files");
1541 assert!(arg.var);
1542
1543 let arg: SpecArg = "[files]…".into();
1544 assert_eq!(arg.name, "files");
1545 assert!(arg.var);
1546 assert!(!arg.required);
1547
1548 let arg: SpecArg = "[args...]".into();
1550 assert_eq!(arg.name, "args");
1551 assert!(arg.var);
1552 assert!(!arg.required);
1553
1554 let arg: SpecArg = "<args...>".into();
1555 assert_eq!(arg.name, "args");
1556 assert!(arg.var);
1557 assert!(arg.required);
1558
1559 let arg: SpecArg = "[args…]".into();
1561 assert_eq!(arg.name, "args");
1562 assert!(arg.var);
1563 assert!(!arg.required);
1564 }
1565
1566 #[test]
1567 fn fixed_arity_placeholders_round_trip() {
1568 let spec: Spec = "arg \"<START> <END>\"\n".parse().unwrap();
1569 let arg = &spec.cmd.args[0];
1570 assert_eq!(arg.value_names, ["START", "END"]);
1571 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1572 assert_eq!(arg.usage, "<START> <END>");
1573
1574 let reparsed: Spec = spec.to_string().parse().unwrap();
1575 assert_eq!(reparsed.cmd.args[0].value_names, ["START", "END"]);
1576 }
1577
1578 #[test]
1579 fn fixed_arity_placeholders_reject_mismatched_bounds() {
1580 let error = "arg \"<START> <END>\" var_min=1 var_max=2\n"
1581 .parse::<Spec>()
1582 .unwrap_err();
1583 assert!(
1584 format!("{error:?}").contains("require var_min=2 and var_max=2"),
1585 "{error:?}"
1586 );
1587 }
1588
1589 #[test]
1590 fn a_single_value_name_replaces_the_display_name() {
1591 let spec: Spec = "arg \"<input>\" { value_names \"INPUT\" }\n"
1592 .parse()
1593 .unwrap();
1594 let arg = &spec.cmd.args[0];
1595 assert_eq!(arg.name, "INPUT");
1596 assert_eq!(arg.usage, "<INPUT>");
1597
1598 let built = SpecArg::builder()
1599 .name("input")
1600 .required(true)
1601 .value_names(["INPUT"])
1602 .build();
1603 assert_eq!(built.name, "INPUT");
1604 assert_eq!(built.usage, "<INPUT>");
1605 }
1606
1607 #[test]
1608 fn builder_fixed_arity_survives_later_bound_setters() {
1609 let after = SpecArg::builder()
1610 .value_names(["START", "END"])
1611 .var(false)
1612 .var_min(1)
1613 .var_max(4)
1614 .build();
1615 let before = SpecArg::builder()
1616 .var(false)
1617 .var_min(1)
1618 .var_max(4)
1619 .value_names(["START", "END"])
1620 .build();
1621 for arg in [after, before] {
1622 assert!(arg.var);
1623 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1624 assert_eq!(arg.usage, "[START] [END]");
1625 }
1626 }
1627
1628 #[test]
1629 fn one_label_with_exact_bounds_renders_each_value_slot() {
1630 let spec: Spec = "arg \"<item>…\" var_min=2 var_max=2 { value_names \"ITEM\" }\n"
1631 .parse()
1632 .unwrap();
1633 assert_eq!(spec.cmd.args[0].usage, "<ITEM> <ITEM>");
1634 let reparsed: Spec = spec.to_string().parse().unwrap();
1635 assert_eq!(reparsed.cmd.args[0].value_names, ["ITEM", "ITEM"]);
1636 assert_eq!(
1637 (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
1638 (Some(2), Some(2))
1639 );
1640
1641 let built = SpecArg::builder()
1642 .value_names(["ITEM"])
1643 .required(true)
1644 .var(true)
1645 .var_min(2)
1646 .var_max(2)
1647 .build();
1648 assert_eq!(built.usage, "<ITEM> <ITEM>");
1649 }
1650
1651 #[test]
1652 fn fixed_arity_placeholders_reject_mixed_requiredness() {
1653 let error = "arg \"<START> [END]\"\n".parse::<Spec>().unwrap_err();
1654 assert!(
1655 format!("{error:?}")
1656 .contains("fixed-arity placeholders must be either all required or all optional"),
1657 "{error:?}"
1658 );
1659 }
1660
1661 #[test]
1662 fn test_arg_child_nodes() {
1663 let spec = Spec::parse(
1664 &Default::default(),
1665 r#"
1666arg "<environment>" {
1667 help "Deployment environment"
1668 choices "dev" "staging" "prod"
1669}
1670arg "[services]" {
1671 help "Services to deploy"
1672 var #true
1673 var_min 0
1674}
1675 "#,
1676 )
1677 .unwrap();
1678
1679 let env_arg = spec
1680 .cmd
1681 .args
1682 .iter()
1683 .find(|a| a.name == "environment")
1684 .unwrap();
1685 assert_eq!(env_arg.help, Some("Deployment environment".to_string()));
1686 assert!(env_arg.choices.is_some());
1687
1688 let svc_arg = spec.cmd.args.iter().find(|a| a.name == "services").unwrap();
1689 assert_eq!(svc_arg.help, Some("Services to deploy".to_string()));
1690 assert!(svc_arg.var);
1691 assert_eq!(svc_arg.var_min, Some(0));
1692 }
1693
1694 #[test]
1695 fn test_arg_long_help_child_node() {
1696 let spec = Spec::parse(
1697 &Default::default(),
1698 r#"
1699arg "<input>" {
1700 help "Input file"
1701 long_help "Extended help text for input"
1702}
1703 "#,
1704 )
1705 .unwrap();
1706
1707 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1708 assert_eq!(input_arg.help, Some("Input file".to_string()));
1709 assert_eq!(
1710 input_arg.help_long,
1711 Some("Extended help text for input".to_string())
1712 );
1713 }
1714
1715 #[test]
1716 fn positional_conflicts_round_trip_without_dropping_members() {
1717 let spec: Spec = "arg \"[VALUE]\" { conflicts \"--from-file\" \"--stdin\" }\n"
1718 .parse()
1719 .unwrap();
1720 assert_eq!(
1721 spec.cmd.args[0].conflicts,
1722 vec!["--from-file".to_string(), "--stdin".to_string()]
1723 );
1724
1725 let rendered = spec.to_string();
1726 let reparsed: Spec = rendered.parse().unwrap();
1727 assert_eq!(reparsed.cmd.args[0].conflicts, spec.cmd.args[0].conflicts);
1728 }
1729}