1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use crate::error::UsageErr;
5use crate::sh::sh;
6use crate::spec::builder::SpecCommandBuilder;
7use crate::spec::context::ParsingContext;
8use crate::spec::effect::{SpecCommandEffect, EFFECT_VALUES};
9use crate::spec::exit_code::SpecExitCode;
10use crate::spec::flagset::SpecUse;
11use crate::spec::group::SpecGroup;
12use crate::spec::helpers::{string_entry, NodeHelper};
13use crate::spec::is_false;
14use crate::spec::mount::SpecMount;
15use crate::spec::output::SpecOutput;
16use crate::spec::unknown_flags::UnknownFlags;
17use crate::{Spec, SpecArg, SpecComplete, SpecFlag};
18use indexmap::IndexMap;
19use itertools::Itertools;
20use kdl::{KdlDocument, KdlEntry, KdlNode};
21use serde::Serialize;
22
23#[derive(Debug, Serialize, Clone)]
42pub struct SpecCommand {
43 pub full_cmd: Vec<String>,
45 pub usage: String,
47 pub subcommands: IndexMap<String, SpecCommand>,
49 pub args: Vec<SpecArg>,
51 pub flags: Vec<SpecFlag>,
53 #[serde(skip)]
60 pub(crate) uses: Vec<SpecUse>,
61 pub mounts: Vec<SpecMount>,
63 #[serde(skip_serializing_if = "Vec::is_empty")]
68 pub groups: Vec<SpecGroup>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub deprecated: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub deprecated_warn_at: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub deprecated_remove_at: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
81 pub effect: Option<SpecCommandEffect>,
82 pub unknown_flags: Option<UnknownFlags>,
90 pub hide: bool,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub help_heading: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub surface: Option<String>,
98 #[serde(skip_serializing_if = "Vec::is_empty")]
100 pub available_if: Vec<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub display_order: Option<usize>,
104 #[serde(skip)]
114 pub mounted: bool,
115 #[serde(skip)]
122 pub flags_from_mount: bool,
123 #[serde(skip_serializing_if = "is_false")]
125 pub subcommand_required: bool,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub subcommand_help_heading: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub subcommand_value_name: Option<String>,
132 #[serde(skip_serializing_if = "is_false")]
134 pub next_line_help: bool,
135 #[serde(skip_serializing_if = "is_false")]
137 pub flatten_help: bool,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub term_width: Option<usize>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub max_term_width: Option<usize>,
144 #[serde(skip_serializing_if = "is_false")]
151 pub external_subcommand: bool,
152 #[serde(skip_serializing_if = "is_false")]
154 pub arg_required_else_help: bool,
155 #[serde(skip_serializing_if = "is_false")]
156 pub disable_help_flag: bool,
157 #[serde(skip_serializing_if = "is_false")]
158 pub disable_help_subcommand: bool,
159 #[serde(skip_serializing_if = "is_false")]
160 pub disable_version_flag: bool,
161 #[serde(skip_serializing_if = "is_false")]
163 pub dont_delimit_trailing_values: bool,
164 pub args_override_self: bool,
167 #[serde(skip_serializing_if = "is_false")]
169 pub subcommand_negates_reqs: bool,
170 #[serde(skip_serializing_if = "is_false")]
172 pub args_conflicts_with_subcommands: bool,
173 #[serde(skip_serializing_if = "is_false")]
174 pub subcommand_precedence_over_arg: bool,
175 #[serde(skip_serializing_if = "is_false")]
177 pub allow_missing_positional: bool,
178 #[serde(skip_serializing_if = "Option::is_none")]
181 pub restart_token: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub help: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub help_long: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub help_md: Option<String>,
191 pub name: String,
193 pub aliases: Vec<String>,
195 pub hidden_aliases: Vec<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub before_help: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub before_help_long: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub before_help_md: Option<String>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub after_help: Option<String>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub after_help_long: Option<String>,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub after_help_md: Option<String>,
215 pub examples: Vec<SpecExample>,
217 pub headings: Vec<SpecHeading>,
219 #[serde(skip_serializing_if = "Vec::is_empty")]
224 pub outputs: Vec<SpecOutput>,
225 #[serde(skip_serializing_if = "Option::is_none")]
230 pub select: Option<String>,
231 #[serde(skip_serializing_if = "Vec::is_empty")]
233 pub exit_codes: Vec<SpecExitCode>,
234 #[serde(skip_serializing_if = "IndexMap::is_empty")]
236 pub complete: IndexMap<String, SpecComplete>,
237
238 #[serde(skip)]
243 pub(crate) subcommand_lookup: OnceLock<HashMap<String, String>>,
244}
245
246impl Default for SpecCommand {
247 fn default() -> Self {
248 Self {
249 full_cmd: vec![],
250 usage: "".to_string(),
251 subcommands: IndexMap::new(),
252 args: vec![],
253 flags: vec![],
254 uses: vec![],
255 mounts: vec![],
256 groups: vec![],
257 deprecated: None,
258 deprecated_warn_at: None,
259 deprecated_remove_at: None,
260 effect: None,
261 unknown_flags: None,
262 hide: false,
263 help_heading: None,
264 surface: None,
265 available_if: vec![],
266 display_order: None,
267 mounted: false,
268 flags_from_mount: false,
269 subcommand_required: false,
270 subcommand_help_heading: None,
271 subcommand_value_name: None,
272 next_line_help: false,
273 flatten_help: false,
274 term_width: None,
275 max_term_width: None,
276 external_subcommand: false,
277 arg_required_else_help: false,
278 disable_help_flag: false,
279 disable_help_subcommand: false,
280 disable_version_flag: false,
281 dont_delimit_trailing_values: false,
282 args_override_self: true,
283 subcommand_negates_reqs: false,
284 args_conflicts_with_subcommands: false,
285 subcommand_precedence_over_arg: false,
286 allow_missing_positional: false,
287 restart_token: None,
288 help: None,
289 help_long: None,
290 help_md: None,
291 name: "".to_string(),
292 aliases: vec![],
293 hidden_aliases: vec![],
294 before_help: None,
295 before_help_long: None,
296 before_help_md: None,
297 after_help: None,
298 after_help_long: None,
299 after_help_md: None,
300 examples: vec![],
301 headings: vec![],
302 outputs: vec![],
303 select: None,
304 exit_codes: vec![],
305 subcommand_lookup: OnceLock::new(),
306 complete: IndexMap::new(),
307 }
308 }
309}
310
311#[derive(Debug, Default, Serialize, Clone)]
312#[non_exhaustive]
313pub struct SpecExample {
314 pub code: String,
315 pub header: Option<String>,
316 pub help: Option<String>,
317 pub lang: String,
318}
319
320impl SpecExample {
321 pub fn new(code: impl Into<String>) -> Self {
323 Self {
324 code: code.into(),
325 ..Default::default()
326 }
327 }
328
329 pub fn header(mut self, header: impl Into<String>) -> Self {
331 self.header = Some(header.into());
332 self
333 }
334
335 pub fn help(mut self, help: impl Into<String>) -> Self {
337 self.help = Some(help.into());
338 self
339 }
340
341 pub fn lang(mut self, lang: impl Into<String>) -> Self {
343 self.lang = lang.into();
344 self
345 }
346}
347
348#[derive(Debug, Default, Serialize, Clone)]
353#[non_exhaustive]
354pub struct SpecHeading {
355 pub title: String,
356 pub help: String,
357}
358
359impl SpecHeading {
360 pub fn new(title: impl Into<String>, help: impl Into<String>) -> Self {
362 Self {
363 title: title.into(),
364 help: help.into(),
365 }
366 }
367}
368
369impl From<&SpecHeading> for KdlNode {
370 fn from(heading: &SpecHeading) -> KdlNode {
371 let mut node = KdlNode::new("heading");
372 node.push(string_entry(None, &heading.title));
373 node.push(string_entry(Some("help"), &heading.help));
374 node
375 }
376}
377
378impl From<&SpecExample> for KdlNode {
379 fn from(example: &SpecExample) -> KdlNode {
380 let mut node = KdlNode::new("example");
381 node.push(string_entry(None, &example.code));
382 if let Some(header) = &example.header {
383 node.push(string_entry(Some("header"), header));
384 }
385 if let Some(help) = &example.help {
386 node.push(string_entry(Some("help"), help));
387 }
388 if !example.lang.is_empty() {
389 node.push(string_entry(Some("lang"), &example.lang));
390 }
391 node
392 }
393}
394
395impl SpecCommand {
396 pub fn builder() -> SpecCommandBuilder {
398 SpecCommandBuilder::new()
399 }
400
401 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
402 node.ensure_arg_len(1..=1)?;
403 let mut cmd = Self {
404 name: node.arg(0)?.ensure_string()?.to_string(),
405 ..Default::default()
406 };
407 for (k, v) in node.props() {
408 match k {
409 "help" => cmd.help = Some(v.ensure_string()?),
410 "long_help" => cmd.help_long = Some(v.ensure_string()?),
411 "help_long" => cmd.help_long = Some(v.ensure_string()?),
412 "help_md" => cmd.help_md = Some(v.ensure_string()?),
413 "before_help" => cmd.before_help = Some(v.ensure_string()?),
414 "before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
415 "before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
416 "before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
417 "after_help" => cmd.after_help = Some(v.ensure_string()?),
418 "after_long_help" => {
419 cmd.after_help_long = Some(v.ensure_string()?);
420 }
421 "after_help_long" => {
422 cmd.after_help_long = Some(v.ensure_string()?);
423 }
424 "after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
425 "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
426 "subcommand_help_heading" => cmd.subcommand_help_heading = Some(v.ensure_string()?),
427 "subcommand_value_name" => cmd.subcommand_value_name = Some(v.ensure_string()?),
428 "next_line_help" => cmd.next_line_help = v.ensure_bool()?,
429 "flatten_help" => cmd.flatten_help = v.ensure_bool()?,
430 "term_width" => cmd.term_width = Some(v.ensure_usize()?),
431 "max_term_width" => cmd.max_term_width = Some(v.ensure_usize()?),
432 "external_subcommand" => cmd.external_subcommand = v.ensure_bool()?,
433 "arg_required_else_help" => cmd.arg_required_else_help = v.ensure_bool()?,
434 "disable_help_flag" => cmd.disable_help_flag = v.ensure_bool()?,
435 "disable_help_subcommand" => cmd.disable_help_subcommand = v.ensure_bool()?,
436 "disable_version_flag" => cmd.disable_version_flag = v.ensure_bool()?,
437 "dont_delimit_trailing_values" => {
438 cmd.dont_delimit_trailing_values = v.ensure_bool()?
439 }
440 "args_override_self" => cmd.args_override_self = v.ensure_bool()?,
441 "subcommand_negates_reqs" => cmd.subcommand_negates_reqs = v.ensure_bool()?,
442 "args_conflicts_with_subcommands" => {
443 cmd.args_conflicts_with_subcommands = v.ensure_bool()?
444 }
445 "subcommand_precedence_over_arg" => {
446 cmd.subcommand_precedence_over_arg = v.ensure_bool()?
447 }
448 "allow_missing_positional" => cmd.allow_missing_positional = v.ensure_bool()?,
449 "hide" => cmd.hide = v.ensure_bool()?,
450 "help_heading" => cmd.help_heading = Some(v.ensure_string()?),
451 "surface" => cmd.surface = Some(v.ensure_string()?),
452 "available_if" => cmd.available_if = vec![v.ensure_string()?],
453 "display_order" => cmd.display_order = Some(v.ensure_usize()?),
454 "unknown_flags" => {
455 let raw = v.ensure_string()?;
456 match raw.parse() {
457 Ok(mode) => cmd.unknown_flags = Some(mode),
458 Err(_) => bail_parse!(
459 ctx,
460 v.entry.span(),
461 "unsupported unknown_flags {raw}, expected one of: {}",
462 crate::spec::unknown_flags::UNKNOWN_FLAGS_VALUES
463 ),
464 }
465 }
466 "effect" => {
467 let raw = v.ensure_string()?;
468 match raw.parse() {
469 Ok(effect) => cmd.effect = Some(effect),
470 Err(_) => bail_parse!(
471 ctx,
472 v.entry.span(),
473 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
474 ),
475 }
476 }
477 "restart_token" => cmd.restart_token = Some(v.ensure_string()?),
478 "deprecated" => {
479 cmd.deprecated = match v.value.as_bool() {
480 Some(true) => Some("deprecated".to_string()),
481 Some(false) => None,
482 None => Some(v.ensure_string()?),
483 }
484 }
485 "deprecated_warn_at" => cmd.deprecated_warn_at = Some(v.ensure_string()?),
486 "deprecated_remove_at" => cmd.deprecated_remove_at = Some(v.ensure_string()?),
487 k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
488 }
489 }
490 for child in node.children() {
491 match child.name() {
492 "flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
493 "use" => {
494 let at = cmd.flags.len();
495 cmd.uses.push(SpecUse::parse(ctx, &child, at)?);
496 }
497 "arg" => {
498 let arg = SpecArg::parse(ctx, &child)?;
499 if arg.delimiter.is_some() && !arg.var {
502 bail_parse!(
503 ctx,
504 child.node.name().span(),
505 "argument <{}> has a delimiter and holds one value; add \
506 `var=#true` for the values it splits into",
507 arg.name
508 );
509 }
510 cmd.args.push(arg);
511 }
512 "mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
513 "group" => cmd.groups.push(SpecGroup::parse(ctx, &child)?),
514 "cmd" => {
515 let node = SpecCommand::parse(ctx, &child)?;
516 cmd.subcommands.insert(node.name.to_string(), node);
517 }
518 "alias" => {
519 let alias = child
520 .ensure_arg_len(1..)?
521 .args()
522 .map(|e| e.ensure_string())
523 .collect::<Result<Vec<_>, _>>()?;
524 let hide = child
525 .get("hide")
526 .map(|n| n.ensure_bool())
527 .unwrap_or(Ok(false))?;
528 if hide {
529 cmd.hidden_aliases.extend(alias);
530 } else {
531 cmd.aliases.extend(alias);
532 }
533 }
534 "example" => {
535 let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
536 let mut example = SpecExample::new(code.trim().to_string());
537 for (k, v) in child.props() {
538 match k {
539 "header" => example.header = Some(v.ensure_string()?),
540 "help" => example.help = Some(v.ensure_string()?),
541 "lang" => example.lang = v.ensure_string()?,
542 k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
543 }
544 }
545 cmd.examples.push(example);
546 }
547 "heading" => {
548 let title = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
549 let mut help = None;
550 for (k, v) in child.props() {
551 match k {
552 "help" => help = Some(v.ensure_string()?),
553 k => bail_parse!(ctx, v.entry.span(), "unsupported heading key {k}"),
554 }
555 }
556 let Some(help) = help else {
557 bail_parse!(ctx, child.node.span(), "heading {title} needs help text");
558 };
559 cmd.headings.push(SpecHeading::new(title, help));
560 }
561 "output" => cmd.outputs.push(SpecOutput::parse(ctx, &child)?),
562 "exit_code" => cmd.exit_codes.push(SpecExitCode::parse(ctx, &child)?),
563 "select" => {
564 cmd.select = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
565 }
566 "help" => {
567 cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
568 }
569 "long_help" => {
570 cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
571 }
572 "help_md" => {
573 cmd.help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
574 }
575 "before_help" => {
576 cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
577 }
578 "before_long_help" => {
579 cmd.before_help_long =
580 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
581 }
582 "before_help_md" => {
583 cmd.before_help_md =
584 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
585 }
586 "after_help" => {
587 cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
588 }
589 "after_long_help" => {
590 cmd.after_help_long =
591 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
592 }
593 "after_help_md" => {
594 cmd.after_help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
595 }
596 "subcommand_required" => {
597 cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
598 }
599 "help_heading" => {
600 cmd.help_heading = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
601 }
602 "surface" => {
603 cmd.surface = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
604 }
605 "available_if" => {
606 cmd.available_if = child
607 .ensure_arg_len(1..)?
608 .args()
609 .map(|entry| entry.ensure_string())
610 .collect::<Result<Vec<_>, _>>()?;
611 }
612 "subcommand_help_heading" => {
613 cmd.subcommand_help_heading =
614 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
615 }
616 "subcommand_value_name" => {
617 cmd.subcommand_value_name =
618 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
619 }
620 "next_line_help" => {
621 cmd.next_line_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
622 }
623 "flatten_help" => {
624 cmd.flatten_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
625 }
626 "term_width" => {
627 cmd.term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
628 }
629 "max_term_width" => {
630 cmd.max_term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
631 }
632 "external_subcommand" => {
633 cmd.external_subcommand = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
634 }
635 "arg_required_else_help" => {
636 cmd.arg_required_else_help =
637 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
638 }
639 "disable_help_flag" => {
640 cmd.disable_help_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
641 }
642 "disable_help_subcommand" => {
643 cmd.disable_help_subcommand =
644 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
645 }
646 "disable_version_flag" => {
647 cmd.disable_version_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
648 }
649 "dont_delimit_trailing_values" => {
650 cmd.dont_delimit_trailing_values =
651 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
652 }
653 "args_override_self" => {
654 cmd.args_override_self = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
655 }
656 "subcommand_negates_reqs" => {
657 cmd.subcommand_negates_reqs =
658 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
659 }
660 "args_conflicts_with_subcommands" => {
661 cmd.args_conflicts_with_subcommands =
662 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
663 }
664 "subcommand_precedence_over_arg" => {
665 cmd.subcommand_precedence_over_arg =
666 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
667 }
668 "allow_missing_positional" => {
669 cmd.allow_missing_positional =
670 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
671 }
672 "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
673 "effect" => {
674 let arg = child.ensure_arg_len(1..=1)?.arg(0)?;
675 let raw = arg.ensure_string()?;
676 match raw.parse() {
677 Ok(effect) => cmd.effect = Some(effect),
678 Err(_) => bail_parse!(
679 ctx,
680 arg.entry.span(),
681 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
682 ),
683 }
684 }
685 "restart_token" => {
686 cmd.restart_token = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
687 }
688 "deprecated" => {
689 cmd.deprecated = match child.arg(0)?.value.as_bool() {
690 Some(true) => Some("deprecated".to_string()),
691 Some(false) => None,
692 None => Some(child.arg(0)?.ensure_string()?),
693 }
694 }
695 "deprecated_warn_at" => {
696 cmd.deprecated_warn_at = Some(child.arg(0)?.ensure_string()?)
697 }
698 "deprecated_remove_at" => {
699 cmd.deprecated_remove_at = Some(child.arg(0)?.ensure_string()?)
700 }
701 "complete" => {
702 let complete = SpecComplete::parse(ctx, &child)?;
703 cmd.complete.insert(complete.name.clone(), complete);
704 }
705 k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
706 }
707 }
708 Ok(cmd)
709 }
710 pub(crate) fn is_empty(&self) -> bool {
711 self.args.is_empty()
712 && self.flags.is_empty()
713 && self.mounts.is_empty()
714 && self.subcommands.is_empty()
715 }
716 pub fn usage(&self) -> String {
717 self.usage_with_subcommands(true)
718 }
719
720 #[cfg(feature = "cli-help")]
723 pub(crate) fn usage_without_subcommands(&self) -> String {
724 self.usage_with_subcommands(false)
725 }
726
727 fn usage_with_subcommands(&self, include_subcommands: bool) -> String {
728 let mut usage = self.full_cmd.join(" ");
729 let flags = self
730 .flags
731 .iter()
732 .filter(|f| !f.hide && !f.builtin)
733 .collect_vec();
734 let args = self.args.iter().filter(|a| !a.hide).collect_vec();
735 if !flags.is_empty() {
736 if flags.len() <= 2 {
737 let inlines = flags
738 .iter()
739 .map(|f| {
740 if f.required {
741 format!("<{}>", f.usage())
742 } else {
743 format!("[{}]", f.usage())
744 }
745 })
746 .join(" ");
747 usage = format!("{usage} {inlines}").trim().to_string();
748 } else if flags.iter().any(|f| f.required) {
749 usage = format!("{usage} <FLAGS>");
750 } else {
751 usage = format!("{usage} [FLAGS]");
752 }
753 }
754 if !args.is_empty() {
755 if args.len() <= 2 {
756 let inlines = args.iter().map(|a| a.usage()).join(" ");
757 usage = format!("{usage} {inlines}").trim().to_string();
758 } else if args.iter().any(|a| a.required) {
759 usage = format!("{usage} <ARGS>…");
760 } else {
761 usage = format!("{usage} [ARGS]…");
762 }
763 }
764 if include_subcommands && !self.subcommands.is_empty() {
769 let name = self
770 .subcommand_value_name
771 .as_deref()
772 .unwrap_or("SUBCOMMAND");
773 usage = format!("{usage} <{name}>");
774 }
775 usage.trim().to_string()
776 }
777 pub(crate) fn reset_subcommand_lookup(&mut self) {
782 self.subcommand_lookup = OnceLock::new();
783 }
784
785 pub(crate) fn merge(&mut self, other: Self) {
786 self.subcommand_lookup = OnceLock::new();
791 let Self {
795 name,
796 help,
797 help_long,
798 help_md,
799 before_help,
800 before_help_long,
801 before_help_md,
802 after_help,
803 after_help_long,
804 after_help_md,
805 args,
806 flags,
807 uses,
808 mounts,
809 groups,
810 aliases,
811 hidden_aliases,
812 examples,
813 headings,
814 outputs,
815 select,
816 exit_codes,
817 hide,
818 help_heading,
819 surface,
820 available_if,
821 display_order,
822 subcommand_required,
823 subcommand_help_heading,
824 subcommand_value_name,
825 next_line_help,
826 flatten_help,
827 term_width,
828 max_term_width,
829 external_subcommand,
830 arg_required_else_help,
831 disable_help_flag,
832 disable_help_subcommand,
833 disable_version_flag,
834 dont_delimit_trailing_values,
835 args_override_self,
836 subcommand_negates_reqs,
837 args_conflicts_with_subcommands,
838 subcommand_precedence_over_arg,
839 allow_missing_positional,
840 restart_token,
841 subcommands,
842 complete,
843 deprecated,
844 deprecated_warn_at,
845 deprecated_remove_at,
846 effect,
847 unknown_flags,
848 full_cmd: _,
850 usage: _,
851 mounted: _,
852 flags_from_mount: _,
853 subcommand_lookup: _,
854 } = other;
855 if !name.is_empty() {
856 self.name = name;
857 }
858 if help.is_some() {
859 self.help = help;
860 }
861 if help_long.is_some() {
862 self.help_long = help_long;
863 }
864 if help_md.is_some() {
865 self.help_md = help_md;
866 }
867 if before_help.is_some() {
868 self.before_help = before_help;
869 }
870 if before_help_long.is_some() {
871 self.before_help_long = before_help_long;
872 }
873 if before_help_md.is_some() {
874 self.before_help_md = before_help_md;
875 }
876 if after_help.is_some() {
877 self.after_help = after_help;
878 }
879 if after_help_long.is_some() {
880 self.after_help_long = after_help_long;
881 }
882 if after_help_md.is_some() {
883 self.after_help_md = after_help_md;
884 }
885 if !args.is_empty() {
886 self.args = args;
887 }
888 let flags_replaced = !flags.is_empty();
889 if flags_replaced {
890 self.flags = flags;
891 }
892 if flags_replaced || !uses.is_empty() {
901 self.uses = uses;
902 }
903 if !mounts.is_empty() {
904 self.mounts = mounts;
905 }
906 if flags_replaced || !groups.is_empty() {
912 self.groups = groups;
913 }
914 if !aliases.is_empty() {
915 self.aliases = aliases;
916 }
917 if !hidden_aliases.is_empty() {
918 self.hidden_aliases = hidden_aliases;
919 }
920 if !examples.is_empty() {
921 self.examples = examples;
922 }
923 if !headings.is_empty() {
924 self.headings = headings;
925 }
926 if flags_replaced || !outputs.is_empty() {
930 self.outputs = outputs;
931 }
932 if flags_replaced || select.is_some() {
933 self.select = select;
934 }
935 if !exit_codes.is_empty() {
936 self.exit_codes = exit_codes;
937 }
938 self.hide = hide;
939 if help_heading.is_some() {
940 self.help_heading = help_heading;
941 }
942 if surface.is_some() {
943 self.surface = surface;
944 }
945 if !available_if.is_empty() {
946 self.available_if = available_if;
947 }
948 if display_order.is_some() {
949 self.display_order = display_order;
950 }
951 self.subcommand_required = subcommand_required;
952 if subcommand_help_heading.is_some() {
953 self.subcommand_help_heading = subcommand_help_heading;
954 }
955 if subcommand_value_name.is_some() {
956 self.subcommand_value_name = subcommand_value_name;
957 }
958 self.next_line_help = next_line_help;
959 self.flatten_help = flatten_help;
960 if term_width.is_some() {
961 self.term_width = term_width;
962 }
963 if max_term_width.is_some() {
964 self.max_term_width = max_term_width;
965 }
966 self.external_subcommand = external_subcommand;
967 self.arg_required_else_help = arg_required_else_help;
968 self.disable_help_flag = disable_help_flag;
969 self.disable_help_subcommand = disable_help_subcommand;
970 self.disable_version_flag = disable_version_flag;
971 self.dont_delimit_trailing_values = dont_delimit_trailing_values;
972 self.args_override_self = args_override_self;
973 self.subcommand_negates_reqs = subcommand_negates_reqs;
974 self.args_conflicts_with_subcommands = args_conflicts_with_subcommands;
975 self.subcommand_precedence_over_arg = subcommand_precedence_over_arg;
976 self.allow_missing_positional = allow_missing_positional;
977 if effect.is_some() {
978 self.effect = effect;
979 }
980 if unknown_flags.is_some() {
981 self.unknown_flags = unknown_flags;
982 }
983 if deprecated.is_some() {
984 self.deprecated = deprecated;
985 }
986 if deprecated_warn_at.is_some() {
987 self.deprecated_warn_at = deprecated_warn_at;
988 }
989 if deprecated_remove_at.is_some() {
990 self.deprecated_remove_at = deprecated_remove_at;
991 }
992 if restart_token.is_some() {
993 self.restart_token = restart_token;
994 }
995 for (name, cmd) in subcommands {
996 self.subcommands.insert(name, cmd);
997 }
998 for (name, complete) in complete {
999 self.complete.insert(name, complete);
1000 }
1001 }
1002
1003 pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
1004 let mut cmds = vec![];
1005 for cmd in self.subcommands.values() {
1006 cmds.push(cmd);
1007 cmds.extend(cmd.all_subcommands());
1008 }
1009 cmds
1010 }
1011
1012 pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
1013 let sl = self.subcommand_lookup.get_or_init(|| {
1014 let mut map = HashMap::new();
1015 for name in self.subcommands.keys() {
1023 map.insert(name.clone(), name.clone());
1024 }
1025 for (name, cmd) in &self.subcommands {
1026 for alias in cmd.aliases.iter().chain(&cmd.hidden_aliases) {
1027 map.entry(alias.clone()).or_insert_with(|| name.clone());
1028 }
1029 }
1030 map
1031 });
1032 let name = sl.get(name)?;
1033 self.subcommands.get(name)
1034 }
1035
1036 pub(crate) fn mount(
1037 &mut self,
1038 global_flag_args: &[String],
1039 injected: Option<&HashMap<String, String>>,
1040 ) -> Result<(), UsageErr> {
1041 for mount in self.mounts.iter().cloned().collect_vec() {
1042 let cmd = if global_flag_args.is_empty() {
1043 mount.run.clone()
1044 } else {
1045 let mut tokens = shell_words::split(&mount.run)
1049 .expect("mount command should be valid shell syntax");
1050 if !tokens.is_empty() {
1051 tokens.splice(1..1, global_flag_args.iter().cloned());
1053 }
1054 shell_words::join(tokens)
1056 };
1057 let output = match injected {
1058 Some(outputs) => outputs
1059 .get(&mount.run)
1060 .cloned()
1061 .ok_or_else(|| UsageErr::MissingMountOutput(mount.run.clone()))?,
1062 None => sh(&cmd)?,
1063 };
1064 let mut spec: Spec = output.parse()?;
1065 if let Some(outputs) = injected {
1066 spec.resolve_mount_outputs_at_root(outputs, false)?;
1069 }
1070 for cmd in spec.cmd.subcommands.values_mut() {
1073 cmd.mark_mounted();
1074 }
1075 self.flags_from_mount |= !spec.cmd.flags.is_empty();
1078 self.merge(spec.cmd);
1079 }
1080 Ok(())
1081 }
1082
1083 pub(crate) fn mark_mounted(&mut self) {
1085 self.mounted = true;
1086 for cmd in self.subcommands.values_mut() {
1087 cmd.mark_mounted();
1088 }
1089 }
1090}
1091
1092impl From<&SpecCommand> for KdlNode {
1093 fn from(cmd: &SpecCommand) -> Self {
1094 let SpecCommand {
1097 name,
1098 hide,
1099 help_heading,
1100 surface,
1101 available_if,
1102 display_order,
1103 subcommand_required,
1104 subcommand_help_heading,
1105 subcommand_value_name,
1106 next_line_help,
1107 flatten_help,
1108 term_width,
1109 max_term_width,
1110 external_subcommand,
1111 arg_required_else_help,
1112 disable_help_flag,
1113 disable_help_subcommand,
1114 disable_version_flag,
1115 dont_delimit_trailing_values,
1116 args_override_self,
1117 subcommand_negates_reqs,
1118 args_conflicts_with_subcommands,
1119 subcommand_precedence_over_arg,
1120 allow_missing_positional,
1121 restart_token,
1122 unknown_flags,
1123 aliases,
1124 hidden_aliases,
1125 help,
1126 help_long,
1127 help_md,
1128 before_help,
1129 before_help_long,
1130 before_help_md,
1131 after_help,
1132 after_help_long,
1133 after_help_md,
1134 deprecated,
1135 deprecated_warn_at,
1136 deprecated_remove_at,
1137 effect,
1138 flags,
1139 args,
1140 mounts,
1141 groups,
1142 subcommands,
1143 complete,
1144 examples,
1145 headings,
1146 outputs,
1147 select,
1148 exit_codes,
1149 uses: _,
1152 full_cmd: _,
1154 usage: _,
1155 mounted: _,
1156 flags_from_mount: _,
1157 subcommand_lookup: _,
1158 } = cmd;
1159 let mut node = Self::new("cmd");
1160 node.entries_mut().push(name.clone().into());
1161 if *hide {
1162 node.entries_mut().push(KdlEntry::new_prop("hide", true));
1163 }
1164 if let Some(heading) = help_heading {
1165 node.entries_mut()
1166 .push(KdlEntry::new_prop("help_heading", heading.clone()));
1167 }
1168 if let Some(surface) = surface {
1169 node.push(KdlEntry::new_prop("surface", surface.clone()));
1170 }
1171 if !available_if.is_empty() {
1172 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1173 let mut condition = KdlNode::new("available_if");
1174 for value in available_if {
1175 condition.push(string_entry(None, value));
1176 }
1177 children.nodes_mut().push(condition);
1178 }
1179 if let Some(order) = display_order {
1180 node.entries_mut()
1181 .push(KdlEntry::new_prop("display_order", *order as i128));
1182 }
1183 if *subcommand_required {
1184 node.entries_mut()
1185 .push(KdlEntry::new_prop("subcommand_required", true));
1186 }
1187 if let Some(heading) = subcommand_help_heading {
1188 node.push(KdlEntry::new_prop(
1189 "subcommand_help_heading",
1190 heading.clone(),
1191 ));
1192 }
1193 if let Some(name) = subcommand_value_name {
1194 node.push(KdlEntry::new_prop("subcommand_value_name", name.clone()));
1195 }
1196 if *next_line_help {
1197 node.push(KdlEntry::new_prop("next_line_help", true));
1198 }
1199 if *flatten_help {
1200 node.push(KdlEntry::new_prop("flatten_help", true));
1201 }
1202 if let Some(width) = term_width {
1203 node.push(KdlEntry::new_prop("term_width", *width as i128));
1204 }
1205 if let Some(width) = max_term_width {
1206 node.push(KdlEntry::new_prop("max_term_width", *width as i128));
1207 }
1208 if *external_subcommand {
1209 node.entries_mut()
1210 .push(KdlEntry::new_prop("external_subcommand", true));
1211 }
1212 if *arg_required_else_help {
1213 node.entries_mut()
1214 .push(KdlEntry::new_prop("arg_required_else_help", true));
1215 }
1216 if *disable_help_flag {
1217 node.push(KdlEntry::new_prop("disable_help_flag", true));
1218 }
1219 if *disable_help_subcommand {
1220 node.push(KdlEntry::new_prop("disable_help_subcommand", true));
1221 }
1222 if *disable_version_flag {
1223 node.push(KdlEntry::new_prop("disable_version_flag", true));
1224 }
1225 if *dont_delimit_trailing_values {
1226 node.entries_mut()
1227 .push(KdlEntry::new_prop("dont_delimit_trailing_values", true));
1228 }
1229 if !*args_override_self {
1230 node.push(KdlEntry::new_prop("args_override_self", false));
1231 }
1232 if *subcommand_negates_reqs {
1233 node.push(KdlEntry::new_prop("subcommand_negates_reqs", true));
1234 }
1235 if *args_conflicts_with_subcommands {
1236 node.push(KdlEntry::new_prop("args_conflicts_with_subcommands", true));
1237 }
1238 if *subcommand_precedence_over_arg {
1239 node.push(KdlEntry::new_prop("subcommand_precedence_over_arg", true));
1240 }
1241 if *allow_missing_positional {
1242 node.push(KdlEntry::new_prop("allow_missing_positional", true));
1243 }
1244 if let Some(restart_token) = &restart_token {
1245 node.entries_mut()
1246 .push(KdlEntry::new_prop("restart_token", restart_token.clone()));
1247 }
1248 if !aliases.is_empty() {
1249 let mut alias_node = KdlNode::new("alias");
1250 for alias in aliases {
1251 alias_node.entries_mut().push(alias.clone().into());
1252 }
1253 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1254 children.nodes_mut().push(alias_node);
1255 }
1256 if !hidden_aliases.is_empty() {
1257 let mut alias_node = KdlNode::new("alias");
1258 for alias in hidden_aliases {
1259 alias_node.entries_mut().push(alias.clone().into());
1260 }
1261 alias_node
1262 .entries_mut()
1263 .push(KdlEntry::new_prop("hide", true));
1264 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1265 children.nodes_mut().push(alias_node);
1266 }
1267 if let Some(help) = &help {
1268 node.entries_mut().push(string_entry(Some("help"), help));
1269 }
1270 if let Some(help) = &help_long {
1271 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1272 let mut node = KdlNode::new("long_help");
1273 node.push(string_entry(None, help));
1274 children.nodes_mut().push(node);
1275 }
1276 if let Some(help) = &help_md {
1277 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1278 let mut node = KdlNode::new("help_md");
1279 node.push(string_entry(None, help));
1280 children.nodes_mut().push(node);
1281 }
1282 if let Some(help) = &before_help {
1283 node.entries_mut()
1284 .push(string_entry(Some("before_help"), help));
1285 }
1286 if let Some(help) = &before_help_long {
1287 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1288 let mut node = KdlNode::new("before_long_help");
1289 node.push(string_entry(None, help));
1290 children.nodes_mut().push(node);
1291 }
1292 if let Some(help) = &before_help_md {
1293 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1294 let mut node = KdlNode::new("before_help_md");
1295 node.push(string_entry(None, help));
1296 children.nodes_mut().push(node);
1297 }
1298 if let Some(help) = &after_help {
1299 node.entries_mut()
1300 .push(string_entry(Some("after_help"), help));
1301 }
1302 if let Some(help) = &after_help_long {
1303 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1304 let mut node = KdlNode::new("after_long_help");
1305 node.push(string_entry(None, help));
1306 children.nodes_mut().push(node);
1307 }
1308 if let Some(help) = &after_help_md {
1309 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1310 let mut node = KdlNode::new("after_help_md");
1311 node.push(string_entry(None, help));
1312 children.nodes_mut().push(node);
1313 }
1314 if let Some(deprecated) = &deprecated {
1315 node.entries_mut()
1316 .push(string_entry(Some("deprecated"), deprecated));
1317 }
1318 if let Some(at) = deprecated_warn_at {
1319 node.push(string_entry(Some("deprecated_warn_at"), at));
1320 }
1321 if let Some(at) = deprecated_remove_at {
1322 node.push(string_entry(Some("deprecated_remove_at"), at));
1323 }
1324 if let Some(effect) = effect {
1325 node.entries_mut()
1326 .push(string_entry(Some("effect"), effect.as_str()));
1327 }
1328 if let Some(unknown_flags) = unknown_flags {
1329 node.entries_mut()
1330 .push(string_entry(Some("unknown_flags"), unknown_flags.as_str()));
1331 }
1332 for flag in flags {
1333 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1334 children.nodes_mut().push(flag.into());
1335 }
1336 for arg in args {
1337 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1338 children.nodes_mut().push(arg.into());
1339 }
1340 for mount in mounts {
1341 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1342 children.nodes_mut().push(mount.into());
1343 }
1344 for group in groups {
1347 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1348 children.nodes_mut().push(group.into());
1349 }
1350 for cmd in subcommands.values() {
1351 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1352 children.nodes_mut().push(cmd.into());
1353 }
1354 for example in examples {
1355 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1356 children.nodes_mut().push(example.into());
1357 }
1358 for heading in headings {
1359 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1360 children.nodes_mut().push(heading.into());
1361 }
1362 for output in outputs {
1365 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1366 children.nodes_mut().push(output.into());
1367 }
1368 if let Some(select) = select {
1369 let mut select_node = KdlNode::new("select");
1370 select_node.push(string_entry(None, select));
1371 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1372 children.nodes_mut().push(select_node);
1373 }
1374 for exit_code in exit_codes {
1375 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1376 children.nodes_mut().push(exit_code.into());
1377 }
1378 for complete in complete.values() {
1379 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1380 children.nodes_mut().push(complete.into());
1381 }
1382 node
1383 }
1384}
1385
1386#[cfg(feature = "clap")]
1387impl From<&clap::Command> for SpecCommand {
1388 fn from(cmd: &clap::Command) -> Self {
1389 let mut spec = Self {
1390 name: cmd.get_name().to_string(),
1391 hide: cmd.is_hide_set(),
1392 help: cmd.get_about().map(|s| s.to_string()),
1393 help_long: cmd.get_long_about().map(|s| s.to_string()),
1394 before_help: cmd.get_before_help().map(|s| s.to_string()),
1395 before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
1396 after_help: cmd.get_after_help().map(|s| s.to_string()),
1397 after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
1398 ..Default::default()
1399 };
1400 spec.external_subcommand = cmd.is_allow_external_subcommands_set();
1420 let forwards = cmd
1421 .get_arguments()
1422 .any(|arg| arg.is_allow_hyphen_values_set() || arg.is_trailing_var_arg_set());
1423 spec.unknown_flags = Some(if forwards {
1424 UnknownFlags::Value
1425 } else {
1426 UnknownFlags::Error
1427 });
1428
1429 for alias in cmd.get_visible_aliases() {
1430 spec.aliases.push(alias.to_string());
1431 }
1432 for alias in cmd.get_all_aliases() {
1433 if spec.aliases.contains(&alias.to_string()) {
1434 continue;
1435 }
1436 spec.hidden_aliases.push(alias.to_string());
1437 }
1438 for arg in cmd.get_arguments() {
1439 let complete_type = crate::spec::arg::value_hint_type(arg.get_value_hint());
1440 let conflicts: Vec<String> = cmd
1441 .get_arg_conflicts_with(arg)
1442 .iter()
1443 .filter_map(|other| match (other.get_long(), other.get_short()) {
1444 (Some(long), _) => Some(format!("--{long}")),
1445 (None, Some(short)) => Some(format!("-{short}")),
1446 (None, None) if other.is_positional() => Some(SpecArg::from(*other).name),
1447 (None, None) => None,
1448 })
1449 .collect();
1450 if arg.is_positional() {
1451 let mut positional: SpecArg = arg.into();
1452 positional.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
1453 positional.conflicts = conflicts;
1454 if let Some(type_) = complete_type {
1455 let name = positional.name.to_lowercase();
1456 spec.complete.insert(
1457 name.clone(),
1458 SpecComplete {
1459 name,
1460 type_: Some(type_.to_string()),
1461 ..Default::default()
1462 },
1463 );
1464 }
1465 spec.args.push(positional)
1466 } else {
1467 let mut flag: SpecFlag = arg.into();
1468 if let Some(value) = &mut flag.arg {
1469 value.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
1470 }
1471 flag.conflicts = conflicts;
1479 if let (Some(type_), Some(value)) = (complete_type, flag.arg.as_ref()) {
1480 let name = value.name.to_lowercase();
1481 spec.complete.insert(
1482 name.clone(),
1483 SpecComplete {
1484 name,
1485 type_: Some(type_.to_string()),
1486 ..Default::default()
1487 },
1488 );
1489 }
1490 spec.flags.push(flag)
1491 }
1492 }
1493 if spec
1498 .args
1499 .windows(2)
1500 .all(|pair| pair[0].display_order <= pair[1].display_order)
1501 {
1502 for arg in &mut spec.args {
1503 arg.display_order = None;
1504 }
1505 }
1506 if spec
1507 .flags
1508 .windows(2)
1509 .all(|pair| pair[0].display_order <= pair[1].display_order)
1510 {
1511 for flag in &mut spec.flags {
1512 flag.display_order = None;
1513 }
1514 }
1515 for group in cmd.get_groups() {
1523 let members: Vec<String> = group
1524 .get_args()
1525 .filter_map(|id| cmd.get_arguments().find(|arg| arg.get_id() == id))
1526 .filter_map(|arg| match (arg.get_long(), arg.get_short()) {
1527 (Some(long), _) => Some(format!("--{long}")),
1528 (None, Some(short)) => Some(format!("-{short}")),
1529 (None, None) if arg.is_positional() => Some(SpecArg::from(arg).name),
1530 (None, None) => None,
1531 })
1532 .collect();
1533 if members.len() < 2 {
1537 continue;
1538 }
1539 let required = group.is_required_set();
1549 let multiple = group.clone().is_multiple();
1553 if multiple && !required {
1554 continue;
1555 }
1556 let mut spec_group = SpecGroup::new(group.get_id().as_str(), members);
1557 spec_group.required = required;
1558 spec_group.multiple = multiple;
1559 spec.groups.push(spec_group);
1560 }
1561 spec.subcommand_required = cmd.is_subcommand_required_set();
1562 spec.subcommand_help_heading = cmd.get_subcommand_help_heading().map(str::to_string);
1563 spec.subcommand_value_name = cmd.get_subcommand_value_name().map(str::to_string);
1564 spec.next_line_help = cmd.is_next_line_help_set();
1565 spec.flatten_help = cmd.is_flatten_help_set();
1566 spec.arg_required_else_help = cmd.is_arg_required_else_help_set();
1567 spec.disable_help_flag = cmd.is_disable_help_flag_set();
1568 spec.disable_help_subcommand =
1569 cmd.get_subcommands().next().is_some() && cmd.is_disable_help_subcommand_set();
1570 spec.disable_version_flag = (cmd.get_version().is_some()
1571 || cmd.get_long_version().is_some())
1572 && cmd.is_disable_version_flag_set();
1573 spec.dont_delimit_trailing_values = cmd.is_dont_delimit_trailing_values_set();
1574 spec.args_override_self = cmd.is_args_override_self();
1575 spec.subcommand_negates_reqs = cmd.is_subcommand_negates_reqs_set();
1576 spec.args_conflicts_with_subcommands = cmd.is_args_conflicts_with_subcommands_set();
1577 spec.subcommand_precedence_over_arg = cmd.is_subcommand_precedence_over_arg_set();
1578 spec.allow_missing_positional = cmd.is_allow_missing_positional_set();
1579 for subcmd in cmd.get_subcommands() {
1580 let mut scmd: SpecCommand = subcmd.into();
1581 scmd.name = subcmd.get_name().to_string();
1582 scmd.display_order = Some(subcmd.get_display_order());
1583 spec.subcommands.insert(scmd.name.clone(), scmd);
1584 }
1585 if spec
1589 .subcommands
1590 .iter()
1591 .all(|(_, subcommand)| subcommand.display_order == Some(999))
1592 {
1593 for (_, subcommand) in &mut spec.subcommands {
1594 subcommand.display_order = None;
1595 }
1596 }
1597 spec
1598 }
1599}
1600
1601#[cfg(feature = "clap")]
1602impl From<clap::Command> for Spec {
1603 fn from(cmd: clap::Command) -> Self {
1604 (&cmd).into()
1605 }
1606}
1607
1608#[cfg(test)]
1609mod tests {
1610 use crate::spec::effect::SpecCommandEffect;
1611 use crate::Spec;
1612 use insta::assert_snapshot;
1613
1614 #[test]
1615 fn test_effect_prop_and_child_node() {
1616 let spec = Spec::parse(
1617 &Default::default(),
1618 r#"
1619bin "mise"
1620cmd "ls" effect="read"
1621cmd "use" effect="write"
1622cmd "uninstall" {
1623 effect "destructive"
1624}
1625cmd "version"
1626 "#,
1627 )
1628 .unwrap();
1629
1630 let cmds = &spec.cmd.subcommands;
1631 assert_eq!(cmds["ls"].effect, Some(SpecCommandEffect::Read));
1632 assert_eq!(cmds["use"].effect, Some(SpecCommandEffect::Write));
1633 assert_eq!(
1634 cmds["uninstall"].effect,
1635 Some(SpecCommandEffect::Destructive)
1636 );
1637 assert_eq!(cmds["version"].effect, None);
1639 }
1640
1641 #[test]
1642 fn test_effect_is_not_inherited_by_subcommands() {
1643 let spec = Spec::parse(
1644 &Default::default(),
1645 r#"
1646bin "git"
1647cmd "remote" effect="read" {
1648 cmd "add" effect="write"
1649 cmd "show"
1650}
1651 "#,
1652 )
1653 .unwrap();
1654
1655 let remote = &spec.cmd.subcommands["remote"];
1656 assert_eq!(remote.effect, Some(SpecCommandEffect::Read));
1657 assert_eq!(
1658 remote.subcommands["add"].effect,
1659 Some(SpecCommandEffect::Write)
1660 );
1661 assert_eq!(remote.subcommands["show"].effect, None);
1662 }
1663
1664 #[test]
1665 fn test_effect_roundtrips_through_kdl() {
1666 let spec = Spec::parse(
1667 &Default::default(),
1668 r#"
1669bin "mise"
1670cmd "ls" effect="read"
1671cmd "uninstall" effect="destructive"
1672 "#,
1673 )
1674 .unwrap();
1675
1676 assert_snapshot!(spec, @r#"
1677 name mise
1678 bin mise
1679 cmd ls effect=read
1680 cmd uninstall effect=destructive
1681 "#);
1682 }
1683
1684 #[test]
1688 fn test_effect_survives_merge() {
1689 let cmd_with = |src: &str| {
1690 Spec::parse(&Default::default(), src)
1691 .unwrap()
1692 .cmd
1693 .subcommands["uninstall"]
1694 .clone()
1695 };
1696
1697 let declared = cmd_with(r#"cmd "uninstall" effect="destructive""#);
1698 let silent = cmd_with(r#"cmd "uninstall" help="Remove a tool""#);
1699 let contradicting = cmd_with(r#"cmd "uninstall" effect="write""#);
1700
1701 let mut cmd = declared.clone();
1702 cmd.merge(silent);
1703 assert_eq!(cmd.effect, Some(SpecCommandEffect::Destructive));
1704
1705 let mut cmd = declared;
1706 cmd.merge(contradicting);
1707 assert_eq!(cmd.effect, Some(SpecCommandEffect::Write));
1708 }
1709
1710 #[test]
1711 fn test_unknown_effect_is_an_error() {
1712 let err = Spec::parse(
1713 &Default::default(),
1714 r#"
1715bin "mise"
1716cmd "ls" effect="readonly"
1717 "#,
1718 )
1719 .unwrap_err();
1720 assert!(
1721 err.to_string().contains("Invalid usage config"),
1722 "unexpected error: {err}"
1723 );
1724 }
1725}
1726
1727#[cfg(test)]
1728mod merge_tests {
1729 use crate::Spec;
1730
1731 fn uninstall(src: &str) -> crate::SpecCommand {
1732 Spec::parse(&Default::default(), src)
1733 .unwrap()
1734 .cmd
1735 .subcommands["uninstall"]
1736 .clone()
1737 }
1738
1739 #[test]
1742 fn test_deprecated_survives_merge() {
1743 let declared = uninstall(r#"cmd "uninstall" deprecated="use `remove`""#);
1744 let silent = uninstall(r#"cmd "uninstall" help="Remove a tool""#);
1745 let contradicting = uninstall(r#"cmd "uninstall" deprecated="gone in v3""#);
1746
1747 let mut cmd = declared.clone();
1748 cmd.merge(silent);
1749 assert_eq!(cmd.deprecated.as_deref(), Some("use `remove`"));
1750
1751 let mut cmd = declared;
1752 cmd.merge(contradicting);
1753 assert_eq!(cmd.deprecated.as_deref(), Some("gone in v3"));
1754 }
1755
1756 #[test]
1757 fn mounted_flags_replace_outputs_and_their_selector() {
1758 let mut mounting = uninstall(
1759 r#"cmd "uninstall" { flag "--format <FORMAT>"; output "json" framing="json"; select "--format" }"#,
1760 );
1761 let mounted = uninstall(r#"cmd "uninstall" { flag "--quiet" }"#);
1762
1763 mounting.merge(mounted);
1764
1765 assert!(mounting.outputs.is_empty());
1766 assert!(mounting.select.is_none());
1767 }
1768}
1769
1770#[cfg(test)]
1771mod roundtrip_tests {
1772 use crate::Spec;
1773
1774 #[test]
1782 fn test_spec_survives_a_kdl_roundtrip() {
1783 let src = r#"
1784name "My CLI"
1785bin "mycli"
1786about "does things"
1787version "1.0.0"
1788author "nobody"
1789license "MIT"
1790
1791flag "-v --verbose" help="Verbose logging" global=#true count=#true
1792arg "<dir>" help="Directory to use"
1793
1794cmd "install" help="Install a package" subcommand_required=#false {
1795 alias "i"
1796 alias "add" hide=#true
1797 long_help "The long help for install"
1798 help_md "The **markdown** help for install"
1799 before_help "before"
1800 before_long_help "The long before-help for install"
1801 before_help_md "The **markdown** before-help for install"
1802 after_help "after"
1803 after_long_help "The long after-help for install"
1804 after_help_md "The **markdown** after-help for install"
1805 arg "<pkg>" help="Package to install"
1806 arg "[dest]" effect="write"
1807 flag "-f --force" help="Overwrite"
1808 flag "--purge" effect="destructive" overrides="-f" required_unless="--keep"
1809 flag "--format <FMT>" help="Output format"
1810 complete "pkg" run="mycli list --available" descriptions=#true
1811 example "mycli install foo" header="Install foo" help="Installs foo" lang="sh"
1812 example "mycli install bar"
1813 heading "Output" help="Formats are stable across releases."
1814 output "human" default=#true help="A progress log"
1815 output "json" framing="json" help="One report object" {
1816 schema "{\n \"type\": \"object\"\n}"
1817 }
1818 select "--format"
1819 exit_code 0 "installed"
1820 exit_code 1 "the package was not found"
1821 cmd "from" help="Install from a source" {
1822 arg "<src>"
1823 }
1824}
1825cmd "wrapped" help="Wraps another CLI" {
1826 mount run="mycli plugin usage-spec"
1827}
1828cmd "remove" help="Remove a package" deprecated="use `uninstall`" effect="destructive"
1829cmd "run" restart_token=":::" help="Run tasks"
1830cmd "exec" external_subcommand=#true help="Run an external command"
1831cmd "hidden" hide=#true
1832 "#;
1833
1834 let original = Spec::parse(&Default::default(), src).unwrap();
1835 let reparsed = Spec::parse(&Default::default(), &original.to_string()).unwrap();
1836
1837 let original = serde_json::to_value(&original).unwrap();
1838 let reparsed = serde_json::to_value(&reparsed).unwrap();
1839 pretty_assertions::assert_eq!(original, reparsed);
1840
1841 let install = &original["cmd"]["subcommands"]["install"];
1844 let purge = install["flags"]
1845 .as_array()
1846 .unwrap()
1847 .iter()
1848 .find(|flag| flag["name"] == "purge")
1849 .unwrap();
1850 assert_eq!(purge["overrides"], serde_json::json!(["-f"]));
1851 assert_eq!(purge["required_unless"], serde_json::json!(["--keep"]));
1852 for key in [
1853 "help_long",
1854 "help_md",
1855 "before_help",
1856 "before_help_long",
1857 "before_help_md",
1858 "after_help",
1859 "after_help_long",
1860 "after_help_md",
1861 "deprecated",
1862 "effect",
1863 "restart_token",
1864 "examples",
1865 "headings",
1866 "complete",
1867 "mounts",
1868 "aliases",
1869 "hidden_aliases",
1870 "outputs",
1871 "select",
1872 "exit_codes",
1873 ] {
1874 let populated = match key {
1875 "deprecated" | "effect" => {
1877 original["cmd"]["subcommands"]["remove"].get(key).is_some()
1878 }
1879 "restart_token" => original["cmd"]["subcommands"]["run"].get(key).is_some(),
1880 "mounts" => !original["cmd"]["subcommands"]["wrapped"]["mounts"]
1881 .as_array()
1882 .unwrap()
1883 .is_empty(),
1884 _ => match install.get(key) {
1885 Some(serde_json::Value::Array(a)) => !a.is_empty(),
1886 Some(serde_json::Value::Object(o)) => !o.is_empty(),
1887 Some(_) => true,
1888 None => false,
1889 },
1890 };
1891 assert!(populated, "fixture does not exercise `{key}`");
1892 }
1893
1894 assert!(
1897 install["flags"]
1898 .as_array()
1899 .unwrap()
1900 .iter()
1901 .any(|f| f.get("effect").is_some()),
1902 "fixture does not exercise a flag-level `effect`"
1903 );
1904 assert!(
1905 install["args"]
1906 .as_array()
1907 .unwrap()
1908 .iter()
1909 .any(|a| a.get("effect").is_some()),
1910 "fixture does not exercise an arg-level `effect`"
1911 );
1912 }
1913 #[cfg(feature = "clap")]
1914 #[test]
1915 fn a_clap_command_says_what_clap_would_do_with_an_unknown_flag() {
1916 use super::{SpecCommand, UnknownFlags};
1917
1918 let plain = clap::Command::new("build")
1922 .arg(clap::Arg::new("target").required(false))
1923 .arg(clap::Arg::new("force").long("force").num_args(0));
1924 let spec: SpecCommand = (&plain).into();
1925 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
1926
1927 let wrapper = clap::Command::new("run").arg(
1930 clap::Arg::new("args")
1931 .num_args(0..)
1932 .allow_hyphen_values(true),
1933 );
1934 let spec: SpecCommand = (&wrapper).into();
1935 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
1936
1937 let trailing = clap::Command::new("exec").arg(
1939 clap::Arg::new("cmd")
1940 .num_args(0..)
1941 .trailing_var_arg(true)
1942 .allow_hyphen_values(true),
1943 );
1944 let spec: SpecCommand = (&trailing).into();
1945 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
1946
1947 let external = clap::Command::new("x").allow_external_subcommands(true);
1950 let spec: SpecCommand = (&external).into();
1951 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
1952 assert!(spec.external_subcommand);
1953 }
1954
1955 #[cfg(feature = "clap")]
1956 #[test]
1957 fn the_decision_survives_being_written_and_read_back() {
1958 use super::SpecCommand;
1959
1960 let plain = clap::Command::new("build").arg(clap::Arg::new("target").required(false));
1963 let spec: SpecCommand = (&plain).into();
1964 let node: kdl::KdlNode = (&spec).into();
1965 let kdl = node.to_string();
1966 assert!(kdl.contains("unknown_flags=error"), "{kdl}");
1967 }
1968
1969 #[cfg(feature = "clap")]
1970 #[test]
1971 fn the_clap_bridge_preserves_args_override_self() {
1972 use super::SpecCommand;
1973
1974 let strict: SpecCommand = (&clap::Command::new("strict")).into();
1975 assert!(!strict.args_override_self, "clap is strict by default");
1976
1977 let permissive: SpecCommand =
1978 (&clap::Command::new("permissive").args_override_self(true)).into();
1979 assert!(permissive.args_override_self);
1980
1981 let node: kdl::KdlNode = (&strict).into();
1982 assert!(node.to_string().contains("args_override_self=#false"));
1983 }
1984
1985 #[cfg(feature = "clap")]
1986 #[test]
1987 fn the_clap_bridge_preserves_subcommand_presentation() {
1988 use super::SpecCommand;
1989
1990 let spec: SpecCommand = (&clap::Command::new("ex")
1991 .subcommand(clap::Command::new("run"))
1992 .subcommand_help_heading("Actions")
1993 .subcommand_value_name("ACTION"))
1994 .into();
1995 assert_eq!(spec.subcommand_help_heading.as_deref(), Some("Actions"));
1996 assert_eq!(spec.subcommand_value_name.as_deref(), Some("ACTION"));
1997 let node: kdl::KdlNode = (&spec).into();
1998 let kdl = node.to_string();
1999 assert!(kdl.contains("subcommand_help_heading=Actions"), "{kdl}");
2000 assert!(kdl.contains("subcommand_value_name=ACTION"), "{kdl}");
2001 }
2002
2003 #[cfg(feature = "clap")]
2004 #[test]
2005 fn the_clap_bridge_preserves_subcommand_negates_requirements() {
2006 use super::SpecCommand;
2007
2008 let spec: SpecCommand = (&clap::Command::new("ex").subcommand_negates_reqs(true)).into();
2009 assert!(spec.subcommand_negates_reqs);
2010 let node: kdl::KdlNode = (&spec).into();
2011 assert!(node.to_string().contains("subcommand_negates_reqs=#true"));
2012 }
2013
2014 #[cfg(feature = "clap")]
2015 #[test]
2016 fn the_clap_bridge_preserves_argument_subcommand_conflicts() {
2017 use super::SpecCommand;
2018
2019 let spec: SpecCommand =
2020 (&clap::Command::new("ex").args_conflicts_with_subcommands(true)).into();
2021 assert!(spec.args_conflicts_with_subcommands);
2022 let node: kdl::KdlNode = (&spec).into();
2023 assert!(node
2024 .to_string()
2025 .contains("args_conflicts_with_subcommands=#true"));
2026 }
2027
2028 #[cfg(feature = "clap")]
2029 #[test]
2030 fn the_clap_bridge_preserves_allow_missing_positional() {
2031 use super::SpecCommand;
2032
2033 let spec: SpecCommand = (&clap::Command::new("ex").allow_missing_positional(true)).into();
2034 assert!(spec.allow_missing_positional);
2035 let node: kdl::KdlNode = (&spec).into();
2036 assert!(node.to_string().contains("allow_missing_positional=#true"));
2037 }
2038}