1use kdl::{KdlDocument, KdlEntry, KdlNode};
2use serde::Serialize;
3use std::fmt::Display;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use crate::error::UsageErr;
8use crate::spec::builder::SpecArgBuilder;
9use crate::spec::context::ParsingContext;
10use crate::spec::effect::{SpecCommandEffect, EFFECT_VALUES};
11use crate::spec::helpers::{string_entry, NodeHelper};
12use crate::spec::is_false;
13use crate::{string, SpecChoices};
14#[cfg(feature = "clap")]
15use crate::{SpecChoice, SpecChoiceAlias};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct SpecRequiredIfEq {
20 pub selector: String,
21 pub value: String,
22}
23
24#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq, strum::EnumString, strum::Display)]
25#[strum(serialize_all = "snake_case")]
26pub enum SpecDoubleDashChoices {
27 Automatic,
29 #[default]
31 Optional,
32 Required,
34 Preserve,
36}
37
38#[derive(Debug, Default, Clone, Serialize)]
55#[non_exhaustive]
56pub struct SpecArg {
57 pub name: String,
59 #[serde(skip_serializing_if = "Vec::is_empty")]
62 pub value_names: Vec<String>,
63 pub usage: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub help: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub help_long: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub help_md: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub help_first_line: Option<String>,
77 pub required: bool,
79 pub double_dash: SpecDoubleDashChoices,
81 #[serde(skip_serializing_if = "is_false")]
83 pub var: bool,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub var_min: Option<usize>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub var_max: Option<usize>,
90 #[serde(skip_serializing_if = "Option::is_none")]
97 pub delimiter: Option<char>,
98 #[serde(skip_serializing_if = "is_false")]
100 pub allow_negative_numbers: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub value_terminator: Option<String>,
104 pub hide: bool,
106 #[serde(skip_serializing_if = "is_false")]
108 pub hide_default_value: bool,
109 #[serde(skip_serializing_if = "is_false")]
111 pub hide_env: bool,
112 #[serde(skip_serializing_if = "is_false")]
114 pub hide_env_values: bool,
115 #[serde(skip_serializing_if = "is_false")]
117 pub hide_possible_values: bool,
118 #[serde(skip_serializing_if = "is_false")]
120 pub hide_short_help: bool,
121 #[serde(skip_serializing_if = "is_false")]
123 pub hide_long_help: bool,
124 #[serde(skip_serializing_if = "Vec::is_empty")]
129 pub conflicts: Vec<String>,
130 #[serde(skip_serializing_if = "Vec::is_empty")]
132 pub requires: Vec<String>,
133 #[serde(skip_serializing_if = "Vec::is_empty")]
135 pub required_if: Vec<String>,
136 #[serde(skip_serializing_if = "Vec::is_empty")]
138 pub required_if_eq: Vec<SpecRequiredIfEq>,
139 #[serde(skip_serializing_if = "Vec::is_empty")]
141 pub required_if_eq_all: Vec<SpecRequiredIfEq>,
142 #[serde(skip_serializing_if = "Vec::is_empty")]
144 pub required_unless: Vec<String>,
145 #[serde(skip_serializing_if = "Vec::is_empty")]
147 pub required_unless_all: Vec<String>,
148 #[serde(skip_serializing_if = "Vec::is_empty")]
150 pub default: Vec<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub choices: Option<SpecChoices>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub validate: Option<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub validate_error: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
163 pub effect: Option<SpecCommandEffect>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub env: Option<String>,
167 #[serde(skip_serializing_if = "Vec::is_empty")]
169 pub env_fallback: Vec<String>,
170 #[serde(skip_serializing_if = "Vec::is_empty")]
172 pub deprecated_env: Vec<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
176 pub help_heading: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub display_order: Option<usize>,
180}
181
182impl SpecArg {
183 pub fn builder() -> SpecArgBuilder {
185 SpecArgBuilder::new()
186 }
187
188 pub fn env_names(&self) -> impl Iterator<Item = &str> {
190 self.env
191 .iter()
192 .map(String::as_str)
193 .chain(self.env_fallback.iter().map(String::as_str))
194 .chain(self.deprecated_env.iter().map(String::as_str))
195 }
196
197 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
198 let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
199 for (k, v) in node.props() {
200 match k {
201 "help" => arg.help = Some(v.ensure_string()?),
202 "long_help" => arg.help_long = Some(v.ensure_string()?),
203 "help_long" => arg.help_long = Some(v.ensure_string()?),
204 "help_md" => arg.help_md = Some(v.ensure_string()?),
205 "required" => arg.required = v.ensure_bool()?,
206 "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
207 "var" => arg.var = v.ensure_bool()?,
208 "delimiter" => {
209 let raw = v.ensure_string()?;
210 let mut chars = raw.chars();
211 match (chars.next(), chars.next()) {
212 (Some(c), None) if c.is_ascii() => arg.delimiter = Some(c),
218 (Some(c), None) => bail_parse!(
219 ctx,
220 v.entry.span(),
221 "a delimiter is one byte, and {c:?} is more than one; use an \
222 ASCII separator"
223 ),
224 _ => bail_parse!(
225 ctx,
226 v.entry.span(),
227 "a delimiter is one character, and {raw:?} is not"
228 ),
229 }
230 }
231 "allow_negative_numbers" => arg.allow_negative_numbers = v.ensure_bool()?,
232 "value_terminator" => arg.value_terminator = v.ensure_string().map(Some)?,
233 "hide" => arg.hide = v.ensure_bool()?,
234 "hide_default_value" => arg.hide_default_value = v.ensure_bool()?,
235 "hide_env" => arg.hide_env = v.ensure_bool()?,
236 "hide_env_values" => arg.hide_env_values = v.ensure_bool()?,
237 "hide_possible_values" => arg.hide_possible_values = v.ensure_bool()?,
238 "hide_short_help" => arg.hide_short_help = v.ensure_bool()?,
239 "hide_long_help" => arg.hide_long_help = v.ensure_bool()?,
240 "conflicts" => arg.conflicts = vec![v.ensure_string()?],
241 "requires" => arg.requires = vec![v.ensure_string()?],
242 "required_if" => arg.required_if = vec![v.ensure_string()?],
243 "required_unless" => arg.required_unless = vec![v.ensure_string()?],
244 "required_unless_all" => arg.required_unless_all = vec![v.ensure_string()?],
245 "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
246 "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
247 "default" => arg.default = vec![v.ensure_string()?],
248 "effect" => {
249 let raw = v.ensure_string()?;
250 match raw.parse() {
251 Ok(effect) => arg.effect = Some(effect),
252 Err(_) => bail_parse!(
253 ctx,
254 v.entry.span(),
255 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
256 ),
257 }
258 }
259 "env" => arg.env = v.ensure_string().map(Some)?,
260 "env_fallback" => arg.env_fallback = vec![v.ensure_string()?],
261 "deprecated_env" => arg.deprecated_env = vec![v.ensure_string()?],
262 "validate" => arg.validate = v.ensure_string().map(Some)?,
263 "validate_error" => arg.validate_error = v.ensure_string().map(Some)?,
264 "help_heading" => arg.help_heading = v.ensure_string().map(Some)?,
265 "display_order" => arg.display_order = v.ensure_usize().map(Some)?,
266 k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
267 }
268 }
269 if !arg.default.is_empty() {
270 arg.required = false;
271 }
272 for child in node.children() {
273 match child.name() {
274 "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
275 "effect" => {
276 let a = child.arg(0)?;
277 let raw = a.ensure_string()?;
278 match raw.parse() {
279 Ok(effect) => arg.effect = Some(effect),
280 Err(_) => bail_parse!(
281 ctx,
282 a.entry.span(),
283 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
284 ),
285 }
286 }
287 "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
288 "env_fallback" => arg.env_fallback = string_args(&child)?,
289 "deprecated_env" => arg.deprecated_env = string_args(&child)?,
290 "validate" => arg.validate = child.arg(0)?.ensure_string().map(Some)?,
291 "validate_error" => {
292 arg.validate_error = child.arg(0)?.ensure_string().map(Some)?;
293 }
294 "help_heading" => {
295 arg.help_heading = child.arg(0)?.ensure_string().map(Some)?;
296 }
297 "display_order" => {
298 arg.display_order = child.arg(0)?.ensure_usize().map(Some)?;
299 }
300 "default" => {
301 let children = child.children();
305 if children.is_empty() {
306 arg.default = vec![child.arg(0)?.ensure_string()?];
308 } else {
309 arg.default = children.iter().map(|c| c.name().to_string()).collect();
312 }
313 }
314 "help" => arg.help = Some(child.arg(0)?.ensure_string()?),
315 "long_help" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
316 "help_long" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
317 "help_md" => arg.help_md = Some(child.arg(0)?.ensure_string()?),
318 "required" => arg.required = child.arg(0)?.ensure_bool()?,
319 "var" => arg.var = child.arg(0)?.ensure_bool()?,
320 "var_min" => arg.var_min = child.arg(0)?.ensure_usize().map(Some)?,
321 "var_max" => arg.var_max = child.arg(0)?.ensure_usize().map(Some)?,
322 "value_names" => {
323 arg.value_names = child
324 .ensure_arg_len(1..)?
325 .args()
326 .map(|entry| entry.ensure_string())
327 .collect::<Result<Vec<_>, _>>()?;
328 }
329 "allow_negative_numbers" => {
330 arg.allow_negative_numbers = child.arg(0)?.ensure_bool()?;
331 }
332 "value_terminator" => {
333 arg.value_terminator = child.arg(0)?.ensure_string().map(Some)?;
334 }
335 "hide" => arg.hide = child.arg(0)?.ensure_bool()?,
336 "hide_default_value" => arg.hide_default_value = child.arg(0)?.ensure_bool()?,
337 "hide_env" => arg.hide_env = child.arg(0)?.ensure_bool()?,
338 "hide_env_values" => arg.hide_env_values = child.arg(0)?.ensure_bool()?,
339 "hide_possible_values" => arg.hide_possible_values = child.arg(0)?.ensure_bool()?,
340 "hide_short_help" => arg.hide_short_help = child.arg(0)?.ensure_bool()?,
341 "hide_long_help" => arg.hide_long_help = child.arg(0)?.ensure_bool()?,
342 "conflicts" => {
343 arg.conflicts = child
344 .ensure_arg_len(1..)?
345 .args()
346 .map(|entry| entry.ensure_string())
347 .collect::<Result<Vec<_>, _>>()?;
348 }
349 "requires" => arg.requires = string_args(&child)?,
350 "required_if" => arg.required_if = string_args(&child)?,
351 "required_if_eq" => arg.required_if_eq.push(required_if_eq(&child)?),
352 "required_if_eq_all" => {
353 let len = child.args().count();
354 if len < 2 || len % 2 != 0 {
355 bail_parse!(
356 ctx,
357 child.node.name().span(),
358 "required_if_eq_all needs selector/value pairs"
359 );
360 }
361 arg.required_if_eq_all = required_if_eq_pairs(&child)?;
362 }
363 "required_unless" => arg.required_unless = string_args(&child)?,
364 "required_unless_all" => arg.required_unless_all = string_args(&child)?,
365 "double_dash" => arg.double_dash = child.arg(0)?.ensure_string()?.parse()?,
366 k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
367 }
368 }
369 if let Some(first) = arg.value_names.first() {
370 arg.name.clone_from(first);
371 }
372 if arg.value_names.len() > 1 {
373 let arity = arg.value_names.len();
374 match (arg.var_min, arg.var_max) {
375 (None, None) => {
376 arg.var_min = Some(arity);
377 arg.var_max = Some(arity);
378 }
379 (Some(min), Some(max)) if min == arity && max == arity => {}
380 _ => bail_parse!(
381 ctx,
382 node.node.name().span(),
383 "{arity} value names require var_min={arity} and var_max={arity}"
384 ),
385 }
386 arg.var = true;
387 }
388 if arg.validate_error.is_some() && arg.validate.is_none() {
389 bail_parse!(
390 ctx,
391 node.node.name().span(),
392 "validate_error requires a validate expression"
393 );
394 }
395 if arg.value_terminator.as_deref() == Some("") {
396 bail_parse!(
397 ctx,
398 node.node.name().span(),
399 "value_terminator cannot be empty"
400 );
401 }
402 if arg.value_terminator.is_some() && !arg.var {
403 bail_parse!(
404 ctx,
405 node.node.name().span(),
406 "value_terminator requires a variadic argument"
407 );
408 }
409 #[cfg(feature = "validation")]
410 if let Some(expression) = &arg.validate {
411 if let Err(error) = usage_validation::check(expression) {
412 bail_parse!(
413 ctx,
414 node.node.name().span(),
415 "invalid validation expression: {error}"
416 );
417 }
418 }
419 arg.usage = arg.usage();
420 if let Some(help) = &arg.help {
421 arg.help_first_line = Some(string::first_line(help));
422 }
423 Ok(arg)
424 }
425}
426
427impl SpecArg {
428 pub fn usage(&self) -> String {
429 let exact_arity = self.var.then_some(()).and_then(|()| {
430 self.var_min
431 .zip(self.var_max)
432 .filter(|(min, max)| min == max && *min > 1)
433 .map(|(arity, _)| arity)
434 });
435 if self.value_names.len() > 1 || exact_arity.is_some() {
436 let labels = if self.value_names.len() > 1 {
437 self.value_names.clone()
438 } else {
439 vec![
440 self.value_names
441 .first()
442 .cloned()
443 .unwrap_or_else(|| self.name.clone());
444 exact_arity.expect("branch checked")
445 ]
446 };
447 let placeholders = labels
448 .iter()
449 .map(|name| {
450 if self.required {
451 format!("<{name}>")
452 } else {
453 format!("[{name}]")
454 }
455 })
456 .collect::<Vec<_>>()
457 .join(" ");
458 return if self.double_dash == SpecDoubleDashChoices::Required {
459 format!("-- {placeholders}")
460 } else {
461 placeholders
462 };
463 }
464 let name = if self.double_dash == SpecDoubleDashChoices::Required {
465 format!("-- {}", self.name)
466 } else {
467 self.name.clone()
468 };
469 let mut name = if self.required {
470 format!("<{name}>")
471 } else {
472 format!("[{name}]")
473 };
474 if self.var {
475 name = format!("{name}…");
476 }
477 name
478 }
479}
480
481impl From<&SpecArg> for KdlNode {
482 fn from(arg: &SpecArg) -> Self {
483 let mut node = KdlNode::new("arg");
484 node.push(KdlEntry::new(arg.usage()));
485 if let Some(desc) = &arg.help {
486 node.push(string_entry(Some("help"), desc));
487 }
488 if let Some(desc) = &arg.help_long {
489 node.push(string_entry(Some("help_long"), desc));
490 }
491 if let Some(desc) = &arg.help_md {
492 node.push(string_entry(Some("help_md"), desc));
493 }
494 if !arg.required {
495 node.push(KdlEntry::new_prop("required", false));
496 }
497 if arg.double_dash == SpecDoubleDashChoices::Automatic
498 || arg.double_dash == SpecDoubleDashChoices::Preserve
499 {
500 node.push(KdlEntry::new_prop(
501 "double_dash",
502 arg.double_dash.to_string(),
503 ));
504 }
505 if arg.var {
506 node.push(KdlEntry::new_prop("var", true));
507 }
508 if let Some(min) = arg.var_min {
509 node.push(KdlEntry::new_prop("var_min", min as i128));
510 }
511 if let Some(max) = arg.var_max {
512 node.push(KdlEntry::new_prop("var_max", max as i128));
513 }
514 if let Some(delimiter) = arg.delimiter {
515 node.push(string_entry(Some("delimiter"), &delimiter.to_string()));
516 }
517 if arg.allow_negative_numbers {
518 node.push(KdlEntry::new_prop("allow_negative_numbers", true));
519 }
520 if let Some(terminator) = &arg.value_terminator {
521 node.push(string_entry(Some("value_terminator"), terminator));
522 }
523 if arg.hide {
524 node.push(KdlEntry::new_prop("hide", true));
525 }
526 for (name, hidden) in [
527 ("hide_default_value", arg.hide_default_value),
528 ("hide_env", arg.hide_env),
529 ("hide_env_values", arg.hide_env_values),
530 ("hide_possible_values", arg.hide_possible_values),
531 ("hide_short_help", arg.hide_short_help),
532 ("hide_long_help", arg.hide_long_help),
533 ] {
534 if hidden {
535 node.push(KdlEntry::new_prop(name, true));
536 }
537 }
538 if arg.conflicts.len() == 1 {
539 node.push(string_entry(Some("conflicts"), &arg.conflicts[0]));
540 } else if !arg.conflicts.is_empty() {
541 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
542 let mut conflicts = KdlNode::new("conflicts");
543 for target in &arg.conflicts {
544 conflicts.push(string_entry(None, target));
545 }
546 children.nodes_mut().push(conflicts);
547 }
548 serialize_selector_list(&mut node, "requires", &arg.requires);
549 serialize_selector_list(&mut node, "required_if", &arg.required_if);
550 serialize_required_if_eq(&mut node, "required_if_eq", &arg.required_if_eq);
551 if !arg.required_if_eq_all.is_empty() {
552 serialize_required_if_eq(&mut node, "required_if_eq_all", &arg.required_if_eq_all);
553 }
554 serialize_selector_list(&mut node, "required_unless", &arg.required_unless);
555 serialize_selector_list(&mut node, "required_unless_all", &arg.required_unless_all);
556 if !arg.default.is_empty() {
558 if arg.default.len() == 1 {
559 node.push(string_entry(Some("default"), &arg.default[0]));
561 } else {
562 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
564 let mut default_node = KdlNode::new("default");
565 let default_children = default_node
566 .children_mut()
567 .get_or_insert_with(KdlDocument::new);
568 for val in &arg.default {
569 default_children
570 .nodes_mut()
571 .push(KdlNode::new(val.as_str()));
572 }
573 children.nodes_mut().push(default_node);
574 }
575 }
576 if let Some(env) = &arg.env {
577 node.push(string_entry(Some("env"), env));
578 }
579 serialize_selector_list(&mut node, "env_fallback", &arg.env_fallback);
580 serialize_selector_list(&mut node, "deprecated_env", &arg.deprecated_env);
581 if let Some(validate) = &arg.validate {
582 node.push(string_entry(Some("validate"), validate));
583 }
584 if arg.validate.is_some() {
585 if let Some(error) = &arg.validate_error {
586 node.push(string_entry(Some("validate_error"), error));
587 }
588 }
589 if let Some(help_heading) = &arg.help_heading {
590 node.push(string_entry(Some("help_heading"), help_heading));
591 }
592 if let Some(order) = arg.display_order {
593 node.push(KdlEntry::new_prop("display_order", order as i128));
594 }
595 if let Some(effect) = &arg.effect {
596 node.push(string_entry(Some("effect"), effect.as_str()));
597 }
598 if let Some(choices) = &arg.choices {
599 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
600 children.nodes_mut().push(choices.into());
601 }
602 node
603 }
604}
605
606fn string_args(node: &NodeHelper<'_>) -> Result<Vec<String>, UsageErr> {
607 node.ensure_arg_len(1..)?
608 .args()
609 .map(|entry| entry.ensure_string())
610 .collect()
611}
612
613fn required_if_eq(node: &NodeHelper<'_>) -> Result<SpecRequiredIfEq, UsageErr> {
614 node.ensure_arg_len(2..=2)?;
615 Ok(SpecRequiredIfEq {
616 selector: node.arg(0)?.ensure_string()?,
617 value: node.arg(1)?.ensure_string()?,
618 })
619}
620
621fn required_if_eq_pairs(node: &NodeHelper<'_>) -> Result<Vec<SpecRequiredIfEq>, UsageErr> {
622 let entries = node.args().collect::<Vec<_>>();
623 entries
624 .chunks_exact(2)
625 .map(|pair| {
626 Ok(SpecRequiredIfEq {
627 selector: pair[0].ensure_string()?,
628 value: pair[1].ensure_string()?,
629 })
630 })
631 .collect()
632}
633
634fn serialize_selector_list(node: &mut KdlNode, name: &str, selectors: &[String]) {
635 if selectors.len() == 1 {
636 node.push(string_entry(Some(name), &selectors[0]));
637 } else if !selectors.is_empty() {
638 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
639 let mut relation = KdlNode::new(name);
640 for selector in selectors {
641 relation.push(string_entry(None, selector));
642 }
643 children.nodes_mut().push(relation);
644 }
645}
646
647fn serialize_required_if_eq(node: &mut KdlNode, name: &str, conditions: &[SpecRequiredIfEq]) {
648 if conditions.is_empty() {
649 return;
650 }
651 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
652 if name == "required_if_eq_all" {
653 let mut relation = KdlNode::new(name);
654 for condition in conditions {
655 relation.push(string_entry(None, &condition.selector));
656 relation.push(string_entry(None, &condition.value));
657 }
658 children.nodes_mut().push(relation);
659 } else {
660 for condition in conditions {
661 let mut relation = KdlNode::new(name);
662 relation.push(string_entry(None, &condition.selector));
663 relation.push(string_entry(None, &condition.value));
664 children.nodes_mut().push(relation);
665 }
666 }
667}
668
669impl From<&str> for SpecArg {
670 fn from(input: &str) -> Self {
671 let (input, after_double_dash) = input
672 .strip_prefix("-- ")
673 .map_or((input, false), |rest| (rest, true));
674 if let Some(placeholders) = fixed_placeholders(input) {
675 let required = placeholders
676 .iter()
677 .all(|placeholder| placeholder.starts_with('<'));
678 let value_names = placeholders
679 .iter()
680 .map(|placeholder| placeholder[1..placeholder.len() - 1].to_string())
681 .collect::<Vec<_>>();
682 return SpecArg {
683 name: value_names[0].clone(),
684 value_names,
685 required,
686 var: true,
687 var_min: Some(placeholders.len()),
688 var_max: Some(placeholders.len()),
689 double_dash: if after_double_dash {
690 SpecDoubleDashChoices::Required
691 } else {
692 SpecDoubleDashChoices::Optional
693 },
694 ..Default::default()
695 };
696 }
697 let mut arg = SpecArg {
698 name: input.to_string(),
699 required: true,
700 double_dash: if after_double_dash {
701 SpecDoubleDashChoices::Required
702 } else {
703 SpecDoubleDashChoices::Optional
704 },
705 ..Default::default()
706 };
707 if let Some(name) = arg
709 .name
710 .strip_suffix("...")
711 .or_else(|| arg.name.strip_suffix("…"))
712 {
713 arg.var = true;
714 arg.name = name.to_string();
715 }
716 let first = arg.name.chars().next().unwrap_or_default();
717 let last = arg.name.chars().last().unwrap_or_default();
718 match (first, last) {
719 ('[', ']') => {
720 arg.name = arg.name[1..arg.name.len() - 1].to_string();
721 arg.required = false;
722 }
723 ('<', '>') => {
724 arg.name = arg.name[1..arg.name.len() - 1].to_string();
725 }
726 _ => {}
727 }
728 if let Some(name) = arg.name.strip_prefix("-- ") {
732 arg.double_dash = SpecDoubleDashChoices::Required;
733 arg.name = name.to_string();
734 }
735 if !arg.var {
737 if let Some(name) = arg
738 .name
739 .strip_suffix("...")
740 .or_else(|| arg.name.strip_suffix("…"))
741 {
742 arg.var = true;
743 arg.name = name.to_string();
744 }
745 }
746 arg
747 }
748}
749impl FromStr for SpecArg {
750 type Err = UsageErr;
751 fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
752 if fixed_placeholders(input.strip_prefix("-- ").unwrap_or(input)).is_some_and(
753 |placeholders| {
754 placeholders
755 .windows(2)
756 .any(|pair| pair[0].starts_with('<') != pair[1].starts_with('<'))
757 },
758 ) {
759 let message =
760 "fixed-arity placeholders must be either all required or all optional".to_string();
761 return Err(UsageErr::InvalidInput(
762 message,
763 (0, input.len()).into(),
764 miette::NamedSource::new("argument", input.to_string()),
765 ));
766 }
767 Ok(input.into())
768 }
769}
770
771fn fixed_placeholders(input: &str) -> Option<Vec<&str>> {
774 if !input.bytes().any(|byte| byte.is_ascii_whitespace()) {
775 return None;
776 }
777 let placeholders: Vec<_> = input.split_whitespace().collect();
778 (placeholders.len() > 1
779 && placeholders.iter().all(|placeholder| {
780 matches!(
781 (placeholder.chars().next(), placeholder.chars().last()),
782 (Some('<'), Some('>')) | (Some('['), Some(']'))
783 )
784 }))
785 .then_some(placeholders)
786}
787
788#[cfg(feature = "clap")]
796pub(crate) fn default_values(arg: &clap::Arg) -> Vec<String> {
797 let raw = arg
798 .get_default_values()
799 .iter()
800 .map(|v| v.to_string_lossy().to_string());
801 match arg.get_value_delimiter() {
802 Some(delimiter) => raw
803 .flat_map(|v| {
804 v.split(delimiter)
805 .map(|part| part.to_string())
806 .collect::<Vec<_>>()
807 })
808 .collect(),
809 None => raw.collect(),
810 }
811}
812
813#[cfg(feature = "clap")]
820pub(crate) fn value_bounds(source: &clap::Arg, target: &mut SpecArg, zero_values_supported: bool) {
821 if source.get_value_delimiter().is_some() {
826 return;
827 }
828
829 let Some(range) = source.get_num_args() else {
830 if target.value_names.len() > 1 {
831 let arity = target.value_names.len();
832 target.var = true;
833 target.var_min = Some(arity);
834 target.var_max = Some(arity);
835 }
836 return;
837 };
838 let min = range.min_values();
839 let max = range.max_values();
840 if max <= 1 || min == 0 && !zero_values_supported {
841 return;
842 }
843
844 target.var = true;
845 target.var_min = Some(min);
846 target.var_max = (max != usize::MAX).then_some(max);
847}
848
849#[cfg(feature = "clap")]
855pub(crate) fn value_names_from_clap(source: &clap::Arg) -> Vec<String> {
856 let names: Vec<String> = source
857 .get_value_names()
858 .unwrap_or_default()
859 .iter()
860 .map(ToString::to_string)
861 .collect();
862 if names.len() <= 1 {
863 return names;
864 }
865 let mismatched_range = source.get_num_args().is_some_and(|range| {
866 range.min_values() != names.len() || range.max_values() != names.len()
867 });
868 if source.get_value_delimiter().is_some() || mismatched_range {
869 names.into_iter().take(1).collect()
870 } else {
871 names
872 }
873}
874
875#[cfg(feature = "clap")]
877pub(crate) fn value_hint_type(hint: clap::ValueHint) -> Option<&'static str> {
878 use clap::ValueHint;
879
880 match hint {
881 ValueHint::Unknown => None,
882 ValueHint::Other => Some("none"),
883 ValueHint::AnyPath | ValueHint::FilePath => Some("path"),
884 ValueHint::DirPath => Some("dir"),
885 ValueHint::ExecutablePath => Some("executable"),
886 ValueHint::CommandName | ValueHint::CommandString => Some("command"),
887 ValueHint::CommandWithArguments => Some("command_args"),
888 ValueHint::Username => Some("username"),
889 ValueHint::Hostname => Some("hostname"),
890 ValueHint::Url => Some("url"),
891 ValueHint::EmailAddress => Some("email"),
892 _ => None,
893 }
894}
895
896#[cfg(feature = "clap")]
897pub(crate) fn choices_from_clap(arg: &clap::Arg) -> Option<SpecChoices> {
898 let possible = arg.get_possible_values();
899 if possible.is_empty() {
900 return None;
901 }
902 let choices = possible
903 .iter()
904 .map(|value| value.get_name().to_string())
905 .collect();
906 let details = possible
907 .iter()
908 .filter_map(|value| {
909 let aliases: Vec<_> = value
910 .get_name_and_aliases()
911 .skip(1)
912 .map(|alias| SpecChoiceAlias {
913 value: alias.to_string(),
914 hide: true,
916 })
917 .collect();
918 let detail = SpecChoice {
919 value: value.get_name().to_string(),
920 help: value.get_help().map(ToString::to_string),
921 hide: value.is_hide_set(),
922 aliases,
923 };
924 (detail.help.is_some() || detail.hide || !detail.aliases.is_empty()).then_some(detail)
925 })
926 .collect();
927 Some(SpecChoices {
928 choices,
929 details,
930 ignore_case: arg.is_ignore_case_set(),
931 ..Default::default()
932 })
933}
934
935#[cfg(feature = "clap")]
936impl From<&clap::Arg> for SpecArg {
937 fn from(arg: &clap::Arg) -> Self {
938 let source = arg;
939 let required = arg.is_required_set();
940 let help = arg.get_help().map(|s| s.to_string());
941 let help_long = arg.get_long_help().map(|s| s.to_string());
942 let help_first_line = help.as_ref().map(|s| string::first_line(s));
943 let hide = arg.is_hide_set();
944 let delimiter = arg.get_value_delimiter();
948 let recorded_delimiter = delimiter.filter(char::is_ascii);
949 let value_terminator = arg.get_value_terminator().map(ToString::to_string);
950 let var = matches!(
951 arg.get_action(),
952 clap::ArgAction::Count | clap::ArgAction::Append
953 ) || delimiter.is_some();
954 let choices = choices_from_clap(arg);
955 let value_names = value_names_from_clap(arg);
956 let mut arg = Self {
957 name: value_names
958 .first()
959 .cloned()
960 .unwrap_or_else(|| source.get_id().to_string()),
961 value_names,
962 usage: "".into(),
963 required,
964 double_dash: if arg.is_last_set() {
965 SpecDoubleDashChoices::Required
966 } else if arg.is_trailing_var_arg_set() {
967 SpecDoubleDashChoices::Automatic
968 } else {
969 SpecDoubleDashChoices::Optional
970 },
971 help,
972 help_long,
973 help_md: None,
974 help_first_line,
975 var,
976 var_max: None,
977 var_min: None,
978 delimiter: recorded_delimiter,
981 allow_negative_numbers: arg.is_allow_negative_numbers_set(),
982 value_terminator: None,
983 hide,
984 hide_default_value: arg.is_hide_default_value_set(),
985 hide_env: arg.is_hide_env_set(),
986 hide_env_values: arg.is_hide_env_values_set(),
987 hide_possible_values: arg.is_hide_possible_values_set(),
988 hide_short_help: arg.is_hide_short_help_set(),
989 hide_long_help: arg.is_hide_long_help_set(),
990 conflicts: Vec::new(),
991 requires: Vec::new(),
992 required_if: Vec::new(),
993 required_if_eq: Vec::new(),
994 required_if_eq_all: Vec::new(),
995 required_unless: Vec::new(),
996 required_unless_all: Vec::new(),
997 default: default_values(arg),
998 choices: None,
999 validate: None,
1000 validate_error: None,
1001 effect: None,
1002 env: None,
1003 env_fallback: Vec::new(),
1004 deprecated_env: Vec::new(),
1005 help_heading: arg.get_help_heading().map(|s| s.to_string()),
1006 display_order: Some(arg.get_display_order()),
1007 };
1008 arg.choices = choices;
1009
1010 value_bounds(source, &mut arg, true);
1011 if arg.var {
1012 arg.value_terminator = value_terminator;
1013 }
1014
1015 arg
1016 }
1017}
1018
1019impl Display for SpecArg {
1020 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1021 write!(f, "{}", self.usage())
1022 }
1023}
1024impl PartialEq for SpecArg {
1025 fn eq(&self, other: &Self) -> bool {
1026 self.name == other.name
1027 }
1028}
1029impl Eq for SpecArg {}
1030impl Hash for SpecArg {
1031 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1032 self.name.hash(state);
1033 }
1034}
1035
1036#[cfg(all(test, feature = "validation"))]
1037mod validation_tests {
1038 use std::collections::HashMap;
1039
1040 use crate::{parse, parse::Parser, Spec};
1041
1042 fn spec() -> Spec {
1043 r#"
1044name "ex"
1045bin "ex"
1046arg "<port>" validate="int(value) >= 1 && int(value) <= 65535" validate_error="must be a valid port"
1047 "#
1048 .parse()
1049 .unwrap()
1050 }
1051
1052 #[test]
1053 fn validation_round_trips_through_kdl() {
1054 let spec = spec();
1055 let kdl = spec.to_string();
1056 let reparsed: Spec = kdl.parse().unwrap();
1057 let arg = &reparsed.cmd.args[0];
1058 assert_eq!(
1059 arg.validate.as_deref(),
1060 Some("int(value) >= 1 && int(value) <= 65535")
1061 );
1062 assert_eq!(arg.validate_error.as_deref(), Some("must be a valid port"));
1063 }
1064
1065 #[test]
1066 fn invalid_validation_declarations_are_rejected_with_the_spec() {
1067 let missing_expression = r#"name "demo"
1068bin "demo"
1069arg "<port>" validate_error="must be a port"
1070"#;
1071 assert!(missing_expression.parse::<Spec>().is_err());
1072
1073 let invalid_expression = r#"name "demo"
1074bin "demo"
1075arg "<port>" validate="int(value) >"
1076"#;
1077 assert!(invalid_expression.parse::<Spec>().is_err());
1078 }
1079
1080 #[test]
1081 fn reference_parser_validates_each_raw_value() {
1082 parse(&spec(), &["ex".to_string(), "9229".to_string()]).unwrap();
1083
1084 let error = parse(&spec(), &["ex".to_string(), "0".to_string()]).unwrap_err();
1085 assert!(
1086 error.to_string().contains("must be a valid port"),
1087 "{error:?}"
1088 );
1089
1090 let variadic: Spec = r#"
1091name "ex"
1092bin "ex"
1093arg "<port>" var=#true validate="int(value) > 0" validate_error="port must be positive"
1094 "#
1095 .parse()
1096 .unwrap();
1097 let error = parse(
1098 &variadic,
1099 &["ex".to_string(), "0".to_string(), "-1".to_string()],
1100 )
1101 .unwrap_err()
1102 .to_string();
1103 assert_eq!(error.matches("port must be positive").count(), 1, "{error}");
1104 }
1105
1106 #[test]
1107 fn reference_parser_validates_environment_and_default_fallbacks() {
1108 let spec: Spec = r#"
1109name "ex"
1110bin "ex"
1111arg "[port]" env="PORT" validate="int(value) > 0" validate_error="port must be positive"
1112flag "--mode" default="bad" {
1113 arg "<mode>" validate="value == 'good'" validate_error="mode must be good"
1114}
1115arg "[ports]..." env="PORTS" var=#true var_max=1 delimiter="," validate="int(value) > 0" validate_error="all ports must be positive"
1116flag "--levels" env="LEVELS" {
1117 arg "<level>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all levels must be good"
1118}
1119flag "--modes" default="good,bad" {
1120 arg "<mode>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all modes must be good"
1121}
1122flag "--conditional" {
1123 default_if "--trigger" "good,bad"
1124 arg "<conditional>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all conditional values must be good"
1125}
1126flag "--repeats <repeat>" env="REPEATS" var=#true var_max=1 delimiter=","
1127flag "--trigger"
1128 "#
1129 .parse()
1130 .unwrap();
1131 let env = HashMap::from([
1132 ("PORT".to_string(), "0".to_string()),
1133 ("PORTS".to_string(), "1,0".to_string()),
1134 ("LEVELS".to_string(), "good,bad".to_string()),
1135 ("REPEATS".to_string(), "one,two".to_string()),
1136 ]);
1137 let error = Parser::new(&spec)
1138 .with_env(env)
1139 .parse(&["ex".to_string(), "--trigger".to_string()])
1140 .unwrap_err();
1141 let error = error.to_string();
1142 assert!(error.contains("port must be positive"), "{error}");
1143 assert!(error.contains("mode must be good"), "{error}");
1144 assert!(error.contains("all ports must be positive"), "{error}");
1145 assert!(error.contains("all levels must be good"), "{error}");
1146 assert!(error.contains("all modes must be good"), "{error}");
1147 assert!(
1148 error.contains("all conditional values must be good"),
1149 "{error}"
1150 );
1151 assert!(
1152 error.contains("Variadic argument <ports> accepts at most 1 value(s), got 2"),
1153 "{error}"
1154 );
1155 for flag in ["levels", "modes", "conditional", "repeats"] {
1156 assert!(
1157 error.contains(&format!(
1158 "Variadic flag --{flag} accepts at most 1 value(s), got 2"
1159 )),
1160 "{error}"
1161 );
1162 }
1163 }
1164}
1165
1166#[cfg(test)]
1167mod delimiter_tests {
1168 use crate::Spec;
1169
1170 #[test]
1171 fn a_delimiter_has_to_be_one_byte() {
1172 for spec in [
1177 "flag \"--tags <tag>\" var=#true delimiter=\"§\"\n",
1178 "arg \"[tags]...\" var=#true delimiter=\"、\"\n",
1179 ] {
1180 let err = spec.parse::<Spec>().unwrap_err();
1181 assert!(format!("{err:?}").contains("one byte"), "{err:?}");
1182 }
1183
1184 let cmd = clap::Command::new("ex").arg(
1188 clap::Arg::new("tags")
1189 .long("tags")
1190 .value_delimiter('、')
1191 .action(clap::ArgAction::Set),
1192 );
1193 let spec = Spec::from(&cmd);
1194 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1195 assert_eq!(
1196 arg.delimiter, None,
1197 "a separator it cannot write is not recorded"
1198 );
1199 assert!(arg.var, "clap still splits, so the values still arrive");
1200 spec.to_string()
1201 .parse::<Spec>()
1202 .expect("what the bridge produces has to parse back");
1203 }
1204
1205 #[test]
1206 fn a_delimiter_round_trips_and_comes_across_from_clap() {
1207 let spec: Spec = "flag \"--tags <tag>\" var=#true delimiter=\",\"\n"
1208 .parse()
1209 .unwrap();
1210 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1211 assert_eq!(arg.delimiter, Some(','));
1212
1213 let reparsed: Spec = spec.to_string().parse().unwrap();
1214 let arg = reparsed.cmd.flags[0].arg.as_ref().unwrap();
1215 assert_eq!(arg.delimiter, Some(','), "{spec}");
1216
1217 let cmd = clap::Command::new("ex").arg(
1220 clap::Arg::new("tags")
1221 .long("tags")
1222 .value_delimiter(',')
1223 .num_args(1..)
1224 .default_value("a,b"),
1225 );
1226 let spec = Spec::from(&cmd);
1227 let flag = &spec.cmd.flags[0];
1228 assert_eq!(flag.arg.as_ref().unwrap().delimiter, Some(','));
1229 assert_eq!(flag.default, vec!["a", "b"]);
1232 }
1233
1234 #[test]
1235 fn a_single_valued_clap_arg_keeps_its_delimiter() {
1236 let cmd = clap::Command::new("ex").arg(
1241 clap::Arg::new("tags")
1242 .long("tags")
1243 .action(clap::ArgAction::Set)
1244 .value_delimiter(','),
1245 );
1246 let spec = Spec::from(&cmd);
1247 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1248 assert_eq!(arg.delimiter, Some(','));
1249 assert!(arg.var, "a delimiter brings `var` with it");
1252 let _: Spec = spec.to_string().parse().expect("{spec}");
1253 }
1254
1255 #[test]
1256 fn a_single_valued_clap_positional_splits_into_stored_values() {
1257 let cmd = clap::Command::new("ex").arg(
1261 clap::Arg::new("tags")
1262 .action(clap::ArgAction::Set)
1263 .value_delimiter(',')
1264 .value_parser(["a", "b"]),
1265 );
1266 let spec = Spec::from(&cmd);
1267 let arg = &spec.cmd.args[0];
1268 assert!(arg.var, "a positional delimiter brings `var` with it");
1269 assert_eq!(arg.delimiter, Some(','));
1270
1271 let input = ["ex", "a,b"].map(str::to_string);
1272 let parsed = crate::parse(&spec, &input).expect("both split values are choices");
1273 let value = parsed
1274 .args
1275 .values()
1276 .next()
1277 .expect("the positional was stored");
1278 assert!(matches!(
1279 value,
1280 crate::parse::ParseValue::MultiString(values)
1281 if values == &["a".to_string(), "b".to_string()]
1282 ));
1283 }
1284
1285 #[test]
1286 fn a_delimiter_needs_somewhere_to_put_what_it_splits() {
1287 let err = "flag \"--tags <tag>\" delimiter=\",\"\n"
1289 .parse::<Spec>()
1290 .unwrap_err();
1291 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1292
1293 let err = "arg \"[tags]\" delimiter=\",\"\n"
1294 .parse::<Spec>()
1295 .unwrap_err();
1296 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1297
1298 let err = "flag \"--quiet\" delimiter=\",\"\n"
1300 .parse::<Spec>()
1301 .unwrap_err();
1302 assert!(format!("{err:?}").contains("takes none"), "{err:?}");
1303
1304 let err = "flag \"--tags <tag>\" var=#true delimiter=\"::\"\n"
1306 .parse::<Spec>()
1307 .unwrap_err();
1308 assert!(format!("{err:?}").contains("one character"), "{err:?}");
1309 }
1310}
1311
1312#[cfg(test)]
1313mod possible_value_tests {
1314 use clap::builder::PossibleValue;
1315
1316 #[test]
1317 fn clap_possible_value_metadata_survives_the_bridge() {
1318 let command = clap::Command::new("ex").arg(
1319 clap::Arg::new("color").ignore_case(true).value_parser([
1320 PossibleValue::new("always")
1321 .help("Always use color")
1322 .alias("yes"),
1323 PossibleValue::new("never").hide(true),
1324 ]),
1325 );
1326 let spec = crate::Spec::from(&command);
1327 let choices = spec.cmd.args[0].choices.as_ref().unwrap();
1328 assert_eq!(choices.choices, ["always", "never"]);
1329 assert!(choices.ignore_case);
1330 assert!(choices.matches("YES"));
1331 assert_eq!(choices.values(), ["always"]);
1332 assert_eq!(choices.details[0].help.as_deref(), Some("Always use color"));
1333 assert!(choices.details[0].aliases[0].hide);
1334 assert!(choices.details[1].hide);
1335 }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340 use crate::{Spec, SpecArg};
1341 use insta::assert_snapshot;
1342
1343 #[test]
1344 fn test_arg_with_env() {
1345 let spec = Spec::parse(
1346 &Default::default(),
1347 r#"
1348arg "<input>" env="MY_INPUT" help="Input file"
1349arg "<output>" env="MY_OUTPUT"
1350 "#,
1351 )
1352 .unwrap();
1353
1354 assert_snapshot!(spec, @r#"
1355 arg <input> help="Input file" env=MY_INPUT
1356 arg <output> env=MY_OUTPUT
1357 "#);
1358
1359 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1360 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1361
1362 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1363 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1364 }
1365
1366 #[test]
1367 fn test_arg_with_env_child_node() {
1368 let spec = Spec::parse(
1369 &Default::default(),
1370 r#"
1371arg "<input>" help="Input file" {
1372 env "MY_INPUT"
1373}
1374arg "<output>" {
1375 env "MY_OUTPUT"
1376}
1377 "#,
1378 )
1379 .unwrap();
1380
1381 assert_snapshot!(spec, @r#"
1382 arg <input> help="Input file" env=MY_INPUT
1383 arg <output> env=MY_OUTPUT
1384 "#);
1385
1386 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1387 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1388
1389 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1390 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1391 }
1392
1393 #[test]
1394 fn test_arg_variadic_syntax() {
1395 use crate::SpecArg;
1396
1397 let arg: SpecArg = "<files>...".into();
1399 assert_eq!(arg.name, "files");
1400 assert!(arg.var);
1401 assert!(arg.required);
1402
1403 let arg: SpecArg = "[files]...".into();
1405 assert_eq!(arg.name, "files");
1406 assert!(arg.var);
1407 assert!(!arg.required);
1408
1409 let arg: SpecArg = "<files>…".into();
1411 assert_eq!(arg.name, "files");
1412 assert!(arg.var);
1413
1414 let arg: SpecArg = "[files]…".into();
1415 assert_eq!(arg.name, "files");
1416 assert!(arg.var);
1417 assert!(!arg.required);
1418
1419 let arg: SpecArg = "[args...]".into();
1421 assert_eq!(arg.name, "args");
1422 assert!(arg.var);
1423 assert!(!arg.required);
1424
1425 let arg: SpecArg = "<args...>".into();
1426 assert_eq!(arg.name, "args");
1427 assert!(arg.var);
1428 assert!(arg.required);
1429
1430 let arg: SpecArg = "[args…]".into();
1432 assert_eq!(arg.name, "args");
1433 assert!(arg.var);
1434 assert!(!arg.required);
1435 }
1436
1437 #[test]
1438 fn fixed_arity_placeholders_round_trip() {
1439 let spec: Spec = "arg \"<START> <END>\"\n".parse().unwrap();
1440 let arg = &spec.cmd.args[0];
1441 assert_eq!(arg.value_names, ["START", "END"]);
1442 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1443 assert_eq!(arg.usage, "<START> <END>");
1444
1445 let reparsed: Spec = spec.to_string().parse().unwrap();
1446 assert_eq!(reparsed.cmd.args[0].value_names, ["START", "END"]);
1447 }
1448
1449 #[test]
1450 fn fixed_arity_placeholders_reject_mismatched_bounds() {
1451 let error = "arg \"<START> <END>\" var_min=1 var_max=2\n"
1452 .parse::<Spec>()
1453 .unwrap_err();
1454 assert!(
1455 format!("{error:?}").contains("require var_min=2 and var_max=2"),
1456 "{error:?}"
1457 );
1458 }
1459
1460 #[test]
1461 fn a_single_value_name_replaces_the_display_name() {
1462 let spec: Spec = "arg \"<input>\" { value_names \"INPUT\" }\n"
1463 .parse()
1464 .unwrap();
1465 let arg = &spec.cmd.args[0];
1466 assert_eq!(arg.name, "INPUT");
1467 assert_eq!(arg.usage, "<INPUT>");
1468
1469 let built = SpecArg::builder()
1470 .name("input")
1471 .required(true)
1472 .value_names(["INPUT"])
1473 .build();
1474 assert_eq!(built.name, "INPUT");
1475 assert_eq!(built.usage, "<INPUT>");
1476 }
1477
1478 #[test]
1479 fn builder_fixed_arity_survives_later_bound_setters() {
1480 let after = SpecArg::builder()
1481 .value_names(["START", "END"])
1482 .var(false)
1483 .var_min(1)
1484 .var_max(4)
1485 .build();
1486 let before = SpecArg::builder()
1487 .var(false)
1488 .var_min(1)
1489 .var_max(4)
1490 .value_names(["START", "END"])
1491 .build();
1492 for arg in [after, before] {
1493 assert!(arg.var);
1494 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1495 assert_eq!(arg.usage, "[START] [END]");
1496 }
1497 }
1498
1499 #[test]
1500 fn one_label_with_exact_bounds_renders_each_value_slot() {
1501 let spec: Spec = "arg \"<item>…\" var_min=2 var_max=2 { value_names \"ITEM\" }\n"
1502 .parse()
1503 .unwrap();
1504 assert_eq!(spec.cmd.args[0].usage, "<ITEM> <ITEM>");
1505 let reparsed: Spec = spec.to_string().parse().unwrap();
1506 assert_eq!(reparsed.cmd.args[0].value_names, ["ITEM", "ITEM"]);
1507 assert_eq!(
1508 (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
1509 (Some(2), Some(2))
1510 );
1511
1512 let built = SpecArg::builder()
1513 .value_names(["ITEM"])
1514 .required(true)
1515 .var(true)
1516 .var_min(2)
1517 .var_max(2)
1518 .build();
1519 assert_eq!(built.usage, "<ITEM> <ITEM>");
1520 }
1521
1522 #[test]
1523 fn fixed_arity_placeholders_reject_mixed_requiredness() {
1524 let error = "arg \"<START> [END]\"\n".parse::<Spec>().unwrap_err();
1525 assert!(
1526 format!("{error:?}")
1527 .contains("fixed-arity placeholders must be either all required or all optional"),
1528 "{error:?}"
1529 );
1530 }
1531
1532 #[test]
1533 fn test_arg_child_nodes() {
1534 let spec = Spec::parse(
1535 &Default::default(),
1536 r#"
1537arg "<environment>" {
1538 help "Deployment environment"
1539 choices "dev" "staging" "prod"
1540}
1541arg "[services]" {
1542 help "Services to deploy"
1543 var #true
1544 var_min 0
1545}
1546 "#,
1547 )
1548 .unwrap();
1549
1550 let env_arg = spec
1551 .cmd
1552 .args
1553 .iter()
1554 .find(|a| a.name == "environment")
1555 .unwrap();
1556 assert_eq!(env_arg.help, Some("Deployment environment".to_string()));
1557 assert!(env_arg.choices.is_some());
1558
1559 let svc_arg = spec.cmd.args.iter().find(|a| a.name == "services").unwrap();
1560 assert_eq!(svc_arg.help, Some("Services to deploy".to_string()));
1561 assert!(svc_arg.var);
1562 assert_eq!(svc_arg.var_min, Some(0));
1563 }
1564
1565 #[test]
1566 fn test_arg_long_help_child_node() {
1567 let spec = Spec::parse(
1568 &Default::default(),
1569 r#"
1570arg "<input>" {
1571 help "Input file"
1572 long_help "Extended help text for input"
1573}
1574 "#,
1575 )
1576 .unwrap();
1577
1578 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1579 assert_eq!(input_arg.help, Some("Input file".to_string()));
1580 assert_eq!(
1581 input_arg.help_long,
1582 Some("Extended help text for input".to_string())
1583 );
1584 }
1585
1586 #[test]
1587 fn positional_conflicts_round_trip_without_dropping_members() {
1588 let spec: Spec = "arg \"[VALUE]\" { conflicts \"--from-file\" \"--stdin\" }\n"
1589 .parse()
1590 .unwrap();
1591 assert_eq!(
1592 spec.cmd.args[0].conflicts,
1593 vec!["--from-file".to_string(), "--stdin".to_string()]
1594 );
1595
1596 let rendered = spec.to_string();
1597 let reparsed: Spec = rendered.parse().unwrap();
1598 assert_eq!(reparsed.cmd.args[0].conflicts, spec.cmd.args[0].conflicts);
1599 }
1600}