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::flagset::SpecUse;
10use crate::spec::group::SpecGroup;
11use crate::spec::helpers::{string_entry, NodeHelper};
12use crate::spec::is_false;
13use crate::spec::mount::SpecMount;
14use crate::spec::unknown_flags::UnknownFlags;
15use crate::{Spec, SpecArg, SpecComplete, SpecFlag};
16use indexmap::IndexMap;
17use itertools::Itertools;
18use kdl::{KdlDocument, KdlEntry, KdlNode};
19use serde::Serialize;
20
21#[derive(Debug, Serialize, Clone)]
40pub struct SpecCommand {
41 pub full_cmd: Vec<String>,
43 pub usage: String,
45 pub subcommands: IndexMap<String, SpecCommand>,
47 pub args: Vec<SpecArg>,
49 pub flags: Vec<SpecFlag>,
51 #[serde(skip)]
58 pub(crate) uses: Vec<SpecUse>,
59 pub mounts: Vec<SpecMount>,
61 #[serde(skip_serializing_if = "Vec::is_empty")]
66 pub groups: Vec<SpecGroup>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub deprecated: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub deprecated_warn_at: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub deprecated_remove_at: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
79 pub effect: Option<SpecCommandEffect>,
80 pub unknown_flags: Option<UnknownFlags>,
88 pub hide: bool,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub help_heading: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub display_order: Option<usize>,
96 #[serde(skip)]
106 pub mounted: bool,
107 #[serde(skip)]
114 pub flags_from_mount: bool,
115 #[serde(skip_serializing_if = "is_false")]
117 pub subcommand_required: bool,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub subcommand_help_heading: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub subcommand_value_name: Option<String>,
124 #[serde(skip_serializing_if = "is_false")]
126 pub next_line_help: bool,
127 #[serde(skip_serializing_if = "is_false")]
129 pub flatten_help: bool,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub term_width: Option<usize>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub max_term_width: Option<usize>,
136 #[serde(skip_serializing_if = "is_false")]
143 pub external_subcommand: bool,
144 #[serde(skip_serializing_if = "is_false")]
146 pub arg_required_else_help: bool,
147 #[serde(skip_serializing_if = "is_false")]
148 pub disable_help_flag: bool,
149 #[serde(skip_serializing_if = "is_false")]
150 pub disable_help_subcommand: bool,
151 #[serde(skip_serializing_if = "is_false")]
152 pub disable_version_flag: bool,
153 #[serde(skip_serializing_if = "is_false")]
155 pub dont_delimit_trailing_values: bool,
156 pub args_override_self: bool,
159 #[serde(skip_serializing_if = "is_false")]
161 pub subcommand_negates_reqs: bool,
162 #[serde(skip_serializing_if = "is_false")]
164 pub args_conflicts_with_subcommands: bool,
165 #[serde(skip_serializing_if = "is_false")]
166 pub subcommand_precedence_over_arg: bool,
167 #[serde(skip_serializing_if = "is_false")]
169 pub allow_missing_positional: bool,
170 #[serde(skip_serializing_if = "Option::is_none")]
173 pub restart_token: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub help: Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub help_long: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub help_md: Option<String>,
183 pub name: String,
185 pub aliases: Vec<String>,
187 pub hidden_aliases: Vec<String>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub before_help: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub before_help_long: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub before_help_md: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub after_help: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub after_help_long: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub after_help_md: Option<String>,
207 pub examples: Vec<SpecExample>,
209 #[serde(skip_serializing_if = "IndexMap::is_empty")]
211 pub complete: IndexMap<String, SpecComplete>,
212
213 #[serde(skip)]
218 pub(crate) subcommand_lookup: OnceLock<HashMap<String, String>>,
219}
220
221impl Default for SpecCommand {
222 fn default() -> Self {
223 Self {
224 full_cmd: vec![],
225 usage: "".to_string(),
226 subcommands: IndexMap::new(),
227 args: vec![],
228 flags: vec![],
229 uses: vec![],
230 mounts: vec![],
231 groups: vec![],
232 deprecated: None,
233 deprecated_warn_at: None,
234 deprecated_remove_at: None,
235 effect: None,
236 unknown_flags: None,
237 hide: false,
238 help_heading: None,
239 display_order: None,
240 mounted: false,
241 flags_from_mount: false,
242 subcommand_required: false,
243 subcommand_help_heading: None,
244 subcommand_value_name: None,
245 next_line_help: false,
246 flatten_help: false,
247 term_width: None,
248 max_term_width: None,
249 external_subcommand: false,
250 arg_required_else_help: false,
251 disable_help_flag: false,
252 disable_help_subcommand: false,
253 disable_version_flag: false,
254 dont_delimit_trailing_values: false,
255 args_override_self: true,
256 subcommand_negates_reqs: false,
257 args_conflicts_with_subcommands: false,
258 subcommand_precedence_over_arg: false,
259 allow_missing_positional: false,
260 restart_token: None,
261 help: None,
262 help_long: None,
263 help_md: None,
264 name: "".to_string(),
265 aliases: vec![],
266 hidden_aliases: vec![],
267 before_help: None,
268 before_help_long: None,
269 before_help_md: None,
270 after_help: None,
271 after_help_long: None,
272 after_help_md: None,
273 examples: vec![],
274 subcommand_lookup: OnceLock::new(),
275 complete: IndexMap::new(),
276 }
277 }
278}
279
280#[derive(Debug, Default, Serialize, Clone)]
281#[non_exhaustive]
282pub struct SpecExample {
283 pub code: String,
284 pub header: Option<String>,
285 pub help: Option<String>,
286 pub lang: String,
287}
288
289impl SpecExample {
290 pub fn new(code: impl Into<String>) -> Self {
292 Self {
293 code: code.into(),
294 ..Default::default()
295 }
296 }
297
298 pub fn header(mut self, header: impl Into<String>) -> Self {
300 self.header = Some(header.into());
301 self
302 }
303
304 pub fn help(mut self, help: impl Into<String>) -> Self {
306 self.help = Some(help.into());
307 self
308 }
309
310 pub fn lang(mut self, lang: impl Into<String>) -> Self {
312 self.lang = lang.into();
313 self
314 }
315}
316
317impl From<&SpecExample> for KdlNode {
318 fn from(example: &SpecExample) -> KdlNode {
319 let mut node = KdlNode::new("example");
320 node.push(string_entry(None, &example.code));
321 if let Some(header) = &example.header {
322 node.push(string_entry(Some("header"), header));
323 }
324 if let Some(help) = &example.help {
325 node.push(string_entry(Some("help"), help));
326 }
327 if !example.lang.is_empty() {
328 node.push(string_entry(Some("lang"), &example.lang));
329 }
330 node
331 }
332}
333
334impl SpecCommand {
335 pub fn builder() -> SpecCommandBuilder {
337 SpecCommandBuilder::new()
338 }
339
340 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
341 node.ensure_arg_len(1..=1)?;
342 let mut cmd = Self {
343 name: node.arg(0)?.ensure_string()?.to_string(),
344 ..Default::default()
345 };
346 for (k, v) in node.props() {
347 match k {
348 "help" => cmd.help = Some(v.ensure_string()?),
349 "long_help" => cmd.help_long = Some(v.ensure_string()?),
350 "help_long" => cmd.help_long = Some(v.ensure_string()?),
351 "help_md" => cmd.help_md = Some(v.ensure_string()?),
352 "before_help" => cmd.before_help = Some(v.ensure_string()?),
353 "before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
354 "before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
355 "before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
356 "after_help" => cmd.after_help = Some(v.ensure_string()?),
357 "after_long_help" => {
358 cmd.after_help_long = Some(v.ensure_string()?);
359 }
360 "after_help_long" => {
361 cmd.after_help_long = Some(v.ensure_string()?);
362 }
363 "after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
364 "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
365 "subcommand_help_heading" => cmd.subcommand_help_heading = Some(v.ensure_string()?),
366 "subcommand_value_name" => cmd.subcommand_value_name = Some(v.ensure_string()?),
367 "next_line_help" => cmd.next_line_help = v.ensure_bool()?,
368 "flatten_help" => cmd.flatten_help = v.ensure_bool()?,
369 "term_width" => cmd.term_width = Some(v.ensure_usize()?),
370 "max_term_width" => cmd.max_term_width = Some(v.ensure_usize()?),
371 "external_subcommand" => cmd.external_subcommand = v.ensure_bool()?,
372 "arg_required_else_help" => cmd.arg_required_else_help = v.ensure_bool()?,
373 "disable_help_flag" => cmd.disable_help_flag = v.ensure_bool()?,
374 "disable_help_subcommand" => cmd.disable_help_subcommand = v.ensure_bool()?,
375 "disable_version_flag" => cmd.disable_version_flag = v.ensure_bool()?,
376 "dont_delimit_trailing_values" => {
377 cmd.dont_delimit_trailing_values = v.ensure_bool()?
378 }
379 "args_override_self" => cmd.args_override_self = v.ensure_bool()?,
380 "subcommand_negates_reqs" => cmd.subcommand_negates_reqs = v.ensure_bool()?,
381 "args_conflicts_with_subcommands" => {
382 cmd.args_conflicts_with_subcommands = v.ensure_bool()?
383 }
384 "subcommand_precedence_over_arg" => {
385 cmd.subcommand_precedence_over_arg = v.ensure_bool()?
386 }
387 "allow_missing_positional" => cmd.allow_missing_positional = v.ensure_bool()?,
388 "hide" => cmd.hide = v.ensure_bool()?,
389 "help_heading" => cmd.help_heading = Some(v.ensure_string()?),
390 "display_order" => cmd.display_order = Some(v.ensure_usize()?),
391 "unknown_flags" => {
392 let raw = v.ensure_string()?;
393 match raw.parse() {
394 Ok(mode) => cmd.unknown_flags = Some(mode),
395 Err(_) => bail_parse!(
396 ctx,
397 v.entry.span(),
398 "unsupported unknown_flags {raw}, expected one of: {}",
399 crate::spec::unknown_flags::UNKNOWN_FLAGS_VALUES
400 ),
401 }
402 }
403 "effect" => {
404 let raw = v.ensure_string()?;
405 match raw.parse() {
406 Ok(effect) => cmd.effect = Some(effect),
407 Err(_) => bail_parse!(
408 ctx,
409 v.entry.span(),
410 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
411 ),
412 }
413 }
414 "restart_token" => cmd.restart_token = Some(v.ensure_string()?),
415 "deprecated" => {
416 cmd.deprecated = match v.value.as_bool() {
417 Some(true) => Some("deprecated".to_string()),
418 Some(false) => None,
419 None => Some(v.ensure_string()?),
420 }
421 }
422 "deprecated_warn_at" => cmd.deprecated_warn_at = Some(v.ensure_string()?),
423 "deprecated_remove_at" => cmd.deprecated_remove_at = Some(v.ensure_string()?),
424 k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
425 }
426 }
427 for child in node.children() {
428 match child.name() {
429 "flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
430 "use" => {
431 let at = cmd.flags.len();
432 cmd.uses.push(SpecUse::parse(ctx, &child, at)?);
433 }
434 "arg" => {
435 let arg = SpecArg::parse(ctx, &child)?;
436 if arg.delimiter.is_some() && !arg.var {
439 bail_parse!(
440 ctx,
441 child.node.name().span(),
442 "argument <{}> has a delimiter and holds one value; add \
443 `var=#true` for the values it splits into",
444 arg.name
445 );
446 }
447 cmd.args.push(arg);
448 }
449 "mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
450 "group" => cmd.groups.push(SpecGroup::parse(ctx, &child)?),
451 "cmd" => {
452 let node = SpecCommand::parse(ctx, &child)?;
453 cmd.subcommands.insert(node.name.to_string(), node);
454 }
455 "alias" => {
456 let alias = child
457 .ensure_arg_len(1..)?
458 .args()
459 .map(|e| e.ensure_string())
460 .collect::<Result<Vec<_>, _>>()?;
461 let hide = child
462 .get("hide")
463 .map(|n| n.ensure_bool())
464 .unwrap_or(Ok(false))?;
465 if hide {
466 cmd.hidden_aliases.extend(alias);
467 } else {
468 cmd.aliases.extend(alias);
469 }
470 }
471 "example" => {
472 let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
473 let mut example = SpecExample::new(code.trim().to_string());
474 for (k, v) in child.props() {
475 match k {
476 "header" => example.header = Some(v.ensure_string()?),
477 "help" => example.help = Some(v.ensure_string()?),
478 "lang" => example.lang = v.ensure_string()?,
479 k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
480 }
481 }
482 cmd.examples.push(example);
483 }
484 "help" => {
485 cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
486 }
487 "long_help" => {
488 cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
489 }
490 "help_md" => {
491 cmd.help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
492 }
493 "before_help" => {
494 cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
495 }
496 "before_long_help" => {
497 cmd.before_help_long =
498 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
499 }
500 "before_help_md" => {
501 cmd.before_help_md =
502 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
503 }
504 "after_help" => {
505 cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
506 }
507 "after_long_help" => {
508 cmd.after_help_long =
509 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
510 }
511 "after_help_md" => {
512 cmd.after_help_md = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
513 }
514 "subcommand_required" => {
515 cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
516 }
517 "help_heading" => {
518 cmd.help_heading = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
519 }
520 "subcommand_help_heading" => {
521 cmd.subcommand_help_heading =
522 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
523 }
524 "subcommand_value_name" => {
525 cmd.subcommand_value_name =
526 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
527 }
528 "next_line_help" => {
529 cmd.next_line_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
530 }
531 "flatten_help" => {
532 cmd.flatten_help = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
533 }
534 "term_width" => {
535 cmd.term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
536 }
537 "max_term_width" => {
538 cmd.max_term_width = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_usize()?)
539 }
540 "external_subcommand" => {
541 cmd.external_subcommand = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
542 }
543 "arg_required_else_help" => {
544 cmd.arg_required_else_help =
545 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
546 }
547 "disable_help_flag" => {
548 cmd.disable_help_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
549 }
550 "disable_help_subcommand" => {
551 cmd.disable_help_subcommand =
552 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
553 }
554 "disable_version_flag" => {
555 cmd.disable_version_flag = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
556 }
557 "dont_delimit_trailing_values" => {
558 cmd.dont_delimit_trailing_values =
559 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
560 }
561 "args_override_self" => {
562 cmd.args_override_self = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
563 }
564 "subcommand_negates_reqs" => {
565 cmd.subcommand_negates_reqs =
566 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
567 }
568 "args_conflicts_with_subcommands" => {
569 cmd.args_conflicts_with_subcommands =
570 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
571 }
572 "subcommand_precedence_over_arg" => {
573 cmd.subcommand_precedence_over_arg =
574 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
575 }
576 "allow_missing_positional" => {
577 cmd.allow_missing_positional =
578 child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
579 }
580 "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
581 "effect" => {
582 let arg = child.ensure_arg_len(1..=1)?.arg(0)?;
583 let raw = arg.ensure_string()?;
584 match raw.parse() {
585 Ok(effect) => cmd.effect = Some(effect),
586 Err(_) => bail_parse!(
587 ctx,
588 arg.entry.span(),
589 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
590 ),
591 }
592 }
593 "restart_token" => {
594 cmd.restart_token = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
595 }
596 "deprecated" => {
597 cmd.deprecated = match child.arg(0)?.value.as_bool() {
598 Some(true) => Some("deprecated".to_string()),
599 Some(false) => None,
600 None => Some(child.arg(0)?.ensure_string()?),
601 }
602 }
603 "deprecated_warn_at" => {
604 cmd.deprecated_warn_at = Some(child.arg(0)?.ensure_string()?)
605 }
606 "deprecated_remove_at" => {
607 cmd.deprecated_remove_at = Some(child.arg(0)?.ensure_string()?)
608 }
609 "complete" => {
610 let complete = SpecComplete::parse(ctx, &child)?;
611 cmd.complete.insert(complete.name.clone(), complete);
612 }
613 k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
614 }
615 }
616 Ok(cmd)
617 }
618 pub(crate) fn is_empty(&self) -> bool {
619 self.args.is_empty()
620 && self.flags.is_empty()
621 && self.mounts.is_empty()
622 && self.subcommands.is_empty()
623 }
624 pub fn usage(&self) -> String {
625 self.usage_with_subcommands(true)
626 }
627
628 #[cfg(feature = "docs")]
631 pub(crate) fn usage_without_subcommands(&self) -> String {
632 self.usage_with_subcommands(false)
633 }
634
635 fn usage_with_subcommands(&self, include_subcommands: bool) -> String {
636 let mut usage = self.full_cmd.join(" ");
637 let flags = self.flags.iter().filter(|f| !f.hide).collect_vec();
638 let args = self.args.iter().filter(|a| !a.hide).collect_vec();
639 if !flags.is_empty() {
640 if flags.len() <= 2 {
641 let inlines = flags
642 .iter()
643 .map(|f| {
644 if f.required {
645 format!("<{}>", f.usage())
646 } else {
647 format!("[{}]", f.usage())
648 }
649 })
650 .join(" ");
651 usage = format!("{usage} {inlines}").trim().to_string();
652 } else if flags.iter().any(|f| f.required) {
653 usage = format!("{usage} <FLAGS>");
654 } else {
655 usage = format!("{usage} [FLAGS]");
656 }
657 }
658 if !args.is_empty() {
659 if args.len() <= 2 {
660 let inlines = args.iter().map(|a| a.usage()).join(" ");
661 usage = format!("{usage} {inlines}").trim().to_string();
662 } else if args.iter().any(|a| a.required) {
663 usage = format!("{usage} <ARGS>…");
664 } else {
665 usage = format!("{usage} [ARGS]…");
666 }
667 }
668 if include_subcommands && !self.subcommands.is_empty() {
673 let name = self
674 .subcommand_value_name
675 .as_deref()
676 .unwrap_or("SUBCOMMAND");
677 usage = format!("{usage} <{name}>");
678 }
679 usage.trim().to_string()
680 }
681 pub(crate) fn merge(&mut self, other: Self) {
682 self.subcommand_lookup = OnceLock::new();
687 let Self {
691 name,
692 help,
693 help_long,
694 help_md,
695 before_help,
696 before_help_long,
697 before_help_md,
698 after_help,
699 after_help_long,
700 after_help_md,
701 args,
702 flags,
703 uses,
704 mounts,
705 groups,
706 aliases,
707 hidden_aliases,
708 examples,
709 hide,
710 help_heading,
711 display_order,
712 subcommand_required,
713 subcommand_help_heading,
714 subcommand_value_name,
715 next_line_help,
716 flatten_help,
717 term_width,
718 max_term_width,
719 external_subcommand,
720 arg_required_else_help,
721 disable_help_flag,
722 disable_help_subcommand,
723 disable_version_flag,
724 dont_delimit_trailing_values,
725 args_override_self,
726 subcommand_negates_reqs,
727 args_conflicts_with_subcommands,
728 subcommand_precedence_over_arg,
729 allow_missing_positional,
730 restart_token,
731 subcommands,
732 complete,
733 deprecated,
734 deprecated_warn_at,
735 deprecated_remove_at,
736 effect,
737 unknown_flags,
738 full_cmd: _,
740 usage: _,
741 mounted: _,
742 flags_from_mount: _,
743 subcommand_lookup: _,
744 } = other;
745 if !name.is_empty() {
746 self.name = name;
747 }
748 if help.is_some() {
749 self.help = help;
750 }
751 if help_long.is_some() {
752 self.help_long = help_long;
753 }
754 if help_md.is_some() {
755 self.help_md = help_md;
756 }
757 if before_help.is_some() {
758 self.before_help = before_help;
759 }
760 if before_help_long.is_some() {
761 self.before_help_long = before_help_long;
762 }
763 if before_help_md.is_some() {
764 self.before_help_md = before_help_md;
765 }
766 if after_help.is_some() {
767 self.after_help = after_help;
768 }
769 if after_help_long.is_some() {
770 self.after_help_long = after_help_long;
771 }
772 if after_help_md.is_some() {
773 self.after_help_md = after_help_md;
774 }
775 if !args.is_empty() {
776 self.args = args;
777 }
778 let flags_replaced = !flags.is_empty();
779 if flags_replaced {
780 self.flags = flags;
781 }
782 if flags_replaced || !uses.is_empty() {
791 self.uses = uses;
792 }
793 if !mounts.is_empty() {
794 self.mounts = mounts;
795 }
796 if flags_replaced || !groups.is_empty() {
802 self.groups = groups;
803 }
804 if !aliases.is_empty() {
805 self.aliases = aliases;
806 }
807 if !hidden_aliases.is_empty() {
808 self.hidden_aliases = hidden_aliases;
809 }
810 if !examples.is_empty() {
811 self.examples = examples;
812 }
813 self.hide = hide;
814 if help_heading.is_some() {
815 self.help_heading = help_heading;
816 }
817 if display_order.is_some() {
818 self.display_order = display_order;
819 }
820 self.subcommand_required = subcommand_required;
821 if subcommand_help_heading.is_some() {
822 self.subcommand_help_heading = subcommand_help_heading;
823 }
824 if subcommand_value_name.is_some() {
825 self.subcommand_value_name = subcommand_value_name;
826 }
827 self.next_line_help = next_line_help;
828 self.flatten_help = flatten_help;
829 if term_width.is_some() {
830 self.term_width = term_width;
831 }
832 if max_term_width.is_some() {
833 self.max_term_width = max_term_width;
834 }
835 self.external_subcommand = external_subcommand;
836 self.arg_required_else_help = arg_required_else_help;
837 self.disable_help_flag = disable_help_flag;
838 self.disable_help_subcommand = disable_help_subcommand;
839 self.disable_version_flag = disable_version_flag;
840 self.dont_delimit_trailing_values = dont_delimit_trailing_values;
841 self.args_override_self = args_override_self;
842 self.subcommand_negates_reqs = subcommand_negates_reqs;
843 self.args_conflicts_with_subcommands = args_conflicts_with_subcommands;
844 self.subcommand_precedence_over_arg = subcommand_precedence_over_arg;
845 self.allow_missing_positional = allow_missing_positional;
846 if effect.is_some() {
847 self.effect = effect;
848 }
849 if unknown_flags.is_some() {
850 self.unknown_flags = unknown_flags;
851 }
852 if deprecated.is_some() {
853 self.deprecated = deprecated;
854 }
855 if deprecated_warn_at.is_some() {
856 self.deprecated_warn_at = deprecated_warn_at;
857 }
858 if deprecated_remove_at.is_some() {
859 self.deprecated_remove_at = deprecated_remove_at;
860 }
861 if restart_token.is_some() {
862 self.restart_token = restart_token;
863 }
864 for (name, cmd) in subcommands {
865 self.subcommands.insert(name, cmd);
866 }
867 for (name, complete) in complete {
868 self.complete.insert(name, complete);
869 }
870 }
871
872 pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
873 let mut cmds = vec![];
874 for cmd in self.subcommands.values() {
875 cmds.push(cmd);
876 cmds.extend(cmd.all_subcommands());
877 }
878 cmds
879 }
880
881 pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
882 let sl = self.subcommand_lookup.get_or_init(|| {
883 let mut map = HashMap::new();
884 for name in self.subcommands.keys() {
892 map.insert(name.clone(), name.clone());
893 }
894 for (name, cmd) in &self.subcommands {
895 for alias in cmd.aliases.iter().chain(&cmd.hidden_aliases) {
896 map.entry(alias.clone()).or_insert_with(|| name.clone());
897 }
898 }
899 map
900 });
901 let name = sl.get(name)?;
902 self.subcommands.get(name)
903 }
904
905 pub(crate) fn mount(
906 &mut self,
907 global_flag_args: &[String],
908 injected: Option<&HashMap<String, String>>,
909 ) -> Result<(), UsageErr> {
910 for mount in self.mounts.iter().cloned().collect_vec() {
911 let cmd = if global_flag_args.is_empty() {
912 mount.run.clone()
913 } else {
914 let mut tokens = shell_words::split(&mount.run)
918 .expect("mount command should be valid shell syntax");
919 if !tokens.is_empty() {
920 tokens.splice(1..1, global_flag_args.iter().cloned());
922 }
923 shell_words::join(tokens)
925 };
926 let output = match injected {
927 Some(outputs) => outputs
928 .get(&mount.run)
929 .cloned()
930 .ok_or_else(|| UsageErr::MissingMountOutput(mount.run.clone()))?,
931 None => sh(&cmd)?,
932 };
933 let mut spec: Spec = output.parse()?;
934 if let Some(outputs) = injected {
935 spec.resolve_mount_outputs_at_root(outputs, false)?;
938 }
939 for cmd in spec.cmd.subcommands.values_mut() {
942 cmd.mark_mounted();
943 }
944 self.flags_from_mount |= !spec.cmd.flags.is_empty();
947 self.merge(spec.cmd);
948 }
949 Ok(())
950 }
951
952 pub(crate) fn mark_mounted(&mut self) {
954 self.mounted = true;
955 for cmd in self.subcommands.values_mut() {
956 cmd.mark_mounted();
957 }
958 }
959}
960
961impl From<&SpecCommand> for KdlNode {
962 fn from(cmd: &SpecCommand) -> Self {
963 let SpecCommand {
966 name,
967 hide,
968 help_heading,
969 display_order,
970 subcommand_required,
971 subcommand_help_heading,
972 subcommand_value_name,
973 next_line_help,
974 flatten_help,
975 term_width,
976 max_term_width,
977 external_subcommand,
978 arg_required_else_help,
979 disable_help_flag,
980 disable_help_subcommand,
981 disable_version_flag,
982 dont_delimit_trailing_values,
983 args_override_self,
984 subcommand_negates_reqs,
985 args_conflicts_with_subcommands,
986 subcommand_precedence_over_arg,
987 allow_missing_positional,
988 restart_token,
989 unknown_flags,
990 aliases,
991 hidden_aliases,
992 help,
993 help_long,
994 help_md,
995 before_help,
996 before_help_long,
997 before_help_md,
998 after_help,
999 after_help_long,
1000 after_help_md,
1001 deprecated,
1002 deprecated_warn_at,
1003 deprecated_remove_at,
1004 effect,
1005 flags,
1006 args,
1007 mounts,
1008 groups,
1009 subcommands,
1010 complete,
1011 examples,
1012 uses: _,
1015 full_cmd: _,
1017 usage: _,
1018 mounted: _,
1019 flags_from_mount: _,
1020 subcommand_lookup: _,
1021 } = cmd;
1022 let mut node = Self::new("cmd");
1023 node.entries_mut().push(name.clone().into());
1024 if *hide {
1025 node.entries_mut().push(KdlEntry::new_prop("hide", true));
1026 }
1027 if let Some(heading) = help_heading {
1028 node.entries_mut()
1029 .push(KdlEntry::new_prop("help_heading", heading.clone()));
1030 }
1031 if let Some(order) = display_order {
1032 node.entries_mut()
1033 .push(KdlEntry::new_prop("display_order", *order as i128));
1034 }
1035 if *subcommand_required {
1036 node.entries_mut()
1037 .push(KdlEntry::new_prop("subcommand_required", true));
1038 }
1039 if let Some(heading) = subcommand_help_heading {
1040 node.push(KdlEntry::new_prop(
1041 "subcommand_help_heading",
1042 heading.clone(),
1043 ));
1044 }
1045 if let Some(name) = subcommand_value_name {
1046 node.push(KdlEntry::new_prop("subcommand_value_name", name.clone()));
1047 }
1048 if *next_line_help {
1049 node.push(KdlEntry::new_prop("next_line_help", true));
1050 }
1051 if *flatten_help {
1052 node.push(KdlEntry::new_prop("flatten_help", true));
1053 }
1054 if let Some(width) = term_width {
1055 node.push(KdlEntry::new_prop("term_width", *width as i128));
1056 }
1057 if let Some(width) = max_term_width {
1058 node.push(KdlEntry::new_prop("max_term_width", *width as i128));
1059 }
1060 if *external_subcommand {
1061 node.entries_mut()
1062 .push(KdlEntry::new_prop("external_subcommand", true));
1063 }
1064 if *arg_required_else_help {
1065 node.entries_mut()
1066 .push(KdlEntry::new_prop("arg_required_else_help", true));
1067 }
1068 if *disable_help_flag {
1069 node.push(KdlEntry::new_prop("disable_help_flag", true));
1070 }
1071 if *disable_help_subcommand {
1072 node.push(KdlEntry::new_prop("disable_help_subcommand", true));
1073 }
1074 if *disable_version_flag {
1075 node.push(KdlEntry::new_prop("disable_version_flag", true));
1076 }
1077 if *dont_delimit_trailing_values {
1078 node.entries_mut()
1079 .push(KdlEntry::new_prop("dont_delimit_trailing_values", true));
1080 }
1081 if !*args_override_self {
1082 node.push(KdlEntry::new_prop("args_override_self", false));
1083 }
1084 if *subcommand_negates_reqs {
1085 node.push(KdlEntry::new_prop("subcommand_negates_reqs", true));
1086 }
1087 if *args_conflicts_with_subcommands {
1088 node.push(KdlEntry::new_prop("args_conflicts_with_subcommands", true));
1089 }
1090 if *subcommand_precedence_over_arg {
1091 node.push(KdlEntry::new_prop("subcommand_precedence_over_arg", true));
1092 }
1093 if *allow_missing_positional {
1094 node.push(KdlEntry::new_prop("allow_missing_positional", true));
1095 }
1096 if let Some(restart_token) = &restart_token {
1097 node.entries_mut()
1098 .push(KdlEntry::new_prop("restart_token", restart_token.clone()));
1099 }
1100 if !aliases.is_empty() {
1101 let mut alias_node = KdlNode::new("alias");
1102 for alias in aliases {
1103 alias_node.entries_mut().push(alias.clone().into());
1104 }
1105 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1106 children.nodes_mut().push(alias_node);
1107 }
1108 if !hidden_aliases.is_empty() {
1109 let mut alias_node = KdlNode::new("alias");
1110 for alias in hidden_aliases {
1111 alias_node.entries_mut().push(alias.clone().into());
1112 }
1113 alias_node
1114 .entries_mut()
1115 .push(KdlEntry::new_prop("hide", true));
1116 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1117 children.nodes_mut().push(alias_node);
1118 }
1119 if let Some(help) = &help {
1120 node.entries_mut().push(string_entry(Some("help"), help));
1121 }
1122 if let Some(help) = &help_long {
1123 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1124 let mut node = KdlNode::new("long_help");
1125 node.push(string_entry(None, help));
1126 children.nodes_mut().push(node);
1127 }
1128 if let Some(help) = &help_md {
1129 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1130 let mut node = KdlNode::new("help_md");
1131 node.push(string_entry(None, help));
1132 children.nodes_mut().push(node);
1133 }
1134 if let Some(help) = &before_help {
1135 node.entries_mut()
1136 .push(string_entry(Some("before_help"), help));
1137 }
1138 if let Some(help) = &before_help_long {
1139 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1140 let mut node = KdlNode::new("before_long_help");
1141 node.push(string_entry(None, help));
1142 children.nodes_mut().push(node);
1143 }
1144 if let Some(help) = &before_help_md {
1145 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1146 let mut node = KdlNode::new("before_help_md");
1147 node.push(string_entry(None, help));
1148 children.nodes_mut().push(node);
1149 }
1150 if let Some(help) = &after_help {
1151 node.entries_mut()
1152 .push(string_entry(Some("after_help"), help));
1153 }
1154 if let Some(help) = &after_help_long {
1155 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1156 let mut node = KdlNode::new("after_long_help");
1157 node.push(string_entry(None, help));
1158 children.nodes_mut().push(node);
1159 }
1160 if let Some(help) = &after_help_md {
1161 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1162 let mut node = KdlNode::new("after_help_md");
1163 node.push(string_entry(None, help));
1164 children.nodes_mut().push(node);
1165 }
1166 if let Some(deprecated) = &deprecated {
1167 node.entries_mut()
1168 .push(string_entry(Some("deprecated"), deprecated));
1169 }
1170 if let Some(at) = deprecated_warn_at {
1171 node.push(string_entry(Some("deprecated_warn_at"), at));
1172 }
1173 if let Some(at) = deprecated_remove_at {
1174 node.push(string_entry(Some("deprecated_remove_at"), at));
1175 }
1176 if let Some(effect) = effect {
1177 node.entries_mut()
1178 .push(string_entry(Some("effect"), effect.as_str()));
1179 }
1180 if let Some(unknown_flags) = unknown_flags {
1181 node.entries_mut()
1182 .push(string_entry(Some("unknown_flags"), unknown_flags.as_str()));
1183 }
1184 for flag in flags {
1185 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1186 children.nodes_mut().push(flag.into());
1187 }
1188 for arg in args {
1189 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1190 children.nodes_mut().push(arg.into());
1191 }
1192 for mount in mounts {
1193 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1194 children.nodes_mut().push(mount.into());
1195 }
1196 for group in groups {
1199 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1200 children.nodes_mut().push(group.into());
1201 }
1202 for cmd in subcommands.values() {
1203 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1204 children.nodes_mut().push(cmd.into());
1205 }
1206 for example in examples {
1207 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1208 children.nodes_mut().push(example.into());
1209 }
1210 for complete in complete.values() {
1211 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
1212 children.nodes_mut().push(complete.into());
1213 }
1214 node
1215 }
1216}
1217
1218#[cfg(feature = "clap")]
1219impl From<&clap::Command> for SpecCommand {
1220 fn from(cmd: &clap::Command) -> Self {
1221 let mut spec = Self {
1222 name: cmd.get_name().to_string(),
1223 hide: cmd.is_hide_set(),
1224 help: cmd.get_about().map(|s| s.to_string()),
1225 help_long: cmd.get_long_about().map(|s| s.to_string()),
1226 before_help: cmd.get_before_help().map(|s| s.to_string()),
1227 before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
1228 after_help: cmd.get_after_help().map(|s| s.to_string()),
1229 after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
1230 ..Default::default()
1231 };
1232 spec.external_subcommand = cmd.is_allow_external_subcommands_set();
1252 let forwards = cmd
1253 .get_arguments()
1254 .any(|arg| arg.is_allow_hyphen_values_set() || arg.is_trailing_var_arg_set());
1255 spec.unknown_flags = Some(if forwards {
1256 UnknownFlags::Value
1257 } else {
1258 UnknownFlags::Error
1259 });
1260
1261 for alias in cmd.get_visible_aliases() {
1262 spec.aliases.push(alias.to_string());
1263 }
1264 for alias in cmd.get_all_aliases() {
1265 if spec.aliases.contains(&alias.to_string()) {
1266 continue;
1267 }
1268 spec.hidden_aliases.push(alias.to_string());
1269 }
1270 for arg in cmd.get_arguments() {
1271 let complete_type = crate::spec::arg::value_hint_type(arg.get_value_hint());
1272 let conflicts: Vec<String> = cmd
1273 .get_arg_conflicts_with(arg)
1274 .iter()
1275 .filter_map(|other| match (other.get_long(), other.get_short()) {
1276 (Some(long), _) => Some(format!("--{long}")),
1277 (None, Some(short)) => Some(format!("-{short}")),
1278 (None, None) if other.is_positional() => Some(SpecArg::from(*other).name),
1279 (None, None) => None,
1280 })
1281 .collect();
1282 if arg.is_positional() {
1283 let mut positional: SpecArg = arg.into();
1284 positional.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
1285 positional.conflicts = conflicts;
1286 if let Some(type_) = complete_type {
1287 let name = positional.name.to_lowercase();
1288 spec.complete.insert(
1289 name.clone(),
1290 SpecComplete {
1291 name,
1292 type_: Some(type_.to_string()),
1293 ..Default::default()
1294 },
1295 );
1296 }
1297 spec.args.push(positional)
1298 } else {
1299 let mut flag: SpecFlag = arg.into();
1300 if let Some(value) = &mut flag.arg {
1301 value.allow_negative_numbers |= cmd.is_allow_negative_numbers_set();
1302 }
1303 flag.conflicts = conflicts;
1311 if let (Some(type_), Some(value)) = (complete_type, flag.arg.as_ref()) {
1312 let name = value.name.to_lowercase();
1313 spec.complete.insert(
1314 name.clone(),
1315 SpecComplete {
1316 name,
1317 type_: Some(type_.to_string()),
1318 ..Default::default()
1319 },
1320 );
1321 }
1322 spec.flags.push(flag)
1323 }
1324 }
1325 if spec
1330 .args
1331 .windows(2)
1332 .all(|pair| pair[0].display_order <= pair[1].display_order)
1333 {
1334 for arg in &mut spec.args {
1335 arg.display_order = None;
1336 }
1337 }
1338 if spec
1339 .flags
1340 .windows(2)
1341 .all(|pair| pair[0].display_order <= pair[1].display_order)
1342 {
1343 for flag in &mut spec.flags {
1344 flag.display_order = None;
1345 }
1346 }
1347 for group in cmd.get_groups() {
1355 let members: Vec<String> = group
1356 .get_args()
1357 .filter_map(|id| cmd.get_arguments().find(|arg| arg.get_id() == id))
1358 .filter_map(|arg| match (arg.get_long(), arg.get_short()) {
1359 (Some(long), _) => Some(format!("--{long}")),
1360 (None, Some(short)) => Some(format!("-{short}")),
1361 (None, None) if arg.is_positional() => Some(SpecArg::from(arg).name),
1362 (None, None) => None,
1363 })
1364 .collect();
1365 if members.len() < 2 {
1369 continue;
1370 }
1371 let required = group.is_required_set();
1381 let multiple = group.clone().is_multiple();
1385 if multiple && !required {
1386 continue;
1387 }
1388 let mut spec_group = SpecGroup::new(group.get_id().as_str(), members);
1389 spec_group.required = required;
1390 spec_group.multiple = multiple;
1391 spec.groups.push(spec_group);
1392 }
1393 spec.subcommand_required = cmd.is_subcommand_required_set();
1394 spec.subcommand_help_heading = cmd.get_subcommand_help_heading().map(str::to_string);
1395 spec.subcommand_value_name = cmd.get_subcommand_value_name().map(str::to_string);
1396 spec.next_line_help = cmd.is_next_line_help_set();
1397 spec.flatten_help = cmd.is_flatten_help_set();
1398 spec.arg_required_else_help = cmd.is_arg_required_else_help_set();
1399 spec.disable_help_flag = cmd.is_disable_help_flag_set();
1400 spec.disable_help_subcommand =
1401 cmd.get_subcommands().next().is_some() && cmd.is_disable_help_subcommand_set();
1402 spec.disable_version_flag = (cmd.get_version().is_some()
1403 || cmd.get_long_version().is_some())
1404 && cmd.is_disable_version_flag_set();
1405 spec.dont_delimit_trailing_values = cmd.is_dont_delimit_trailing_values_set();
1406 spec.args_override_self = cmd.is_args_override_self();
1407 spec.subcommand_negates_reqs = cmd.is_subcommand_negates_reqs_set();
1408 spec.args_conflicts_with_subcommands = cmd.is_args_conflicts_with_subcommands_set();
1409 spec.subcommand_precedence_over_arg = cmd.is_subcommand_precedence_over_arg_set();
1410 spec.allow_missing_positional = cmd.is_allow_missing_positional_set();
1411 for subcmd in cmd.get_subcommands() {
1412 let mut scmd: SpecCommand = subcmd.into();
1413 scmd.name = subcmd.get_name().to_string();
1414 scmd.display_order = Some(subcmd.get_display_order());
1415 spec.subcommands.insert(scmd.name.clone(), scmd);
1416 }
1417 if spec
1421 .subcommands
1422 .iter()
1423 .all(|(_, subcommand)| subcommand.display_order == Some(999))
1424 {
1425 for (_, subcommand) in &mut spec.subcommands {
1426 subcommand.display_order = None;
1427 }
1428 }
1429 spec
1430 }
1431}
1432
1433#[cfg(feature = "clap")]
1434impl From<clap::Command> for Spec {
1435 fn from(cmd: clap::Command) -> Self {
1436 (&cmd).into()
1437 }
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442 use crate::spec::effect::SpecCommandEffect;
1443 use crate::Spec;
1444 use insta::assert_snapshot;
1445
1446 #[test]
1447 fn test_effect_prop_and_child_node() {
1448 let spec = Spec::parse(
1449 &Default::default(),
1450 r#"
1451bin "mise"
1452cmd "ls" effect="read"
1453cmd "use" effect="write"
1454cmd "uninstall" {
1455 effect "destructive"
1456}
1457cmd "version"
1458 "#,
1459 )
1460 .unwrap();
1461
1462 let cmds = &spec.cmd.subcommands;
1463 assert_eq!(cmds["ls"].effect, Some(SpecCommandEffect::Read));
1464 assert_eq!(cmds["use"].effect, Some(SpecCommandEffect::Write));
1465 assert_eq!(
1466 cmds["uninstall"].effect,
1467 Some(SpecCommandEffect::Destructive)
1468 );
1469 assert_eq!(cmds["version"].effect, None);
1471 }
1472
1473 #[test]
1474 fn test_effect_is_not_inherited_by_subcommands() {
1475 let spec = Spec::parse(
1476 &Default::default(),
1477 r#"
1478bin "git"
1479cmd "remote" effect="read" {
1480 cmd "add" effect="write"
1481 cmd "show"
1482}
1483 "#,
1484 )
1485 .unwrap();
1486
1487 let remote = &spec.cmd.subcommands["remote"];
1488 assert_eq!(remote.effect, Some(SpecCommandEffect::Read));
1489 assert_eq!(
1490 remote.subcommands["add"].effect,
1491 Some(SpecCommandEffect::Write)
1492 );
1493 assert_eq!(remote.subcommands["show"].effect, None);
1494 }
1495
1496 #[test]
1497 fn test_effect_roundtrips_through_kdl() {
1498 let spec = Spec::parse(
1499 &Default::default(),
1500 r#"
1501bin "mise"
1502cmd "ls" effect="read"
1503cmd "uninstall" effect="destructive"
1504 "#,
1505 )
1506 .unwrap();
1507
1508 assert_snapshot!(spec, @r#"
1509 name mise
1510 bin mise
1511 cmd ls effect=read
1512 cmd uninstall effect=destructive
1513 "#);
1514 }
1515
1516 #[test]
1520 fn test_effect_survives_merge() {
1521 let cmd_with = |src: &str| {
1522 Spec::parse(&Default::default(), src)
1523 .unwrap()
1524 .cmd
1525 .subcommands["uninstall"]
1526 .clone()
1527 };
1528
1529 let declared = cmd_with(r#"cmd "uninstall" effect="destructive""#);
1530 let silent = cmd_with(r#"cmd "uninstall" help="Remove a tool""#);
1531 let contradicting = cmd_with(r#"cmd "uninstall" effect="write""#);
1532
1533 let mut cmd = declared.clone();
1534 cmd.merge(silent);
1535 assert_eq!(cmd.effect, Some(SpecCommandEffect::Destructive));
1536
1537 let mut cmd = declared;
1538 cmd.merge(contradicting);
1539 assert_eq!(cmd.effect, Some(SpecCommandEffect::Write));
1540 }
1541
1542 #[test]
1543 fn test_unknown_effect_is_an_error() {
1544 let err = Spec::parse(
1545 &Default::default(),
1546 r#"
1547bin "mise"
1548cmd "ls" effect="readonly"
1549 "#,
1550 )
1551 .unwrap_err();
1552 assert!(
1553 err.to_string().contains("Invalid usage config"),
1554 "unexpected error: {err}"
1555 );
1556 }
1557}
1558
1559#[cfg(test)]
1560mod merge_tests {
1561 use crate::Spec;
1562
1563 fn uninstall(src: &str) -> crate::SpecCommand {
1564 Spec::parse(&Default::default(), src)
1565 .unwrap()
1566 .cmd
1567 .subcommands["uninstall"]
1568 .clone()
1569 }
1570
1571 #[test]
1574 fn test_deprecated_survives_merge() {
1575 let declared = uninstall(r#"cmd "uninstall" deprecated="use `remove`""#);
1576 let silent = uninstall(r#"cmd "uninstall" help="Remove a tool""#);
1577 let contradicting = uninstall(r#"cmd "uninstall" deprecated="gone in v3""#);
1578
1579 let mut cmd = declared.clone();
1580 cmd.merge(silent);
1581 assert_eq!(cmd.deprecated.as_deref(), Some("use `remove`"));
1582
1583 let mut cmd = declared;
1584 cmd.merge(contradicting);
1585 assert_eq!(cmd.deprecated.as_deref(), Some("gone in v3"));
1586 }
1587}
1588
1589#[cfg(test)]
1590mod roundtrip_tests {
1591 use crate::Spec;
1592
1593 #[test]
1601 fn test_spec_survives_a_kdl_roundtrip() {
1602 let src = r#"
1603name "My CLI"
1604bin "mycli"
1605about "does things"
1606version "1.0.0"
1607author "nobody"
1608license "MIT"
1609
1610flag "-v --verbose" help="Verbose logging" global=#true count=#true
1611arg "<dir>" help="Directory to use"
1612
1613cmd "install" help="Install a package" subcommand_required=#false {
1614 alias "i"
1615 alias "add" hide=#true
1616 long_help "The long help for install"
1617 help_md "The **markdown** help for install"
1618 before_help "before"
1619 before_long_help "The long before-help for install"
1620 before_help_md "The **markdown** before-help for install"
1621 after_help "after"
1622 after_long_help "The long after-help for install"
1623 after_help_md "The **markdown** after-help for install"
1624 arg "<pkg>" help="Package to install"
1625 arg "[dest]" effect="write"
1626 flag "-f --force" help="Overwrite"
1627 flag "--purge" effect="destructive" overrides="-f" required_unless="--keep"
1628 complete "pkg" run="mycli list --available" descriptions=#true
1629 example "mycli install foo" header="Install foo" help="Installs foo" lang="sh"
1630 example "mycli install bar"
1631 cmd "from" help="Install from a source" {
1632 arg "<src>"
1633 }
1634}
1635cmd "wrapped" help="Wraps another CLI" {
1636 mount run="mycli plugin usage-spec"
1637}
1638cmd "remove" help="Remove a package" deprecated="use `uninstall`" effect="destructive"
1639cmd "run" restart_token=":::" help="Run tasks"
1640cmd "exec" external_subcommand=#true help="Run an external command"
1641cmd "hidden" hide=#true
1642 "#;
1643
1644 let original = Spec::parse(&Default::default(), src).unwrap();
1645 let reparsed = Spec::parse(&Default::default(), &original.to_string()).unwrap();
1646
1647 let original = serde_json::to_value(&original).unwrap();
1648 let reparsed = serde_json::to_value(&reparsed).unwrap();
1649 pretty_assertions::assert_eq!(original, reparsed);
1650
1651 let install = &original["cmd"]["subcommands"]["install"];
1654 let purge = install["flags"]
1655 .as_array()
1656 .unwrap()
1657 .iter()
1658 .find(|flag| flag["name"] == "purge")
1659 .unwrap();
1660 assert_eq!(purge["overrides"], serde_json::json!(["-f"]));
1661 assert_eq!(purge["required_unless"], serde_json::json!(["--keep"]));
1662 for key in [
1663 "help_long",
1664 "help_md",
1665 "before_help",
1666 "before_help_long",
1667 "before_help_md",
1668 "after_help",
1669 "after_help_long",
1670 "after_help_md",
1671 "deprecated",
1672 "effect",
1673 "restart_token",
1674 "examples",
1675 "complete",
1676 "mounts",
1677 "aliases",
1678 "hidden_aliases",
1679 ] {
1680 let populated = match key {
1681 "deprecated" | "effect" => {
1683 original["cmd"]["subcommands"]["remove"].get(key).is_some()
1684 }
1685 "restart_token" => original["cmd"]["subcommands"]["run"].get(key).is_some(),
1686 "mounts" => !original["cmd"]["subcommands"]["wrapped"]["mounts"]
1687 .as_array()
1688 .unwrap()
1689 .is_empty(),
1690 _ => match install.get(key) {
1691 Some(serde_json::Value::Array(a)) => !a.is_empty(),
1692 Some(serde_json::Value::Object(o)) => !o.is_empty(),
1693 Some(_) => true,
1694 None => false,
1695 },
1696 };
1697 assert!(populated, "fixture does not exercise `{key}`");
1698 }
1699
1700 assert!(
1703 install["flags"]
1704 .as_array()
1705 .unwrap()
1706 .iter()
1707 .any(|f| f.get("effect").is_some()),
1708 "fixture does not exercise a flag-level `effect`"
1709 );
1710 assert!(
1711 install["args"]
1712 .as_array()
1713 .unwrap()
1714 .iter()
1715 .any(|a| a.get("effect").is_some()),
1716 "fixture does not exercise an arg-level `effect`"
1717 );
1718 }
1719 #[cfg(feature = "clap")]
1720 #[test]
1721 fn a_clap_command_says_what_clap_would_do_with_an_unknown_flag() {
1722 use super::{SpecCommand, UnknownFlags};
1723
1724 let plain = clap::Command::new("build")
1728 .arg(clap::Arg::new("target").required(false))
1729 .arg(clap::Arg::new("force").long("force").num_args(0));
1730 let spec: SpecCommand = (&plain).into();
1731 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
1732
1733 let wrapper = clap::Command::new("run").arg(
1736 clap::Arg::new("args")
1737 .num_args(0..)
1738 .allow_hyphen_values(true),
1739 );
1740 let spec: SpecCommand = (&wrapper).into();
1741 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
1742
1743 let trailing = clap::Command::new("exec").arg(
1745 clap::Arg::new("cmd")
1746 .num_args(0..)
1747 .trailing_var_arg(true)
1748 .allow_hyphen_values(true),
1749 );
1750 let spec: SpecCommand = (&trailing).into();
1751 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Value));
1752
1753 let external = clap::Command::new("x").allow_external_subcommands(true);
1756 let spec: SpecCommand = (&external).into();
1757 assert_eq!(spec.unknown_flags, Some(UnknownFlags::Error));
1758 assert!(spec.external_subcommand);
1759 }
1760
1761 #[cfg(feature = "clap")]
1762 #[test]
1763 fn the_decision_survives_being_written_and_read_back() {
1764 use super::SpecCommand;
1765
1766 let plain = clap::Command::new("build").arg(clap::Arg::new("target").required(false));
1769 let spec: SpecCommand = (&plain).into();
1770 let node: kdl::KdlNode = (&spec).into();
1771 let kdl = node.to_string();
1772 assert!(kdl.contains("unknown_flags=error"), "{kdl}");
1773 }
1774
1775 #[cfg(feature = "clap")]
1776 #[test]
1777 fn the_clap_bridge_preserves_args_override_self() {
1778 use super::SpecCommand;
1779
1780 let strict: SpecCommand = (&clap::Command::new("strict")).into();
1781 assert!(!strict.args_override_self, "clap is strict by default");
1782
1783 let permissive: SpecCommand =
1784 (&clap::Command::new("permissive").args_override_self(true)).into();
1785 assert!(permissive.args_override_self);
1786
1787 let node: kdl::KdlNode = (&strict).into();
1788 assert!(node.to_string().contains("args_override_self=#false"));
1789 }
1790
1791 #[cfg(feature = "clap")]
1792 #[test]
1793 fn the_clap_bridge_preserves_subcommand_presentation() {
1794 use super::SpecCommand;
1795
1796 let spec: SpecCommand = (&clap::Command::new("ex")
1797 .subcommand(clap::Command::new("run"))
1798 .subcommand_help_heading("Actions")
1799 .subcommand_value_name("ACTION"))
1800 .into();
1801 assert_eq!(spec.subcommand_help_heading.as_deref(), Some("Actions"));
1802 assert_eq!(spec.subcommand_value_name.as_deref(), Some("ACTION"));
1803 let node: kdl::KdlNode = (&spec).into();
1804 let kdl = node.to_string();
1805 assert!(kdl.contains("subcommand_help_heading=Actions"), "{kdl}");
1806 assert!(kdl.contains("subcommand_value_name=ACTION"), "{kdl}");
1807 }
1808
1809 #[cfg(feature = "clap")]
1810 #[test]
1811 fn the_clap_bridge_preserves_subcommand_negates_requirements() {
1812 use super::SpecCommand;
1813
1814 let spec: SpecCommand = (&clap::Command::new("ex").subcommand_negates_reqs(true)).into();
1815 assert!(spec.subcommand_negates_reqs);
1816 let node: kdl::KdlNode = (&spec).into();
1817 assert!(node.to_string().contains("subcommand_negates_reqs=#true"));
1818 }
1819
1820 #[cfg(feature = "clap")]
1821 #[test]
1822 fn the_clap_bridge_preserves_argument_subcommand_conflicts() {
1823 use super::SpecCommand;
1824
1825 let spec: SpecCommand =
1826 (&clap::Command::new("ex").args_conflicts_with_subcommands(true)).into();
1827 assert!(spec.args_conflicts_with_subcommands);
1828 let node: kdl::KdlNode = (&spec).into();
1829 assert!(node
1830 .to_string()
1831 .contains("args_conflicts_with_subcommands=#true"));
1832 }
1833
1834 #[cfg(feature = "clap")]
1835 #[test]
1836 fn the_clap_bridge_preserves_allow_missing_positional() {
1837 use super::SpecCommand;
1838
1839 let spec: SpecCommand = (&clap::Command::new("ex").allow_missing_positional(true)).into();
1840 assert!(spec.allow_missing_positional);
1841 let node: kdl::KdlNode = (&spec).into();
1842 assert!(node.to_string().contains("allow_missing_positional=#true"));
1843 }
1844}