1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use crate::error::UsageErr;
5use crate::kdl::{KdlDocument, KdlEntry, KdlNode};
6use crate::sh::sh;
7use crate::spec::builder::SpecCommandBuilder;
8use crate::spec::clause::SpecClause;
9use crate::spec::context::ParsingContext;
10use crate::spec::effect::{EFFECT_VALUES, SpecCommandEffect};
11use crate::spec::exit_code::SpecExitCode;
12use crate::spec::flagset::SpecUse;
13use crate::spec::group::SpecGroup;
14use crate::spec::helpers::{NodeHelper, string_entry};
15use crate::spec::is_false;
16use crate::spec::mount::SpecMount;
17use crate::spec::output::SpecOutput;
18use crate::spec::unknown_flags::UnknownFlags;
19use crate::{Spec, SpecArg, SpecComplete, SpecFlag};
20use indexmap::IndexMap;
21use itertools::Itertools;
22use serde::Serialize;
23
24#[derive(Debug, Serialize, Clone)]
43pub struct SpecCommand {
44 pub full_cmd: Vec<String>,
46 pub usage: String,
48 pub subcommands: IndexMap<String, SpecCommand>,
50 pub args: Vec<SpecArg>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub clause: Option<SpecClause>,
55 pub flags: Vec<SpecFlag>,
57 #[serde(skip)]
64 pub(crate) uses: Vec<SpecUse>,
65 pub mounts: Vec<SpecMount>,
67 #[serde(skip_serializing_if = "Vec::is_empty")]
72 pub groups: Vec<SpecGroup>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub deprecated: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub deprecated_warn_at: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub deprecated_remove_at: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
85 pub effect: Option<SpecCommandEffect>,
86 pub unknown_flags: Option<UnknownFlags>,
94 pub hide: bool,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub help_heading: Option<String>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub surface: Option<String>,
102 #[serde(skip_serializing_if = "Vec::is_empty")]
104 pub available_if: Vec<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub display_order: Option<usize>,
108 #[serde(skip)]
118 pub mounted: bool,
119 #[serde(skip)]
126 pub flags_from_mount: bool,
127 #[serde(skip_serializing_if = "is_false")]
129 pub subcommand_required: bool,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub subcommand_help_heading: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub subcommand_value_name: Option<String>,
136 #[serde(skip_serializing_if = "is_false")]
138 pub next_line_help: bool,
139 #[serde(skip_serializing_if = "is_false")]
141 pub flatten_help: bool,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub term_width: Option<usize>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub max_term_width: Option<usize>,
148 #[serde(skip_serializing_if = "is_false")]
155 pub external_subcommand: bool,
156 #[serde(skip_serializing_if = "is_false")]
158 pub arg_required_else_help: bool,
159 #[serde(skip_serializing_if = "is_false")]
160 pub disable_help_flag: bool,
161 #[serde(skip_serializing_if = "is_false")]
162 pub disable_help_subcommand: bool,
163 #[serde(skip_serializing_if = "is_false")]
164 pub disable_version_flag: bool,
165 #[serde(skip_serializing_if = "is_false")]
167 pub dont_delimit_trailing_values: bool,
168 pub args_override_self: bool,
171 #[serde(skip_serializing_if = "is_false")]
173 pub subcommand_negates_reqs: bool,
174 #[serde(skip_serializing_if = "is_false")]
176 pub args_conflicts_with_subcommands: bool,
177 #[serde(skip_serializing_if = "is_false")]
178 pub subcommand_precedence_over_arg: bool,
179 #[serde(skip_serializing_if = "is_false")]
181 pub allow_missing_positional: bool,
182 #[serde(skip_serializing_if = "Option::is_none")]
185 pub restart_token: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub help: Option<String>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub help_long: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub help_md: Option<String>,
195 pub name: String,
197 pub aliases: Vec<String>,
199 pub hidden_aliases: Vec<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub before_help: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub before_help_long: Option<String>,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub before_help_md: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub after_help: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub after_help_long: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub after_help_md: Option<String>,
219 pub examples: Vec<SpecExample>,
221 pub headings: Vec<SpecHeading>,
223 #[serde(skip_serializing_if = "Vec::is_empty")]
228 pub outputs: Vec<SpecOutput>,
229 #[serde(skip_serializing_if = "Option::is_none")]
234 pub select: Option<String>,
235 #[serde(skip_serializing_if = "Vec::is_empty")]
237 pub exit_codes: Vec<SpecExitCode>,
238 #[serde(skip_serializing_if = "IndexMap::is_empty")]
240 pub complete: IndexMap<String, SpecComplete>,
241
242 #[serde(skip)]
247 pub(crate) subcommand_lookup: OnceLock<HashMap<String, String>>,
248}
249
250impl Default for SpecCommand {
251 fn default() -> Self {
252 Self {
253 full_cmd: vec![],
254 usage: "".to_string(),
255 subcommands: IndexMap::new(),
256 args: vec![],
257 flags: vec![],
258 uses: vec![],
259 mounts: vec![],
260 groups: vec![],
261 deprecated: None,
262 deprecated_warn_at: None,
263 deprecated_remove_at: None,
264 effect: None,
265 unknown_flags: None,
266 hide: false,
267 help_heading: None,
268 surface: None,
269 available_if: vec![],
270 display_order: None,
271 mounted: false,
272 flags_from_mount: false,
273 subcommand_required: false,
274 subcommand_help_heading: None,
275 subcommand_value_name: None,
276 next_line_help: false,
277 flatten_help: false,
278 term_width: None,
279 max_term_width: None,
280 external_subcommand: false,
281 arg_required_else_help: false,
282 disable_help_flag: false,
283 disable_help_subcommand: false,
284 disable_version_flag: false,
285 dont_delimit_trailing_values: false,
286 args_override_self: true,
287 subcommand_negates_reqs: false,
288 args_conflicts_with_subcommands: false,
289 subcommand_precedence_over_arg: false,
290 allow_missing_positional: false,
291 restart_token: None,
292 clause: None,
293 help: None,
294 help_long: None,
295 help_md: None,
296 name: "".to_string(),
297 aliases: vec![],
298 hidden_aliases: vec![],
299 before_help: None,
300 before_help_long: None,
301 before_help_md: None,
302 after_help: None,
303 after_help_long: None,
304 after_help_md: None,
305 examples: vec![],
306 headings: vec![],
307 outputs: vec![],
308 select: None,
309 exit_codes: vec![],
310 subcommand_lookup: OnceLock::new(),
311 complete: IndexMap::new(),
312 }
313 }
314}
315
316#[derive(Debug, Default, Serialize, Clone)]
317#[non_exhaustive]
318pub struct SpecExample {
319 pub code: String,
320 pub header: Option<String>,
321 pub help: Option<String>,
322 pub lang: String,
323}
324
325impl SpecExample {
326 pub fn new(code: impl Into<String>) -> Self {
328 Self {
329 code: code.into(),
330 ..Default::default()
331 }
332 }
333
334 pub fn header(mut self, header: impl Into<String>) -> Self {
336 self.header = Some(header.into());
337 self
338 }
339
340 pub fn help(mut self, help: impl Into<String>) -> Self {
342 self.help = Some(help.into());
343 self
344 }
345
346 pub fn lang(mut self, lang: impl Into<String>) -> Self {
348 self.lang = lang.into();
349 self
350 }
351}
352
353#[derive(Debug, Default, Serialize, Clone)]
358#[non_exhaustive]
359pub struct SpecHeading {
360 pub title: String,
361 pub help: String,
362}
363
364impl SpecHeading {
365 pub fn new(title: impl Into<String>, help: impl Into<String>) -> Self {
367 Self {
368 title: title.into(),
369 help: help.into(),
370 }
371 }
372}
373
374impl From<&SpecHeading> for KdlNode {
375 fn from(heading: &SpecHeading) -> KdlNode {
376 let mut node = KdlNode::new("heading");
377 node.push(string_entry(None, &heading.title));
378 node.push(string_entry(Some("help"), &heading.help));
379 node
380 }
381}
382
383impl From<&SpecExample> for KdlNode {
384 fn from(example: &SpecExample) -> KdlNode {
385 let mut node = KdlNode::new("example");
386 node.push(string_entry(None, &example.code));
387 if let Some(header) = &example.header {
388 node.push(string_entry(Some("header"), header));
389 }
390 if let Some(help) = &example.help {
391 node.push(string_entry(Some("help"), help));
392 }
393 if !example.lang.is_empty() {
394 node.push(string_entry(Some("lang"), &example.lang));
395 }
396 node
397 }
398}
399
400impl SpecCommand {
401 pub fn builder() -> SpecCommandBuilder {
403 SpecCommandBuilder::new()
404 }
405
406 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
407 node.ensure_arg_len(1..=1)?;
408 let mut cmd = Self {
409 name: node.arg(0)?.ensure_string()?.to_string(),
410 ..Default::default()
411 };
412 for (k, v) in node.props() {
413 match k {
414 "help" => cmd.help = Some(v.ensure_string()?),
415 "long_help" => cmd.help_long = Some(v.ensure_string()?),
416 "help_long" => cmd.help_long = Some(v.ensure_string()?),
417 "help_md" => cmd.help_md = Some(v.ensure_string()?),
418 "before_help" => cmd.before_help = Some(v.ensure_string()?),
419 "before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
420 "before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
421 "before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
422 "after_help" => cmd.after_help = Some(v.ensure_string()?),
423 "after_long_help" => {
424 cmd.after_help_long = Some(v.ensure_string()?);
425 }
426 "after_help_long" => {
427 cmd.after_help_long = Some(v.ensure_string()?);
428 }
429 "after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
430 "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
431 "subcommand_help_heading" => cmd.subcommand_help_heading = Some(v.ensure_string()?),
432 "subcommand_value_name" => cmd.subcommand_value_name = Some(v.ensure_string()?),
433 "next_line_help" => cmd.next_line_help = v.ensure_bool()?,
434 "flatten_help" => cmd.flatten_help = v.ensure_bool()?,
435 "term_width" => cmd.term_width = Some(v.ensure_usize()?),
436 "max_term_width" => cmd.max_term_width = Some(v.ensure_usize()?),
437 "external_subcommand" => cmd.external_subcommand = v.ensure_bool()?,
438 "arg_required_else_help" => cmd.arg_required_else_help = v.ensure_bool()?,
439 "disable_help_flag" => cmd.disable_help_flag = v.ensure_bool()?,
440 "disable_help_subcommand" => cmd.disable_help_subcommand = v.ensure_bool()?,
441 "disable_version_flag" => cmd.disable_version_flag = v.ensure_bool()?,
442 "dont_delimit_trailing_values" => {
443 cmd.dont_delimit_trailing_values = v.ensure_bool()?
444 }
445 "args_override_self" => cmd.args_override_self = v.ensure_bool()?,
446 "subcommand_negates_reqs" => cmd.subcommand_negates_reqs = v.ensure_bool()?,
447 "args_conflicts_with_subcommands" => {
448 cmd.args_conflicts_with_subcommands = v.ensure_bool()?
449 }
450 "subcommand_precedence_over_arg" => {
451 cmd.subcommand_precedence_over_arg = v.ensure_bool()?
452 }
453 "allow_missing_positional" => cmd.allow_missing_positional = v.ensure_bool()?,
454 "hide" => cmd.hide = v.ensure_bool()?,
455 "help_heading" => cmd.help_heading = Some(v.ensure_string()?),
456 "surface" => cmd.surface = Some(v.ensure_string()?),
457 "available_if" => cmd.available_if = vec![v.ensure_string()?],
458 "display_order" => cmd.display_order = Some(v.ensure_usize()?),
459 "unknown_flags" => {
460 let raw = v.ensure_string()?;
461 match raw.parse() {
462 Ok(mode) => cmd.unknown_flags = Some(mode),
463 Err(_) => bail_parse!(
464 ctx,
465 v.entry.span(),
466 "unsupported unknown_flags {raw}, expected one of: {}",
467 crate::spec::unknown_flags::UNKNOWN_FLAGS_VALUES
468 ),
469 }
470 }
471 "effect" => {
472 let raw = v.ensure_string()?;
473 match raw.parse() {
474 Ok(effect) => cmd.effect = Some(effect),
475 Err(_) => bail_parse!(
476 ctx,
477 v.entry.span(),
478 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
479 ),
480 }
481 }
482 "restart_token" => cmd.restart_token = Some(v.ensure_string()?),
483 "deprecated" => {
484 cmd.deprecated = match v.value.as_bool() {
485 Some(true) => Some("deprecated".to_string()),
486 Some(false) => None,
487 None => Some(v.ensure_string()?),
488 }
489 }
490 "deprecated_warn_at" => cmd.deprecated_warn_at = Some(v.ensure_string()?),
491 "deprecated_remove_at" => cmd.deprecated_remove_at = Some(v.ensure_string()?),
492 k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
493 }
494 }
495 for child in node.children() {
496 match child.name() {
497 "flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
498 "use" => {
499 let at = cmd.flags.len();
500 cmd.uses.push(SpecUse::parse(ctx, &child, at)?);
501 }
502 "arg" => {
503 let arg = SpecArg::parse(ctx, &child)?;
504 if arg.delimiter.is_some() && !arg.var {
507 bail_parse!(
508 ctx,
509 child.node.name().span(),
510 "argument <{}> has a delimiter and holds one value; add \
511 `var=#true` for the values it splits into",
512 arg.name
513 );
514 }
515 cmd.args.push(arg);
516 }
517 "clause" => {
518 if cmd.clause.is_some() {
519 bail_parse!(
520 ctx,
521 child.node.name().span(),
522 "a command may declare at most one clause"
523 );
524 }
525 cmd.clause = Some(SpecClause::parse(ctx, &child)?);
526 }
527 "mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
528 "group" => cmd.groups.push(SpecGroup::parse(ctx, &child)?),
529 "cmd" => {
530 let node = SpecCommand::parse(ctx, &child)?;
531 cmd.subcommands.insert(node.name.to_string(), node);
532 }
533 "alias" => {
534 let alias = child
535 .ensure_arg_len(1..)?
536 .args()
537 .map(|e| e.ensure_string())
538 .collect::<Result<Vec<_>, _>>()?;
539 let hide = child
540 .get("hide")
541 .map(|n| n.ensure_bool())
542 .unwrap_or(Ok(false))?;
543 if hide {
544 cmd.hidden_aliases.extend(alias);
545 } else {
546 cmd.aliases.extend(alias);
547 }
548 }
549 "example" => {
550 let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
551 let mut example = SpecExample::new(code.trim().to_string());
552 for (k, v) in child.props() {
553 match k {
554 "header" => example.header = Some(v.ensure_string()?),
555 "help" => example.help = Some(v.ensure_string()?),
556 "lang" => example.lang = v.ensure_string()?,
557 k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
558 }
559 }
560 cmd.examples.push(example);
561 }
562 "heading" => {
563 let title = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
564 let mut help = None;
565 for (k, v) in child.props() {
566 match k {
567 "help" => help = Some(v.ensure_string()?),
568 k => bail_parse!(ctx, v.entry.span(), "unsupported heading key {k}"),
569 }
570 }
571 let Some(help) = help else {
572 bail_parse!(ctx, child.node.span(), "heading {title} needs help text");
573 };
574 cmd.headings.push(SpecHeading::new(title, help));
575 }
576 "output" => cmd.outputs.push(SpecOutput::parse(ctx, &child)?),
577 "exit_code" => cmd.exit_codes.push(SpecExitCode::parse(ctx, &child)?),
578 "select" => {
579 cmd.select = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
580 }
581 "help" => {
582 cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
583 }
584 "long_help" => {
585 cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
586 }
587 "help_md" => {
588 cmd.help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
589 }
590 "before_help" => {
591 cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
592 }
593 "before_long_help" => {
594 cmd.before_help_long =
595 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
596 }
597 "before_help_md" => {
598 cmd.before_help_md =
599 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
600 }
601 "after_help" => {
602 cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
603 }
604 "after_long_help" => {
605 cmd.after_help_long =
606 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
607 }
608 "after_help_md" => {
609 cmd.after_help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
610 }
611 "subcommand_required" => {
612 cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
613 }
614 "help_heading" => {
615 cmd.help_heading = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
616 }
617 "surface" => {
618 cmd.surface = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
619 }
620 "available_if" => {
621 cmd.available_if = child
622 .ensure_arg_len(1..)?
623 .args()
624 .map(|entry| entry.ensure_string())
625 .collect::<Result<Vec<_>, _>>()?;
626 }
627 "subcommand_help_heading" => {
628 cmd.subcommand_help_heading =
629 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
630 }
631 "subcommand_value_name" => {
632 cmd.subcommand_value_name =
633 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
634 }
635 "next_line_help" => {
636 cmd.next_line_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
637 }
638 "flatten_help" => {
639 cmd.flatten_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
640 }
641 "term_width" => {
642 cmd.term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
643 }
644 "max_term_width" => {
645 cmd.max_term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
646 }
647 "external_subcommand" => {
648 cmd.external_subcommand = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
649 }
650 "arg_required_else_help" => {
651 cmd.arg_required_else_help =
652 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
653 }
654 "disable_help_flag" => {
655 cmd.disable_help_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
656 }
657 "disable_help_subcommand" => {
658 cmd.disable_help_subcommand =
659 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
660 }
661 "disable_version_flag" => {
662 cmd.disable_version_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
663 }
664 "dont_delimit_trailing_values" => {
665 cmd.dont_delimit_trailing_values =
666 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
667 }
668 "args_override_self" => {
669 cmd.args_override_self = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
670 }
671 "subcommand_negates_reqs" => {
672 cmd.subcommand_negates_reqs =
673 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
674 }
675 "args_conflicts_with_subcommands" => {
676 cmd.args_conflicts_with_subcommands =
677 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
678 }
679 "subcommand_precedence_over_arg" => {
680 cmd.subcommand_precedence_over_arg =
681 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
682 }
683 "allow_missing_positional" => {
684 cmd.allow_missing_positional =
685 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
686 }
687 "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
688 "effect" => {
689 let arg = child.ensure_arg_len(1..=1)?.arg(0)?;
690 let raw = arg.ensure_string()?;
691 match raw.parse() {
692 Ok(effect) => cmd.effect = Some(effect),
693 Err(_) => bail_parse!(
694 ctx,
695 arg.entry.span(),
696 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
697 ),
698 }
699 }
700 "restart_token" => {
701 cmd.restart_token = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
702 }
703 "deprecated" => {
704 cmd.deprecated = match child.arg(0)?.value.as_bool() {
705 Some(true) => Some("deprecated".to_string()),
706 Some(false) => None,
707 None => Some(child.arg(0)?.ensure_string()?),
708 }
709 }
710 "deprecated_warn_at" => {
711 cmd.deprecated_warn_at = Some(child.arg(0)?.ensure_string()?)
712 }
713 "deprecated_remove_at" => {
714 cmd.deprecated_remove_at = Some(child.arg(0)?.ensure_string()?)
715 }
716 "complete" => {
717 let complete = SpecComplete::parse(ctx, &child)?;
718 cmd.complete.insert(complete.name.clone(), complete);
719 }
720 k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
721 }
722 }
723 let mut sigils: Vec<&str> = Vec::new();
724 for arg in &cmd.args {
725 if let Some(sigil) = &arg.sigil {
726 if let Some(existing) = sigils
727 .iter()
728 .find(|existing| existing.starts_with(sigil) || sigil.starts_with(**existing))
729 {
730 bail_parse!(
731 ctx,
732 node.node.name().span(),
733 "argument sigils must not overlap: {existing:?} and {sigil:?}"
734 );
735 }
736 sigils.push(sigil);
737 }
738 }
739 if let Some(clause) = &cmd.clause {
740 if !cmd.args.is_empty() {
741 bail_parse!(
742 ctx,
743 node.span(),
744 "a command cannot declare both top-level arguments and a clause"
745 );
746 }
747 if cmd.restart_token.is_some() {
748 bail_parse!(
749 ctx,
750 node.span(),
751 "a command cannot declare both restart_token and a clause"
752 );
753 }
754 if clause.args.iter().any(|arg| arg.sigil.is_some()) {
755 bail_parse!(
756 ctx,
757 node.span(),
758 "sigil arguments are not supported inside clauses"
759 );
760 }
761 if let Some(spelling) = clause.conflicting_flag_spelling(&cmd.flags) {
762 bail_parse!(
763 ctx,
764 node.span(),
765 "clause flag spelling {spelling:?} conflicts with another flag on this command"
766 );
767 }
768 }
769 Ok(cmd)
770 }
771
772 pub(crate) fn validate_sigil_prefixes(&self) -> Result<(), String> {
773 fn validate(cmd: &SpecCommand, ancestors: &[String]) -> Result<(), String> {
774 let mut active = ancestors.to_vec();
775 for sigil in cmd.args.iter().filter_map(|arg| arg.sigil.as_ref()) {
776 if let Some(existing) = active.iter().find(|existing| {
777 existing.starts_with(sigil.as_str()) || sigil.starts_with(existing.as_str())
778 }) {
779 return Err(format!(
780 "argument sigils must not overlap: {existing:?} and {sigil:?}"
781 ));
782 }
783 active.push(sigil.clone());
784 }
785 for subcommand in cmd.subcommands.values() {
786 validate(subcommand, &active)?;
787 }
788 Ok(())
789 }
790
791 validate(self, &[])
792 }
793
794 pub(crate) fn validate_clause_flag_spellings(&self) -> Result<(), String> {
795 fn validate(cmd: &SpecCommand) -> Result<(), String> {
796 if let Some(clause) = &cmd.clause
797 && let Some(spelling) = clause.conflicting_flag_spelling(&cmd.flags)
798 {
799 return Err(format!(
800 "clause flag spelling {spelling:?} conflicts with another flag on this command"
801 ));
802 }
803 for subcommand in cmd.subcommands.values() {
804 validate(subcommand)?;
805 }
806 Ok(())
807 }
808
809 validate(self)
810 }
811 pub(crate) fn is_empty(&self) -> bool {
812 self.args.is_empty()
813 && self.clause.is_none()
814 && self.flags.is_empty()
815 && self.mounts.is_empty()
816 && self.subcommands.is_empty()
817 }
818 pub fn usage(&self) -> String {
819 self.usage_with_subcommands(true)
820 }
821
822 #[cfg(feature = "cli-help")]
825 pub(crate) fn usage_without_subcommands(&self) -> String {
826 self.usage_with_subcommands(false)
827 }
828
829 fn usage_with_subcommands(&self, include_subcommands: bool) -> String {
830 let mut usage = self.full_cmd.join(" ");
831 let flags = self
832 .flags
833 .iter()
834 .filter(|f| !f.hide && !f.builtin)
835 .collect_vec();
836 let args = self.args.iter().filter(|a| !a.hide).collect_vec();
837 if !flags.is_empty() {
838 if flags.len() <= 2 {
839 let inlines = flags
840 .iter()
841 .map(|f| {
842 if f.required {
843 format!("<{}>", f.usage())
844 } else {
845 format!("[{}]", f.usage())
846 }
847 })
848 .join(" ");
849 usage = format!("{usage} {inlines}").trim().to_string();
850 } else if flags.iter().any(|f| f.required) {
851 usage = format!("{usage} <FLAGS>");
852 } else {
853 usage = format!("{usage} [FLAGS]");
854 }
855 }
856 if !args.is_empty() {
857 if args.len() <= 2 {
858 let inlines = args.iter().map(|a| a.usage()).join(" ");
859 usage = format!("{usage} {inlines}").trim().to_string();
860 } else if args.iter().any(|a| a.required) {
861 usage = format!("{usage} <ARGS>…");
862 } else {
863 usage = format!("{usage} [ARGS]…");
864 }
865 }
866 if let Some(clause) = &self.clause {
867 usage = format!("{usage} {}", clause.usage());
868 }
869 if include_subcommands && !self.subcommands.is_empty() {
874 let name = self
875 .subcommand_value_name
876 .as_deref()
877 .unwrap_or("SUBCOMMAND");
878 usage = if self.subcommand_required {
879 format!("{usage} <{name}>")
880 } else {
881 format!("{usage} [{name}]")
882 };
883 }
884 for synopsis in self
885 .mounts
886 .iter()
887 .filter_map(|mount| mount.synopsis.as_deref())
888 {
889 usage = format!("{usage} {synopsis}");
890 }
891 usage.trim().to_string()
892 }
893 pub(crate) fn reset_subcommand_lookup(&mut self) {
898 self.subcommand_lookup = OnceLock::new();
899 }
900
901 pub(crate) fn merge(&mut self, other: Self) {
902 self.subcommand_lookup = OnceLock::new();
907 let Self {
911 name,
912 help,
913 help_long,
914 help_md,
915 before_help,
916 before_help_long,
917 before_help_md,
918 after_help,
919 after_help_long,
920 after_help_md,
921 args,
922 clause,
923 flags,
924 uses,
925 mounts,
926 groups,
927 aliases,
928 hidden_aliases,
929 examples,
930 headings,
931 outputs,
932 select,
933 exit_codes,
934 hide,
935 help_heading,
936 surface,
937 available_if,
938 display_order,
939 subcommand_required,
940 subcommand_help_heading,
941 subcommand_value_name,
942 next_line_help,
943 flatten_help,
944 term_width,
945 max_term_width,
946 external_subcommand,
947 arg_required_else_help,
948 disable_help_flag,
949 disable_help_subcommand,
950 disable_version_flag,
951 dont_delimit_trailing_values,
952 args_override_self,
953 subcommand_negates_reqs,
954 args_conflicts_with_subcommands,
955 subcommand_precedence_over_arg,
956 allow_missing_positional,
957 restart_token,
958 subcommands,
959 complete,
960 deprecated,
961 deprecated_warn_at,
962 deprecated_remove_at,
963 effect,
964 unknown_flags,
965 full_cmd: _,
967 usage: _,
968 mounted: _,
969 flags_from_mount: _,
970 subcommand_lookup: _,
971 } = other;
972 if !name.is_empty() {
973 self.name = name;
974 }
975 if help.is_some() {
976 self.help = help;
977 }
978 if help_long.is_some() {
979 self.help_long = help_long;
980 }
981 if help_md.is_some() {
982 self.help_md = help_md;
983 }
984 if before_help.is_some() {
985 self.before_help = before_help;
986 }
987 if before_help_long.is_some() {
988 self.before_help_long = before_help_long;
989 }
990 if before_help_md.is_some() {
991 self.before_help_md = before_help_md;
992 }
993 if after_help.is_some() {
994 self.after_help = after_help;
995 }
996 if after_help_long.is_some() {
997 self.after_help_long = after_help_long;
998 }
999 if after_help_md.is_some() {
1000 self.after_help_md = after_help_md;
1001 }
1002 if !args.is_empty() {
1003 self.args = args;
1004 }
1005 if clause.is_some() {
1006 self.clause = clause;
1007 }
1008 let flags_replaced = !flags.is_empty();
1009 if flags_replaced {
1010 self.flags = flags;
1011 }
1012 if flags_replaced || !uses.is_empty() {
1021 self.uses = uses;
1022 }
1023 if !mounts.is_empty() {
1024 self.mounts = mounts;
1025 }
1026 if flags_replaced || !groups.is_empty() {
1032 self.groups = groups;
1033 }
1034 if !aliases.is_empty() {
1035 self.aliases = aliases;
1036 }
1037 if !hidden_aliases.is_empty() {
1038 self.hidden_aliases = hidden_aliases;
1039 }
1040 if !examples.is_empty() {
1041 self.examples = examples;
1042 }
1043 if !headings.is_empty() {
1044 self.headings = headings;
1045 }
1046 if flags_replaced || !outputs.is_empty() {
1050 self.outputs = outputs;
1051 }
1052 if flags_replaced || select.is_some() {
1053 self.select = select;
1054 }
1055 if !exit_codes.is_empty() {
1056 self.exit_codes = exit_codes;
1057 }
1058 self.hide = hide;
1059 if help_heading.is_some() {
1060 self.help_heading = help_heading;
1061 }
1062 if surface.is_some() {
1063 self.surface = surface;
1064 }
1065 if !available_if.is_empty() {
1066 self.available_if = available_if;
1067 }
1068 if display_order.is_some() {
1069 self.display_order = display_order;
1070 }
1071 self.subcommand_required = subcommand_required;
1072 if subcommand_help_heading.is_some() {
1073 self.subcommand_help_heading = subcommand_help_heading;
1074 }
1075 if subcommand_value_name.is_some() {
1076 self.subcommand_value_name = subcommand_value_name;
1077 }
1078 self.next_line_help = next_line_help;
1079 self.flatten_help = flatten_help;
1080 if term_width.is_some() {
1081 self.term_width = term_width;
1082 }
1083 if max_term_width.is_some() {
1084 self.max_term_width = max_term_width;
1085 }
1086 self.external_subcommand = external_subcommand;
1087 self.arg_required_else_help = arg_required_else_help;
1088 self.disable_help_flag = disable_help_flag;
1089 self.disable_help_subcommand = disable_help_subcommand;
1090 self.disable_version_flag = disable_version_flag;
1091 self.dont_delimit_trailing_values = dont_delimit_trailing_values;
1092 self.args_override_self = args_override_self;
1093 self.subcommand_negates_reqs = subcommand_negates_reqs;
1094 self.args_conflicts_with_subcommands = args_conflicts_with_subcommands;
1095 self.subcommand_precedence_over_arg = subcommand_precedence_over_arg;
1096 self.allow_missing_positional = allow_missing_positional;
1097 if effect.is_some() {
1098 self.effect = effect;
1099 }
1100 if unknown_flags.is_some() {
1101 self.unknown_flags = unknown_flags;
1102 }
1103 if deprecated.is_some() {
1104 self.deprecated = deprecated;
1105 }
1106 if deprecated_warn_at.is_some() {
1107 self.deprecated_warn_at = deprecated_warn_at;
1108 }
1109 if deprecated_remove_at.is_some() {
1110 self.deprecated_remove_at = deprecated_remove_at;
1111 }
1112 if restart_token.is_some() {
1113 self.restart_token = restart_token;
1114 }
1115 for (name, cmd) in subcommands {
1116 self.subcommands.insert(name, cmd);
1117 }
1118 for (name, complete) in complete {
1119 self.complete.insert(name, complete);
1120 }
1121 }
1122
1123 pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
1124 let mut cmds = vec![];
1125 for cmd in self.subcommands.values() {
1126 cmds.push(cmd);
1127 cmds.extend(cmd.all_subcommands());
1128 }
1129 cmds
1130 }
1131
1132 pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
1133 let sl = self.subcommand_lookup.get_or_init(|| {
1134 let mut map = HashMap::new();
1135 for name in self.subcommands.keys() {
1143 map.insert(name.clone(), name.clone());
1144 }
1145 for (name, cmd) in &self.subcommands {
1146 for alias in cmd.aliases.iter().chain(&cmd.hidden_aliases) {
1147 map.entry(alias.clone()).or_insert_with(|| name.clone());
1148 }
1149 }
1150 map
1151 });
1152 let name = sl.get(name)?;
1153 self.subcommands.get(name)
1154 }
1155
1156 pub(crate) fn mount(
1157 &mut self,
1158 global_flag_args: &[String],
1159 injected: Option<&HashMap<String, String>>,
1160 ) -> Result<(), UsageErr> {
1161 for mount in self.mounts.iter().cloned().collect_vec() {
1162 let cmd = if global_flag_args.is_empty() {
1163 mount.run.clone()
1164 } else {
1165 let mut tokens = crate::shell_words::split(&mount.run)
1169 .expect("mount command should be valid shell syntax");
1170 if !tokens.is_empty() {
1171 tokens.splice(1..1, global_flag_args.iter().cloned());
1173 }
1174 crate::shell_words::join(tokens)
1176 };
1177 let output = match injected {
1178 Some(outputs) => outputs
1179 .get(&mount.run)
1180 .cloned()
1181 .ok_or_else(|| UsageErr::MissingMountOutput(mount.run.clone()))?,
1182 None => sh(&cmd).map_err(|err| match err {
1185 UsageErr::ShellError(message) if !self.name.is_empty() => {
1186 UsageErr::ShellError(format!(
1187 "could not read the commands mounted under `{}`: {message}",
1188 self.name
1189 ))
1190 }
1191 err => err,
1192 })?,
1193 };
1194 let mut spec: Spec = output.parse()?;
1195 if let Some(outputs) = injected {
1196 spec.resolve_mount_outputs_at_root(outputs, false)?;
1199 }
1200 for cmd in spec.cmd.subcommands.values_mut() {
1203 cmd.mark_mounted();
1204 }
1205 self.flags_from_mount |= !spec.cmd.flags.is_empty();
1208 let (help_flag, help_subcommand, version_flag) = (
1211 self.disable_help_flag,
1212 self.disable_help_subcommand,
1213 self.disable_version_flag,
1214 );
1215 self.merge(spec.cmd);
1216 self.disable_help_flag |= help_flag;
1217 self.disable_help_subcommand |= help_subcommand;
1218 self.disable_version_flag |= version_flag;
1219 }
1220 self.validate_clause_flag_spellings()
1221 .map_err(UsageErr::InvalidSpec)
1222 }
1223
1224 pub(crate) fn mark_mounted(&mut self) {
1226 self.mounted = true;
1227 for cmd in self.subcommands.values_mut() {
1228 cmd.mark_mounted();
1229 }
1230 }
1231}
1232
1233impl From<&SpecCommand> for KdlNode {
1234 fn from(cmd: &SpecCommand) -> Self {
1235 let SpecCommand {
1238 name,
1239 hide,
1240 help_heading,
1241 surface,
1242 available_if,
1243 display_order,
1244 subcommand_required,
1245 subcommand_help_heading,
1246 subcommand_value_name,
1247 next_line_help,
1248 flatten_help,
1249 term_width,
1250 max_term_width,
1251 external_subcommand,
1252 arg_required_else_help,
1253 disable_help_flag,
1254 disable_help_subcommand,
1255 disable_version_flag,
1256 dont_delimit_trailing_values,
1257 args_override_self,
1258 subcommand_negates_reqs,
1259 args_conflicts_with_subcommands,
1260 subcommand_precedence_over_arg,
1261 allow_missing_positional,
1262 restart_token,
1263 unknown_flags,
1264 aliases,
1265 hidden_aliases,
1266 help,
1267 help_long,
1268 help_md,
1269 before_help,
1270 before_help_long,
1271 before_help_md,
1272 after_help,
1273 after_help_long,
1274 after_help_md,
1275 deprecated,
1276 deprecated_warn_at,
1277 deprecated_remove_at,
1278 effect,
1279 flags,
1280 args,
1281 clause,
1282 mounts,
1283 groups,
1284 subcommands,
1285 complete,
1286 examples,
1287 headings,
1288 outputs,
1289 select,
1290 exit_codes,
1291 uses: _,
1294 full_cmd: _,
1296 usage: _,
1297 mounted: _,
1298 flags_from_mount: _,
1299 subcommand_lookup: _,
1300 } = cmd;
1301 let mut node = Self::new("cmd");
1302 node.entries_mut().push(name.clone().into());
1303 if *hide {
1304 node.entries_mut().push(KdlEntry::new_prop("hide", true));
1305 }
1306 if let Some(heading) = help_heading {
1307 node.entries_mut()
1308 .push(KdlEntry::new_prop("help_heading", heading.clone()));
1309 }
1310 if let Some(surface) = surface {
1311 node.push(KdlEntry::new_prop("surface", surface.clone()));
1312 }
1313 if !available_if.is_empty() {
1314 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1315 let mut condition = KdlNode::new("available_if");
1316 for value in available_if {
1317 condition.push(string_entry(None, value));
1318 }
1319 children.nodes_mut().push(condition);
1320 }
1321 if let Some(order) = display_order {
1322 node.entries_mut()
1323 .push(KdlEntry::new_prop("display_order", *order as i128));
1324 }
1325 if *subcommand_required {
1326 node.entries_mut()
1327 .push(KdlEntry::new_prop("subcommand_required", true));
1328 }
1329 if let Some(heading) = subcommand_help_heading {
1330 node.push(KdlEntry::new_prop(
1331 "subcommand_help_heading",
1332 heading.clone(),
1333 ));
1334 }
1335 if let Some(name) = subcommand_value_name {
1336 node.push(KdlEntry::new_prop("subcommand_value_name", name.clone()));
1337 }
1338 if *next_line_help {
1339 node.push(KdlEntry::new_prop("next_line_help", true));
1340 }
1341 if *flatten_help {
1342 node.push(KdlEntry::new_prop("flatten_help", true));
1343 }
1344 if let Some(width) = term_width {
1345 node.push(KdlEntry::new_prop("term_width", *width as i128));
1346 }
1347 if let Some(width) = max_term_width {
1348 node.push(KdlEntry::new_prop("max_term_width", *width as i128));
1349 }
1350 if *external_subcommand {
1351 node.entries_mut()
1352 .push(KdlEntry::new_prop("external_subcommand", true));
1353 }
1354 if *arg_required_else_help {
1355 node.entries_mut()
1356 .push(KdlEntry::new_prop("arg_required_else_help", true));
1357 }
1358 if *disable_help_flag {
1359 node.push(KdlEntry::new_prop("disable_help_flag", true));
1360 }
1361 if *disable_help_subcommand {
1362 node.push(KdlEntry::new_prop("disable_help_subcommand", true));
1363 }
1364 if *disable_version_flag {
1365 node.push(KdlEntry::new_prop("disable_version_flag", true));
1366 }
1367 if *dont_delimit_trailing_values {
1368 node.entries_mut()
1369 .push(KdlEntry::new_prop("dont_delimit_trailing_values", true));
1370 }
1371 if !*args_override_self {
1372 node.push(KdlEntry::new_prop("args_override_self", false));
1373 }
1374 if *subcommand_negates_reqs {
1375 node.push(KdlEntry::new_prop("subcommand_negates_reqs", true));
1376 }
1377 if *args_conflicts_with_subcommands {
1378 node.push(KdlEntry::new_prop("args_conflicts_with_subcommands", true));
1379 }
1380 if *subcommand_precedence_over_arg {
1381 node.push(KdlEntry::new_prop("subcommand_precedence_over_arg", true));
1382 }
1383 if *allow_missing_positional {
1384 node.push(KdlEntry::new_prop("allow_missing_positional", true));
1385 }
1386 if let Some(restart_token) = &restart_token {
1387 node.entries_mut()
1388 .push(KdlEntry::new_prop("restart_token", restart_token.clone()));
1389 }
1390 if !aliases.is_empty() {
1391 let mut alias_node = KdlNode::new("alias");
1392 for alias in aliases {
1393 alias_node.entries_mut().push(alias.clone().into());
1394 }
1395 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1396 children.nodes_mut().push(alias_node);
1397 }
1398 if !hidden_aliases.is_empty() {
1399 let mut alias_node = KdlNode::new("alias");
1400 for alias in hidden_aliases {
1401 alias_node.entries_mut().push(alias.clone().into());
1402 }
1403 alias_node
1404 .entries_mut()
1405 .push(KdlEntry::new_prop("hide", true));
1406 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1407 children.nodes_mut().push(alias_node);
1408 }
1409 if let Some(help) = &help {
1410 node.entries_mut().push(string_entry(Some("help"), help));
1411 }
1412 if let Some(help) = &help_long {
1413 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1414 let mut node = KdlNode::new("long_help");
1415 node.push(string_entry(None, help));
1416 children.nodes_mut().push(node);
1417 }
1418 if let Some(help) = &help_md {
1419 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1420 let mut node = KdlNode::new("help_md");
1421 node.push(string_entry(None, help));
1422 children.nodes_mut().push(node);
1423 }
1424 if let Some(help) = &before_help {
1425 node.entries_mut()
1426 .push(string_entry(Some("before_help"), help));
1427 }
1428 if let Some(help) = &before_help_long {
1429 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1430 let mut node = KdlNode::new("before_long_help");
1431 node.push(string_entry(None, help));
1432 children.nodes_mut().push(node);
1433 }
1434 if let Some(help) = &before_help_md {
1435 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1436 let mut node = KdlNode::new("before_help_md");
1437 node.push(string_entry(None, help));
1438 children.nodes_mut().push(node);
1439 }
1440 if let Some(help) = &after_help {
1441 node.entries_mut()
1442 .push(string_entry(Some("after_help"), help));
1443 }
1444 if let Some(help) = &after_help_long {
1445 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1446 let mut node = KdlNode::new("after_long_help");
1447 node.push(string_entry(None, help));
1448 children.nodes_mut().push(node);
1449 }
1450 if let Some(help) = &after_help_md {
1451 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1452 let mut node = KdlNode::new("after_help_md");
1453 node.push(string_entry(None, help));
1454 children.nodes_mut().push(node);
1455 }
1456 if let Some(deprecated) = &deprecated {
1457 node.entries_mut()
1458 .push(string_entry(Some("deprecated"), deprecated));
1459 }
1460 if let Some(at) = deprecated_warn_at {
1461 node.push(string_entry(Some("deprecated_warn_at"), at));
1462 }
1463 if let Some(at) = deprecated_remove_at {
1464 node.push(string_entry(Some("deprecated_remove_at"), at));
1465 }
1466 if let Some(effect) = effect {
1467 node.entries_mut()
1468 .push(string_entry(Some("effect"), effect.as_str()));
1469 }
1470 if let Some(unknown_flags) = unknown_flags {
1471 node.entries_mut()
1472 .push(string_entry(Some("unknown_flags"), unknown_flags.as_str()));
1473 }
1474 for flag in flags {
1475 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1476 children.nodes_mut().push(flag.into());
1477 }
1478 for arg in args {
1479 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1480 children.nodes_mut().push(arg.into());
1481 }
1482 if let Some(clause) = clause {
1483 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1484 children.nodes_mut().push(clause.into());
1485 }
1486 for mount in mounts {
1487 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1488 children.nodes_mut().push(mount.into());
1489 }
1490 for group in groups {
1493 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1494 children.nodes_mut().push(group.into());
1495 }
1496 for cmd in subcommands.values() {
1497 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1498 children.nodes_mut().push(cmd.into());
1499 }
1500 for example in examples {
1501 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1502 children.nodes_mut().push(example.into());
1503 }
1504 for heading in headings {
1505 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1506 children.nodes_mut().push(heading.into());
1507 }
1508 for output in outputs {
1511 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1512 children.nodes_mut().push(output.into());
1513 }
1514 if let Some(select) = select {
1515 let mut select_node = KdlNode::new("select");
1516 select_node.push(string_entry(None, select));
1517 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1518 children.nodes_mut().push(select_node);
1519 }
1520 for exit_code in exit_codes {
1521 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1522 children.nodes_mut().push(exit_code.into());
1523 }
1524 for complete in complete.values() {
1525 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1526 children.nodes_mut().push(complete.into());
1527 }
1528 node
1529 }
1530}
1531
1532#[cfg(feature = "clap")]
1533impl From<&clap::Command> for SpecCommand {
1534 fn from(cmd: &clap::Command) -> Self {
1535 let mut spec = Self {
1536 name: cmd.get_name().to_string(),
1537 hide: cmd.is_hide_set(),
1538 help: cmd.get_about().map(|s| s.to_string()),
1539 help_long: cmd.get_long_about().map(|s| s.to_string()),
1540 before_help: cmd.get_before_help().map(|s| s.to_string()),
1541 before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
1542 after_help: cmd.get_after_help().map(|s| s.to_string()),
1543 after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
1544 ..Default::default()
1545 };
1546 spec.external_subcommand = cmd.is_allow_external_subcommands_set();
1566 let forwards = cmd
1567 .get_arguments()
1568 .any(|arg| arg.is_allow_hyphen_values_set() || arg.is_trailing_var_arg_set());
1569 spec.unknown_flags = Some(if forwards {
1570 UnknownFlags::Value
1571 } else {
1572 UnknownFlags::Error
1573 });
1574
1575 for alias in cmd.get_visible_aliases() {
1576 spec.aliases.push(alias.to_string());
1577 }
1578 for alias in cmd.get_all_aliases() {
1579 if spec.aliases.contains(&alias.to_string()) {
1580 continue;
1581 }
1582 spec.hidden_aliases.push(alias.to_string());
1583 }
1584 for arg in cmd.get_arguments() {
1585 let complete_type = crate::spec::arg::value_hint_type(arg.get_value_hint());
1586 let conflicts: Vec<String> = cmd
1587 .get_arg_conflicts_with(arg)
1588 .iter()
1589 .filter_map(|other| match (other.get_long(), other.get_short()) {
1590 (Some(long), _) => Some(format!("--{long}")),
1591 (None, Some(short)) => Some(format!("-{short}")),
1592 (None, None) if other.is_positional() => Some(SpecArg::from(*other).name),
1593 (None, None) => None,
1594 })
1595 .collect();
1596 if arg.is_positional() {
1597 let mut positional: SpecArg = arg.into();
1598 positional.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
1599 positional.conflicts = conflicts;
1600 if let Some(type_) = complete_type {
1601 let name = positional.name.to_lowercase();
1602 spec.complete.insert(
1603 name.clone(),
1604 SpecComplete {
1605 name,
1606 type_: Some(type_.to_string()),
1607 ..Default::default()
1608 },
1609 );
1610 }
1611 spec.args.push(positional)
1612 } else {
1613 let mut flag: SpecFlag = arg.into();
1614 if let Some(value) = &mut flag.arg {
1615 value.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
1616 }
1617 flag.conflicts = conflicts;
1625 if let (Some(type_), Some(value)) = (complete_type, flag.arg.as_ref()) {
1626 let name = value.name.to_lowercase();
1627 spec.complete.insert(
1628 name.clone(),
1629 SpecComplete {
1630 name,
1631 type_: Some(type_.to_string()),
1632 ..Default::default()
1633 },
1634 );
1635 }
1636 spec.flags.push(flag)
1637 }
1638 }
1639 if spec
1644 .args
1645 .windows(2)
1646 .all(|pair| pair[0].display_order <= pair[1].display_order)
1647 {
1648 for arg in &mut spec.args {
1649 arg.display_order = None;
1650 }
1651 }
1652 if spec
1653 .flags
1654 .windows(2)
1655 .all(|pair| pair[0].display_order <= pair[1].display_order)
1656 {
1657 for flag in &mut spec.flags {
1658 flag.display_order = None;
1659 }
1660 }
1661 for group in cmd.get_groups() {
1669 let members: Vec<String> = group
1670 .get_args()
1671 .filter_map(|id| cmd.get_arguments().find(|arg| arg.get_id() == id))
1672 .filter_map(|arg| match (arg.get_long(), arg.get_short()) {
1673 (Some(long), _) => Some(format!("--{long}")),
1674 (None, Some(short)) => Some(format!("-{short}")),
1675 (None, None) if arg.is_positional() => Some(SpecArg::from(arg).name),
1676 (None, None) => None,
1677 })
1678 .collect();
1679 if members.len() < 2 {
1683 continue;
1684 }
1685 let required = group.is_required_set();
1695 let multiple = group.clone().is_multiple();
1699 if multiple && !required {
1700 continue;
1701 }
1702 let mut spec_group = SpecGroup::new(group.get_id().as_str(), members);
1703 spec_group.required = required;
1704 spec_group.multiple = multiple;
1705 spec.groups.push(spec_group);
1706 }
1707 spec.subcommand_required = cmd.is_subcommand_required_set();
1708 spec.subcommand_help_heading = cmd.get_subcommand_help_heading().map(str::to_string);
1709 spec.subcommand_value_name = cmd.get_subcommand_value_name().map(str::to_string);
1710 spec.next_line_help = cmd.is_next_line_help_set();
1711 spec.flatten_help = cmd.is_flatten_help_set();
1712 spec.arg_required_else_help = cmd.is_arg_required_else_help_set();
1713 spec.disable_help_flag = cmd.is_disable_help_flag_set();
1714 spec.disable_help_subcommand =
1715 cmd.get_subcommands().next().is_some() && cmd.is_disable_help_subcommand_set();
1716 spec.disable_version_flag = (cmd.get_version().is_some()
1717 || cmd.get_long_version().is_some())
1718 && cmd.is_disable_version_flag_set();
1719 spec.dont_delimit_trailing_values = cmd.is_dont_delimit_trailing_values_set();
1720 spec.args_override_self = cmd.is_args_override_self();
1721 spec.subcommand_negates_reqs = cmd.is_subcommand_negates_reqs_set();
1722 spec.args_conflicts_with_subcommands = cmd.is_args_conflicts_with_subcommands_set();
1723 spec.subcommand_precedence_over_arg = cmd.is_subcommand_precedence_over_arg_set();
1724 spec.allow_missing_positional = cmd.is_allow_missing_positional_set();
1725 for subcmd in cmd.get_subcommands() {
1726 let mut scmd: SpecCommand = subcmd.into();
1727 scmd.name = subcmd.get_name().to_string();
1728 scmd.display_order = Some(subcmd.get_display_order());
1729 spec.subcommands.insert(scmd.name.clone(), scmd);
1730 }
1731 if spec
1735 .subcommands
1736 .iter()
1737 .all(|(_, subcommand)| subcommand.display_order == Some(999))
1738 {
1739 for (_, subcommand) in &mut spec.subcommands {
1740 subcommand.display_order = None;
1741 }
1742 }
1743 spec
1744 }
1745}
1746
1747#[cfg(feature = "clap")]
1748impl From<clap::Command> for Spec {
1749 fn from(cmd: clap::Command) -> Self {
1750 (&cmd).into()
1751 }
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756 use crate::Spec;
1757 use crate::spec::effect::SpecCommandEffect;
1758 use insta::assert_snapshot;
1759
1760 #[test]
1761 fn overlapping_sigils_are_rejected_on_one_command_and_across_subcommands() {
1762 for spec in [
1763 r#"bin "ex"
1764arg "[short]..." sigil="+"
1765arg "[long]..." sigil="++"
1766"#,
1767 r#"bin "ex"
1768arg "[short]..." sigil="+"
1769cmd "run" { arg "[long]..." sigil="++" }
1770"#,
1771 ] {
1772 let error = Spec::parse(&Default::default(), spec).unwrap_err();
1773 assert!(
1774 format!("{error:?}").contains("argument sigils must not overlap"),
1775 "{error:?}"
1776 );
1777 }
1778 }
1779
1780 #[test]
1781 fn test_effect_prop_and_child_node() {
1782 let spec = Spec::parse(
1783 &Default::default(),
1784 r#"
1785bin "mise"
1786cmd "ls" effect="read"
1787cmd "use" effect="write"
1788cmd "uninstall" {
1789 effect "destructive"
1790}
1791cmd "version"
1792 "#,
1793 )
1794 .unwrap();
1795
1796 let cmds = &spec.cmd.subcommands;
1797 assert_eq!(cmds["ls"].effect, Some(SpecCommandEffect::Read));
1798 assert_eq!(cmds["use"].effect, Some(SpecCommandEffect::Write));
1799 assert_eq!(
1800 cmds["uninstall"].effect,
1801 Some(SpecCommandEffect::Destructive)
1802 );
1803 assert_eq!(cmds["version"].effect, None);
1805 }
1806
1807 #[test]
1808 fn test_effect_is_not_inherited_by_subcommands() {
1809 let spec = Spec::parse(
1810 &Default::default(),
1811 r#"
1812bin "git"
1813cmd "remote" effect="read" {
1814 cmd "add" effect="write"
1815 cmd "show"
1816}
1817 "#,
1818 )
1819 .unwrap();
1820
1821 let remote = &spec.cmd.subcommands["remote"];
1822 assert_eq!(remote.effect, Some(SpecCommandEffect::Read));
1823 assert_eq!(
1824 remote.subcommands["add"].effect,
1825 Some(SpecCommandEffect::Write)
1826 );
1827 assert_eq!(remote.subcommands["show"].effect, None);
1828 }
1829
1830 #[test]
1831 fn test_effect_roundtrips_through_kdl() {
1832 let spec = Spec::parse(
1833 &Default::default(),
1834 r#"
1835bin "mise"
1836cmd "ls" effect="read"
1837cmd "uninstall" effect="destructive"
1838 "#,
1839 )
1840 .unwrap();
1841
1842 assert_snapshot!(spec, @r#"
1843 name mise
1844 bin mise
1845 cmd ls effect=read
1846 cmd uninstall effect=destructive
1847 "#);
1848 }
1849
1850 #[test]
1854 fn test_effect_survives_merge() {
1855 let cmd_with = |src: &str| {
1856 Spec::parse(&Default::default(), src)
1857 .unwrap()
1858 .cmd
1859 .subcommands["uninstall"]
1860 .clone()
1861 };
1862
1863 let declared = cmd_with(r#"cmd "uninstall" effect="destructive""#);
1864 let silent = cmd_with(r#"cmd "uninstall" help="Remove a tool""#);
1865 let contradicting = cmd_with(r#"cmd "uninstall" effect="write""#);
1866
1867 let mut cmd = declared.clone();
1868 cmd.merge(silent);
1869 assert_eq!(cmd.effect, Some(SpecCommandEffect::Destructive));
1870
1871 let mut cmd = declared;
1872 cmd.merge(contradicting);
1873 assert_eq!(cmd.effect, Some(SpecCommandEffect::Write));
1874 }
1875
1876 #[test]
1877 fn test_unknown_effect_is_an_error() {
1878 let err = Spec::parse(
1879 &Default::default(),
1880 r#"
1881bin "mise"
1882cmd "ls" effect="readonly"
1883 "#,
1884 )
1885 .unwrap_err();
1886 assert!(
1887 err.to_string().contains("Invalid usage config"),
1888 "unexpected error: {err}"
1889 );
1890 }
1891}
1892
1893#[cfg(test)]
1894mod merge_tests {
1895 use crate::Spec;
1896
1897 fn uninstall(src: &str) -> crate::SpecCommand {
1898 Spec::parse(&Default::default(), src)
1899 .unwrap()
1900 .cmd
1901 .subcommands["uninstall"]
1902 .clone()
1903 }
1904
1905 #[test]
1908 fn test_deprecated_survives_merge() {
1909 let declared = uninstall(r#"cmd "uninstall" deprecated="use `remove`""#);
1910 let silent = uninstall(r#"cmd "uninstall" help="Remove a tool""#);
1911 let contradicting = uninstall(r#"cmd "uninstall" deprecated="gone in v3""#);
1912
1913 let mut cmd = declared.clone();
1914 cmd.merge(silent);
1915 assert_eq!(cmd.deprecated.as_deref(), Some("use `remove`"));
1916
1917 let mut cmd = declared;
1918 cmd.merge(contradicting);
1919 assert_eq!(cmd.deprecated.as_deref(), Some("gone in v3"));
1920 }
1921
1922 #[test]
1923 fn mounted_flags_replace_outputs_and_their_selector() {
1924 let mut mounting = uninstall(
1925 r#"cmd "uninstall" { flag "--format <FORMAT>"; output "json" framing="json"; select "--format" }"#,
1926 );
1927 let mounted = uninstall(r#"cmd "uninstall" { flag "--quiet" }"#);
1928
1929 mounting.merge(mounted);
1930
1931 assert!(mounting.outputs.is_empty());
1932 assert!(mounting.select.is_none());
1933 }
1934}
1935
1936#[cfg(test)]
1937mod roundtrip_tests {
1938 use crate::Spec;
1939 use crate::kdl;
1940
1941 #[test]
1949 fn test_spec_survives_a_kdl_roundtrip() {
1950 let src = r#"
1951name "My CLI"
1952bin "mycli"
1953about "does things"
1954version "1.0.0"
1955author "nobody"
1956license "MIT"
1957
1958flag "-v --verbose" help="Verbose logging" global=#true count=#true
1959arg "<dir>" help="Directory to use"
1960
1961cmd "install" help="Install a package" subcommand_required=#false {
1962 alias "i"
1963 alias "add" hide=#true
1964 long_help "The long help for install"
1965 help_md "The **markdown** help for install"
1966 before_help "before"
1967 before_long_help "The long before-help for install"
1968 before_help_md "The **markdown** before-help for install"
1969 after_help "after"
1970 after_long_help "The long after-help for install"
1971 after_help_md "The **markdown** after-help for install"
1972 arg "<pkg>" help="Package to install"
1973 arg "[dest]" effect="write"
1974 flag "-f --force" help="Overwrite"
1975 flag "--purge" effect="destructive" overrides="-f" required_unless="--keep"
1976 flag "--format <FMT>" help="Output format"
1977 complete "pkg" run="mycli list --available" descriptions=#true
1978 example "mycli install foo" header="Install foo" help="Installs foo" lang="sh"
1979 example "mycli install bar"
1980 heading "Output" help="Formats are stable across releases."
1981 output "human" default=#true help="A progress log"
1982 output "json" framing="json" help="One report object" {
1983 schema "{\n \"type\": \"object\"\n}"
1984 }
1985 select "--format"
1986 exit_code 0 "installed"
1987 exit_code 1 "the package was not found"
1988 cmd "from" help="Install from a source" {
1989 arg "<src>"
1990 }
1991}
1992cmd "wrapped" help="Wraps another CLI" {
1993 mount run="mycli plugin usage-spec"
1994}
1995cmd "remove" help="Remove a package" deprecated="use `uninstall`" effect="destructive"
1996cmd "run" restart_token=":::" help="Run tasks"
1997cmd "exec" external_subcommand=#true help="Run an external command"
1998cmd "hidden" hide=#true
1999 "#;
2000
2001 let original = Spec::parse(&Default::default(), src).unwrap();
2002 let reparsed = Spec::parse(&Default::default(), &original.to_string()).unwrap();
2003
2004 let original = serde_json::to_value(&original).unwrap();
2005 let reparsed = serde_json::to_value(&reparsed).unwrap();
2006 pretty_assertions::assert_eq!(original, reparsed);
2007
2008 let install = &original["cmd"]["subcommands"]["install"];
2011 let purge = install["flags"]
2012 .as_array()
2013 .unwrap()
2014 .iter()
2015 .find(|flag| flag["name"] == "purge")
2016 .unwrap();
2017 assert_eq!(purge["overrides"], serde_json::json!(["-f"]));
2018 assert_eq!(purge["required_unless"], serde_json::json!(["--keep"]));
2019 for key in [
2020 "help_long",
2021 "help_md",
2022 "before_help",
2023 "before_help_long",
2024 "before_help_md",
2025 "after_help",
2026 "after_help_long",
2027 "after_help_md",
2028 "deprecated",
2029 "effect",
2030 "restart_token",
2031 "examples",
2032 "headings",
2033 "complete",
2034 "mounts",
2035 "aliases",
2036 "hidden_aliases",
2037 "outputs",
2038 "select",
2039 "exit_codes",
2040 ] {
2041 let populated = match key {
2042 "deprecated" | "effect" => {
2044 original["cmd"]["subcommands"]["remove"].get(key).is_some()
2045 }
2046 "restart_token" => original["cmd"]["subcommands"]["run"].get(key).is_some(),
2047 "mounts" => !original["cmd"]["subcommands"]["wrapped"]["mounts"]
2048 .as_array()
2049 .unwrap()
2050 .is_empty(),
2051 _ => match install.get(key) {
2052 Some(serde_json::Value::Array(a)) => !a.is_empty(),
2053 Some(serde_json::Value::Object(o)) => !o.is_empty(),
2054 Some(_) => true,
2055 None => false,
2056 },
2057 };
2058 assert!(populated, "fixture does not exercise `{key}`");
2059 }
2060
2061 assert!(
2064 install["flags"]
2065 .as_array()
2066 .unwrap()
2067 .iter()
2068 .any(|f| f.get("effect").is_some()),
2069 "fixture does not exercise a flag-level `effect`"
2070 );
2071 assert!(
2072 install["args"]
2073 .as_array()
2074 .unwrap()
2075 .iter()
2076 .any(|a| a.get("effect").is_some()),
2077 "fixture does not exercise an arg-level `effect`"
2078 );
2079 }
2080 #[cfg(feature = "clap")]
2081 #[test]
2082 fn a_clap_command_says_what_clap_would_do_with_an_unknown_flag() {
2083 use super::{SpecCommand, UnknownFlags};
2084
2085 let plain = clap::Command::new("build")
2089 .arg(clap::Arg::new("target").required(false))
2090 .arg(clap::Arg::new("force").long("force").num_args(0));
2091 let spec: SpecCommand = (&plain).into();
2092 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
2093
2094 let wrapper = clap::Command::new("run").arg(
2097 clap::Arg::new("args")
2098 .num_args(0..)
2099 .allow_hyphen_values(true),
2100 );
2101 let spec: SpecCommand = (&wrapper).into();
2102 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
2103
2104 let trailing = clap::Command::new("exec").arg(
2106 clap::Arg::new("cmd")
2107 .num_args(0..)
2108 .trailing_var_arg(true)
2109 .allow_hyphen_values(true),
2110 );
2111 let spec: SpecCommand = (&trailing).into();
2112 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
2113
2114 let external = clap::Command::new("x").allow_external_subcommands(true);
2117 let spec: SpecCommand = (&external).into();
2118 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
2119 assert!(spec.external_subcommand);
2120 }
2121
2122 #[cfg(feature = "clap")]
2123 #[test]
2124 fn the_decision_survives_being_written_and_read_back() {
2125 use super::SpecCommand;
2126
2127 let plain = clap::Command::new("build").arg(clap::Arg::new("target").required(false));
2130 let spec: SpecCommand = (&plain).into();
2131 let node: kdl::KdlNode = (&spec).into();
2132 let kdl = node.to_string();
2133 assert!(kdl.contains("unknown_flags=error"), "{kdl}");
2134 }
2135
2136 #[cfg(feature = "clap")]
2137 #[test]
2138 fn the_clap_bridge_preserves_args_override_self() {
2139 use super::SpecCommand;
2140
2141 let strict: SpecCommand = (&clap::Command::new("strict")).into();
2142 assert!(!strict.args_override_self, "clap is strict by default");
2143
2144 let permissive: SpecCommand =
2145 (&clap::Command::new("permissive").args_override_self(true)).into();
2146 assert!(permissive.args_override_self);
2147
2148 let node: kdl::KdlNode = (&strict).into();
2149 assert!(node.to_string().contains("args_override_self=#false"));
2150 }
2151
2152 #[cfg(feature = "clap")]
2153 #[test]
2154 fn the_clap_bridge_preserves_subcommand_presentation() {
2155 use super::SpecCommand;
2156
2157 let spec: SpecCommand = (&clap::Command::new("ex")
2158 .subcommand(clap::Command::new("run"))
2159 .subcommand_help_heading("Actions")
2160 .subcommand_value_name("ACTION"))
2161 .into();
2162 assert_eq!(spec.subcommand_help_heading.as_deref(), Some("Actions"));
2163 assert_eq!(spec.subcommand_value_name.as_deref(), Some("ACTION"));
2164 let node: kdl::KdlNode = (&spec).into();
2165 let kdl = node.to_string();
2166 assert!(kdl.contains("subcommand_help_heading=Actions"), "{kdl}");
2167 assert!(kdl.contains("subcommand_value_name=ACTION"), "{kdl}");
2168 }
2169
2170 #[cfg(feature = "clap")]
2171 #[test]
2172 fn the_clap_bridge_preserves_subcommand_negates_requirements() {
2173 use super::SpecCommand;
2174
2175 let spec: SpecCommand = (&clap::Command::new("ex").subcommand_negates_reqs(true)).into();
2176 assert!(spec.subcommand_negates_reqs);
2177 let node: kdl::KdlNode = (&spec).into();
2178 assert!(node.to_string().contains("subcommand_negates_reqs=#true"));
2179 }
2180
2181 #[cfg(feature = "clap")]
2182 #[test]
2183 fn the_clap_bridge_preserves_argument_subcommand_conflicts() {
2184 use super::SpecCommand;
2185
2186 let spec: SpecCommand =
2187 (&clap::Command::new("ex").args_conflicts_with_subcommands(true)).into();
2188 assert!(spec.args_conflicts_with_subcommands);
2189 let node: kdl::KdlNode = (&spec).into();
2190 assert!(
2191 node.to_string()
2192 .contains("args_conflicts_with_subcommands=#true")
2193 );
2194 }
2195
2196 #[cfg(feature = "clap")]
2197 #[test]
2198 fn the_clap_bridge_preserves_allow_missing_positional() {
2199 use super::SpecCommand;
2200
2201 let spec: SpecCommand = (&clap::Command::new("ex").allow_missing_positional(true)).into();
2202 assert!(spec.allow_missing_positional);
2203 let node: kdl::KdlNode = (&spec).into();
2204 assert!(node.to_string().contains("allow_missing_positional=#true"));
2205 }
2206}