Skip to main content

usage/spec/
cmd.rs

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/// A CLI command or subcommand specification.
22///
23/// Commands define the structure of a CLI, including their flags, arguments,
24/// and nested subcommands. The root command represents the main CLI entry point.
25///
26/// # Example
27///
28/// ```
29/// use usage::{SpecCommand, SpecFlag, SpecArg};
30///
31/// let cmd = SpecCommand::builder()
32///     .name("install")
33///     .help("Install a package")
34///     .alias("i")
35///     .flag(SpecFlag::builder().short('f').long("force").build())
36///     .arg(SpecArg::builder().name("package").required(true).build())
37///     .build();
38/// ```
39#[derive(Debug, Serialize, Clone)]
40pub struct SpecCommand {
41    /// Full command path from root (e.g., ["git", "remote", "add"])
42    pub full_cmd: Vec<String>,
43    /// Generated usage string
44    pub usage: String,
45    /// Nested subcommands indexed by name
46    pub subcommands: IndexMap<String, SpecCommand>,
47    /// Positional arguments for this command
48    pub args: Vec<SpecArg>,
49    /// Flags/options for this command
50    pub flags: Vec<SpecFlag>,
51    /// Flagsets this command pulls in, and where in [`Self::flags`] they belong.
52    ///
53    /// `pub(crate)` because it is always empty by the time anyone else can see it: a `use` is
54    /// resolved while the spec is read, so a consumer holding a `SpecCommand` holds the flags
55    /// the sets named. Visible to the rest of the crate only so that other modules can
56    /// destructure `SpecCommand` exhaustively.
57    #[serde(skip)]
58    pub(crate) uses: Vec<SpecUse>,
59    /// Mounted external specs
60    pub mounts: Vec<SpecMount>,
61    /// Sets of flags that relate to one another as a set.
62    ///
63    /// Pairwise [`conflicts`](SpecFlag::conflicts) can say everything a plain group says
64    /// and cannot say `required`: "one of these is needed" is a statement about the set.
65    #[serde(skip_serializing_if = "Vec::is_empty")]
66    pub groups: Vec<SpecGroup>,
67    /// Deprecation message if this command is deprecated
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub deprecated: Option<String>,
70    /// Version at which consumers should begin warning about this command.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub deprecated_warn_at: Option<String>,
73    /// Version at which consumers expect this command to be removed.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub deprecated_remove_at: Option<String>,
76    /// What running this command does to the world: read, write or destructive.
77    /// Not inherited by subcommands.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub effect: Option<SpecCommandEffect>,
80    /// What to do here with a flag-like token that names no declared flag.
81    ///
82    /// Unset means "whatever encloses this command decided" — the nearest command
83    /// above that set one, or failing that the spec, or failing that
84    /// [`UnknownFlags::Value`]. Unlike [`SpecCommandEffect`] this *is* inherited,
85    /// because it describes how a command line is read rather than what a command
86    /// does, and a CLI that forwards options generally forwards them everywhere.
87    pub unknown_flags: Option<UnknownFlags>,
88    /// Whether to hide this command from help output
89    pub hide: bool,
90    /// Help section this command appears under in its parent's command list.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub help_heading: Option<String>,
93    /// Explicit placement within its parent's command section.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub display_order: Option<usize>,
96    /// True when this command came from a [`SpecMount`], i.e. it describes another
97    /// program's CLI that was merged in at parse time.
98    ///
99    /// The flags of the commands *above* a mounted command belong to the mounting CLI,
100    /// not to the mounted program, so they are not offered in completions once a mounted
101    /// command has been reached. They stay recognized by the parser, since they may
102    /// legitimately appear *before* the mounted command on the command line.
103    ///
104    /// Runtime-only: it is derived from `mount` nodes and is not part of the spec syntax.
105    #[serde(skip)]
106    pub mounted: bool,
107    /// True when a [`SpecMount`] brought flags of its own onto this command. A mounted spec's
108    /// root flags are merged into the command the mount sits on, *replacing* that command's
109    /// flags (see [`SpecCommand::merge`]), so when this is set every flag here describes the
110    /// mounted program and is offered inside the mounted commands accordingly.
111    ///
112    /// Runtime-only, like [`SpecCommand::mounted`].
113    #[serde(skip)]
114    pub flags_from_mount: bool,
115    /// Whether a subcommand must be provided
116    #[serde(skip_serializing_if = "is_false")]
117    pub subcommand_required: bool,
118    /// Heading used for this command's subcommand section.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub subcommand_help_heading: Option<String>,
121    /// Placeholder used for subcommands in the synopsis.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub subcommand_value_name: Option<String>,
124    /// Put each argument, flag, and subcommand description on the following line.
125    #[serde(skip_serializing_if = "is_false")]
126    pub next_line_help: bool,
127    /// Expand each visible subcommand's summary and arguments into this command's help page.
128    #[serde(skip_serializing_if = "is_false")]
129    pub flatten_help: bool,
130    /// Fixed help width. Zero disables wrapping.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub term_width: Option<usize>,
133    /// Maximum detected terminal width when `term_width` is unset. Zero disables the cap.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub max_term_width: Option<usize>,
136    /// Whether an unmatched word is forwarded as an external command plus the rest of argv.
137    ///
138    /// clap's `allow_external_subcommands` / `#[command(external_subcommand)]`. Known
139    /// subcommands still win; a `default_subcommand` still catches first. Once the
140    /// unmatched word is taken, remaining tokens — including `--help` — are not parsed
141    /// as this command's flags.
142    #[serde(skip_serializing_if = "is_false")]
143    pub external_subcommand: bool,
144    /// Whether a bare invocation of this command shows its help.
145    #[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    /// Whether delimiter splitting is disabled after `--` or for an automatic trailing arg.
154    #[serde(skip_serializing_if = "is_false")]
155    pub dont_delimit_trailing_values: bool,
156    /// Whether a later occurrence of a single-valued argument replaces the earlier one.
157    /// Permissive by default; set false to report duplicates.
158    pub args_override_self: bool,
159    /// Whether selecting a subcommand satisfies this command's required arguments.
160    #[serde(skip_serializing_if = "is_false")]
161    pub subcommand_negates_reqs: bool,
162    /// Whether binding an argument prevents selecting a later subcommand.
163    #[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    /// Allow required positionals after optional positionals to claim the remaining words.
168    #[serde(skip_serializing_if = "is_false")]
169    pub allow_missing_positional: bool,
170    /// Token that resets argument parsing, allowing multiple command invocations.
171    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub restart_token: Option<String>,
174    /// Short help text shown in command listings
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub help: Option<String>,
177    /// Extended help text shown with --help
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub help_long: Option<String>,
180    /// Markdown-formatted help text
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub help_md: Option<String>,
183    /// Command name (e.g., "install")
184    pub name: String,
185    /// Alternative names for this command
186    pub aliases: Vec<String>,
187    /// Hidden alternative names (not shown in help)
188    pub hidden_aliases: Vec<String>,
189    /// Text displayed before the help content
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub before_help: Option<String>,
192    /// Extended text displayed before help content
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub before_help_long: Option<String>,
195    /// Markdown text displayed before help content
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub before_help_md: Option<String>,
198    /// Text displayed after the help content
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub after_help: Option<String>,
201    /// Extended text displayed after help content
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub after_help_long: Option<String>,
204    /// Markdown text displayed after help content
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub after_help_md: Option<String>,
207    /// Usage examples for this command
208    pub examples: Vec<SpecExample>,
209    /// Custom completers for arguments
210    #[serde(skip_serializing_if = "IndexMap::is_empty")]
211    pub complete: IndexMap<String, SpecComplete>,
212
213    /// Cache for subcommand name lookups (including aliases).
214    ///
215    /// `pub(crate)` only so that other modules can destructure `SpecCommand`
216    /// exhaustively; it stays private to the crate.
217    #[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    /// An example invocation shown in generated docs and help.
291    pub fn new(code: impl Into<String>) -> Self {
292        Self {
293            code: code.into(),
294            ..Default::default()
295        }
296    }
297
298    /// Heading shown above the example.
299    pub fn header(mut self, header: impl Into<String>) -> Self {
300        self.header = Some(header.into());
301        self
302    }
303
304    /// Prose shown with the example.
305    pub fn help(mut self, help: impl Into<String>) -> Self {
306        self.help = Some(help.into());
307        self
308    }
309
310    /// Language used for syntax highlighting.
311    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    /// Create a new builder for SpecCommand
336    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                    // As on a flag: splitting a word that has room for one value would
437                    // drop everything after the first separator.
438                    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    // `docs` only, like `SpecChoices::for_help`: the usage line without the subcommand
629    // placeholder is a help-page shape, and nothing else asks for it.
630    #[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        // TODO: mounts?
669        // if !self.mounts.is_empty() {
670        //     name = format!("{name} [mounts]");
671        // }
672        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        // Merging can add subcommands and aliases, and `find_subcommand` memoizes
683        // its lookup into a OnceLock — so the cache has to go, or a name that
684        // arrived here would not be findable. This worked before only because
685        // mounting happened to precede the first lookup on a given command.
686        self.subcommand_lookup = OnceLock::new();
687        // Destructured exhaustively (no `..`) so that adding a field to
688        // SpecCommand fails to compile until this decides what merging it means.
689        // Runtime-derived fields are explicitly ignored rather than skipped.
690        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            // Recomputed from the merged command, never carried over.
739            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        // Unresolved `use` nodes travel with the flags they were written among, for the same
783        // reason groups do — including when that means going with none. A `use` is a
784        // declaration of flags, so whoever owns the flag list owns it: an included file that
785        // replaces this command's flags replaces what it says about them, and a `use` left
786        // behind would splice a set into the incoming list at a position from the old one.
787        //
788        // `other.uses` is normally empty either way: a spec resolves its own sets before it
789        // can be merged into another, and what arrives here has been through that already.
790        if flags_replaced || !uses.is_empty() {
791            self.uses = uses;
792        }
793        if !mounts.is_empty() {
794            self.mounts = mounts;
795        }
796        // Groups travel with the flags they name. A mounted spec that replaces this
797        // command's flags replaces its groups too — including with none, which is the
798        // case that matters: keeping the old set would enforce exclusivity between flags
799        // that are no longer here, and a required group whose members nothing answers to
800        // would reject every invocation.
801        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            // Names first, then aliases only where nothing answers already: a
885            // command's own name outranks another command's alias, so reordering
886            // `cmd` blocks cannot change which command a word selects.
887            //
888            // Inserting both in one pass instead let the *last* declaration win,
889            // which was the opposite of what usage-argv did with the same spec —
890            // it takes the first. Neither was a rule anyone had chosen.
891            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                // Parse the mount command into tokens, insert global flags after the first token
915                // e.g., "mise tasks ls" becomes "mise --cd dir2 tasks ls"
916                // Handles quoted arguments correctly: "cmd 'arg with spaces'" stays correct
917                let mut tokens = shell_words::split(&mount.run)
918                    .expect("mount command should be valid shell syntax");
919                if !tokens.is_empty() {
920                    // Insert global flags after the first token (the command name)
921                    tokens.splice(1..1, global_flag_args.iter().cloned());
922                }
923                // Join tokens back into a properly quoted command string
924                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                // A mounted spec's root is merged into this command, so its root-only
936                // default-subcommand precedence does not apply while composing mounts.
937                spec.resolve_mount_outputs_at_root(outputs, false)?;
938            }
939            // The subcommands emitted by a mount describe another program, so mark them (and
940            // everything below them) as mounted. See `SpecCommand::mounted`.
941            for cmd in spec.cmd.subcommands.values_mut() {
942                cmd.mark_mounted();
943            }
944            // `merge` folds the mounted spec's root flags into this command; remember that they
945            // came from the mount. See `SpecCommand::flags_from_mount`.
946            self.flags_from_mount |= !spec.cmd.flags.is_empty();
947            self.merge(spec.cmd);
948        }
949        Ok(())
950    }
951
952    /// Mark this command and all of its subcommands as coming from a mount.
953    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        // Destructured exhaustively (no `..`) so that adding a field to
964        // SpecCommand fails to compile until this decides how to serialize it.
965        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            // Resolved while the spec was read: whatever a `use` named is among `flags`
1013            // by now, so emitting the request too would declare those flags twice.
1014            uses: _,
1015            // Derived from the spec rather than written by it.
1016            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        // After the flags they name, so a reader meets the members before the rule
1197        // about them.
1198        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        // What clap would do with a dash-word it does not recognize, said out loud.
1233        //
1234        // clap rejects one; this spec's default is to offer it to the positionals, because a
1235        // spec also describes wrappers — a script run through `usage exec`, a task's arguments —
1236        // where a dash-word is data in transit rather than a mistake. A CLI generated *from
1237        // clap*, though, is not one of those: clap already decided, and saying nothing here
1238        // silently loosened every command it described. mise's spec has 211 commands and not one
1239        // of them said `unknown_flags`, so `mise use --globa` became a tool named `--globa`
1240        // rather than the error clap gives.
1241        //
1242        // Which commands forward unknown *flags* is clap's own knowledge: an argument that
1243        // accepts hyphen values, or a trailing var arg. In mise that is five commands —
1244        // `run`, `watch`, `asdf`, `tool-stub` and the root's implicit task arguments — and
1245        // the other two hundred get the stricter reading back.
1246        //
1247        // An external subcommand is a different shape: an unmatched *word* is forwarded with
1248        // the rest of argv. clap still rejects an unknown flag on such a command (`x --wat`),
1249        // so mapping `allow_external_subcommands` onto `unknown_flags=value` silently loosened
1250        // every clap CLI that allowed one.
1251        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                // clap keeps conflicts on the command rather than on the argument, so
1304                // this is the only place both are in view. Written with dashes,
1305                // matching how the spec refers to a flag everywhere else.
1306                //
1307                // A short-only flag is named `-s`, which selectors accept as readily as
1308                // `--long`: taking only the long form would have dropped the conflict
1309                // and let the spec accept a combination clap rejects.
1310                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        // clap assigns an implicit monotonically increasing order to arguments. Emitting
1326        // that number for every ordinary declaration makes generated specs noisy without
1327        // changing presentation, since usage already retains declaration order. Keep the
1328        // values only when they actually reorder a section.
1329        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        // Groups, which clap does expose — `get_groups`, and `get_args` on each. A group
1348        // names its members by clap's internal id, so each is resolved back to the flag it
1349        // points at and written as a selector, the way conflicts are just above.
1350        //
1351        // clap's own `--help` groups (`ArgGroup` ids it creates for its built-in flags)
1352        // have no members of ours in them, so the two-member floor drops them naturally
1353        // rather than needing a name check.
1354        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            // Below two members there is no rule left to enforce: whatever the group said
1366            // about "at most one" or "at least one" is either vacuous or is plain
1367            // required-ness on the single flag, which the flag already carries.
1368            if members.len() < 2 {
1369                continue;
1370            }
1371            // `multiple` without `required` enforces nothing at all — any number of
1372            // members, none of them needed — so there is nothing to carry across.
1373            //
1374            // This is not a corner case. clap's *derive* emits exactly that group for
1375            // every `#[derive(Args)]` struct, named after the struct and holding all its
1376            // fields, to make `flatten` work: `clap_derive`'s `args.rs` builds
1377            // `ArgGroup::new(id).multiple(true)`. Carrying them would put a `group Lint
1378            // …` in the spec of every clap-derived CLI, including this repository's own,
1379            // describing bookkeeping rather than a rule anyone declared.
1380            let required = group.is_required_set();
1381            // `is_multiple` takes `&mut self` in clap, and a `&ArgGroup` is all a
1382            // `Command` hands out — so the group is cloned to ask. Once per group at
1383            // spec-generation time, which is a build step rather than a parse.
1384            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        // 999 is clap's ordinary subcommand order. Leaving every command at that value lets
1418        // usage's existing alphabetical tie-breaker produce the same page without serializing
1419        // redundant metadata.
1420        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        // Unspecified stays unknown rather than defaulting to anything.
1470        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    /// `merge` is how included and mounted specs are composed onto a command.
1517    /// It has to treat `effect` the way it treats every other optional field:
1518    /// an overlay that says nothing must not erase what is already declared.
1519    #[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    /// An overlay that says nothing about deprecation must not un-deprecate a
1572    /// command that already declared it.
1573    #[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    /// Serializing a spec back to KDL and reparsing it must not lose anything.
1594    ///
1595    /// The parser is a match on node names, so exhaustive destructuring can't
1596    /// catch a field the serializer knows about but the parser doesn't, or the
1597    /// reverse. Comparing the serde representation covers every field without
1598    /// this test having to enumerate them, so a new field is covered the day it
1599    /// is added.
1600    #[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        // Equality is only meaningful if the fixture actually populated the
1652        // fields, so guard against a future edit quietly emptying it out.
1653        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                // These sit on other commands in the fixture.
1682                "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        // Flag- and arg-level effects live one level down, so check them
1701        // explicitly rather than by name against the command object.
1702        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        // clap rejects a dash-word it does not know; this spec's default is to offer it to the
1725        // positionals. Saying nothing therefore loosened every command generated from clap —
1726        // which is how `mise use --globa` became a tool named `--globa` rather than an error.
1727        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        // A command that forwards says so, and clap is the one that knows: an argument taking
1734        // hyphen values is what a wrapper looks like.
1735        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        // As does one whose trailing argument swallows the rest.
1744        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        // And a command that takes whatever subcommand it is given, which is a different
1754        // shape from forwarding unknown flags: clap still rejects `x --wat`.
1755        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        // The point of setting it is what a *parser* does with the spec afterwards, so the round
1767        // trip is what makes it true rather than the field.
1768        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}