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::helpers::NodeHelper;
9use crate::spec::is_false;
10use crate::spec::mount::SpecMount;
11use crate::{Spec, SpecArg, SpecComplete, SpecFlag};
12use indexmap::IndexMap;
13use itertools::Itertools;
14use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
15use serde::Serialize;
16
17#[derive(Debug, Serialize, Clone)]
18pub struct SpecCommand {
19    pub full_cmd: Vec<String>,
20    pub usage: String,
21    pub subcommands: IndexMap<String, SpecCommand>,
22    pub args: Vec<SpecArg>,
23    pub flags: Vec<SpecFlag>,
24    pub mounts: Vec<SpecMount>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub deprecated: Option<String>,
27    pub hide: bool,
28    #[serde(skip_serializing_if = "is_false")]
29    pub subcommand_required: bool,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub help: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub help_long: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub help_md: Option<String>,
36    pub name: String,
37    pub aliases: Vec<String>,
38    pub hidden_aliases: Vec<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub before_help: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub before_help_long: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub before_help_md: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub after_help: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub after_help_long: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub after_help_md: Option<String>,
51    pub examples: Vec<SpecExample>,
52    #[serde(skip_serializing_if = "IndexMap::is_empty")]
53    pub complete: IndexMap<String, SpecComplete>,
54
55    // TODO: make this non-public
56    #[serde(skip)]
57    subcommand_lookup: OnceLock<HashMap<String, String>>,
58}
59
60impl Default for SpecCommand {
61    fn default() -> Self {
62        Self {
63            full_cmd: vec![],
64            usage: "".to_string(),
65            subcommands: IndexMap::new(),
66            args: vec![],
67            flags: vec![],
68            mounts: vec![],
69            deprecated: None,
70            hide: false,
71            subcommand_required: false,
72            help: None,
73            help_long: None,
74            help_md: None,
75            name: "".to_string(),
76            aliases: vec![],
77            hidden_aliases: vec![],
78            before_help: None,
79            before_help_long: None,
80            before_help_md: None,
81            after_help: None,
82            after_help_long: None,
83            after_help_md: None,
84            examples: vec![],
85            subcommand_lookup: OnceLock::new(),
86            complete: IndexMap::new(),
87        }
88    }
89}
90
91#[derive(Debug, Default, Serialize, Clone)]
92pub struct SpecExample {
93    pub code: String,
94    pub header: Option<String>,
95    pub help: Option<String>,
96    pub lang: String,
97}
98
99impl SpecExample {
100    pub(crate) fn new(code: String) -> Self {
101        Self {
102            code,
103            ..Default::default()
104        }
105    }
106}
107
108impl From<&SpecExample> for KdlNode {
109    fn from(example: &SpecExample) -> KdlNode {
110        let mut node = KdlNode::new("example");
111        node.push(KdlEntry::new(example.code.clone()));
112        if let Some(header) = &example.header {
113            node.push(KdlEntry::new_prop("header", header.clone()));
114        }
115        if let Some(help) = &example.help {
116            node.push(KdlEntry::new_prop("help", help.clone()));
117        }
118        if !example.lang.is_empty() {
119            node.push(KdlEntry::new_prop("lang", example.lang.clone()));
120        }
121        node
122    }
123}
124
125impl SpecCommand {
126    /// Create a new builder for SpecCommand
127    pub fn builder() -> SpecCommandBuilder {
128        SpecCommandBuilder::new()
129    }
130
131    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
132        node.ensure_arg_len(1..=1)?;
133        let mut cmd = Self {
134            name: node.arg(0)?.ensure_string()?.to_string(),
135            ..Default::default()
136        };
137        for (k, v) in node.props() {
138            match k {
139                "help" => cmd.help = Some(v.ensure_string()?),
140                "long_help" => cmd.help_long = Some(v.ensure_string()?),
141                "help_long" => cmd.help_long = Some(v.ensure_string()?),
142                "help_md" => cmd.help_md = Some(v.ensure_string()?),
143                "before_help" => cmd.before_help = Some(v.ensure_string()?),
144                "before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
145                "before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
146                "before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
147                "after_help" => cmd.after_help = Some(v.ensure_string()?),
148                "after_long_help" => {
149                    cmd.after_help_long = Some(v.ensure_string()?);
150                }
151                "after_help_long" => {
152                    cmd.after_help_long = Some(v.ensure_string()?);
153                }
154                "after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
155                "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
156                "hide" => cmd.hide = v.ensure_bool()?,
157                "deprecated" => {
158                    cmd.deprecated = match v.value.as_bool() {
159                        Some(true) => Some("deprecated".to_string()),
160                        Some(false) => None,
161                        None => Some(v.ensure_string()?),
162                    }
163                }
164                k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
165            }
166        }
167        for child in node.children() {
168            match child.name() {
169                "flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
170                "arg" => cmd.args.push(SpecArg::parse(ctx, &child)?),
171                "mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
172                "cmd" => {
173                    let node = SpecCommand::parse(ctx, &child)?;
174                    cmd.subcommands.insert(node.name.to_string(), node);
175                }
176                "alias" => {
177                    let alias = child
178                        .ensure_arg_len(1..)?
179                        .args()
180                        .map(|e| e.ensure_string())
181                        .collect::<Result<Vec<_>, _>>()?;
182                    let hide = child
183                        .get("hide")
184                        .map(|n| n.ensure_bool())
185                        .unwrap_or(Ok(false))?;
186                    if hide {
187                        cmd.hidden_aliases.extend(alias);
188                    } else {
189                        cmd.aliases.extend(alias);
190                    }
191                }
192                "example" => {
193                    let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
194                    let mut example = SpecExample::new(code.trim().to_string());
195                    for (k, v) in child.props() {
196                        match k {
197                            "header" => example.header = Some(v.ensure_string()?),
198                            "help" => example.help = Some(v.ensure_string()?),
199                            "lang" => example.lang = v.ensure_string()?,
200                            k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
201                        }
202                    }
203                    cmd.examples.push(example);
204                }
205                "help" => {
206                    cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
207                }
208                "long_help" => {
209                    cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
210                }
211                "before_help" => {
212                    cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
213                }
214                "before_long_help" => {
215                    cmd.before_help_long =
216                        Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
217                }
218                "after_help" => {
219                    cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
220                }
221                "after_long_help" => {
222                    cmd.after_help_long =
223                        Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
224                }
225                "subcommand_required" => {
226                    cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
227                }
228                "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
229                "deprecated" => {
230                    cmd.deprecated = match child.arg(0)?.value.as_bool() {
231                        Some(true) => Some("deprecated".to_string()),
232                        Some(false) => None,
233                        None => Some(child.arg(0)?.ensure_string()?),
234                    }
235                }
236                "complete" => {
237                    let complete = SpecComplete::parse(ctx, &child)?;
238                    cmd.complete.insert(complete.name.clone(), complete);
239                }
240                k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
241            }
242        }
243        Ok(cmd)
244    }
245    pub(crate) fn is_empty(&self) -> bool {
246        self.args.is_empty()
247            && self.flags.is_empty()
248            && self.mounts.is_empty()
249            && self.subcommands.is_empty()
250    }
251    pub fn usage(&self) -> String {
252        let mut usage = self.full_cmd.join(" ");
253        let flags = self.flags.iter().filter(|f| !f.hide).collect_vec();
254        let args = self.args.iter().filter(|a| !a.hide).collect_vec();
255        if !flags.is_empty() {
256            if flags.len() <= 2 {
257                let inlines = flags
258                    .iter()
259                    .map(|f| {
260                        if f.required {
261                            format!("<{}>", f.usage())
262                        } else {
263                            format!("[{}]", f.usage())
264                        }
265                    })
266                    .join(" ");
267                usage = format!("{usage} {inlines}").trim().to_string();
268            } else if flags.iter().any(|f| f.required) {
269                usage = format!("{usage} <FLAGS>");
270            } else {
271                usage = format!("{usage} [FLAGS]");
272            }
273        }
274        if !args.is_empty() {
275            if args.len() <= 2 {
276                let inlines = args.iter().map(|a| a.usage()).join(" ");
277                usage = format!("{usage} {inlines}").trim().to_string();
278            } else if args.iter().any(|a| a.required) {
279                usage = format!("{usage} <ARGS>…");
280            } else {
281                usage = format!("{usage} [ARGS]…");
282            }
283        }
284        // TODO: mounts?
285        // if !self.mounts.is_empty() {
286        //     name = format!("{name} [mounts]");
287        // }
288        if !self.subcommands.is_empty() {
289            usage = format!("{usage} <SUBCOMMAND>");
290        }
291        usage.trim().to_string()
292    }
293    pub(crate) fn merge(&mut self, other: Self) {
294        if !other.name.is_empty() {
295            self.name = other.name;
296        }
297        if other.help.is_some() {
298            self.help = other.help;
299        }
300        if other.help_long.is_some() {
301            self.help_long = other.help_long;
302        }
303        if other.help_md.is_some() {
304            self.help_md = other.help_md;
305        }
306        if other.before_help.is_some() {
307            self.before_help = other.before_help;
308        }
309        if other.before_help_long.is_some() {
310            self.before_help_long = other.before_help_long;
311        }
312        if other.before_help_md.is_some() {
313            self.before_help_md = other.before_help_md;
314        }
315        if other.after_help.is_some() {
316            self.after_help = other.after_help;
317        }
318        if other.after_help_long.is_some() {
319            self.after_help_long = other.after_help_long;
320        }
321        if other.after_help_md.is_some() {
322            self.after_help_md = other.after_help_md;
323        }
324        if !other.args.is_empty() {
325            self.args = other.args;
326        }
327        if !other.flags.is_empty() {
328            self.flags = other.flags;
329        }
330        if !other.mounts.is_empty() {
331            self.mounts = other.mounts;
332        }
333        if !other.aliases.is_empty() {
334            self.aliases = other.aliases;
335        }
336        if !other.hidden_aliases.is_empty() {
337            self.hidden_aliases = other.hidden_aliases;
338        }
339        if !other.examples.is_empty() {
340            self.examples = other.examples;
341        }
342        self.hide = other.hide;
343        self.subcommand_required = other.subcommand_required;
344        for (name, cmd) in other.subcommands {
345            self.subcommands.insert(name, cmd);
346        }
347        for (name, complete) in other.complete {
348            self.complete.insert(name, complete);
349        }
350    }
351
352    pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
353        let mut cmds = vec![];
354        for cmd in self.subcommands.values() {
355            cmds.push(cmd);
356            cmds.extend(cmd.all_subcommands());
357        }
358        cmds
359    }
360
361    pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
362        let sl = self.subcommand_lookup.get_or_init(|| {
363            let mut map = HashMap::new();
364            for (name, cmd) in &self.subcommands {
365                map.insert(name.clone(), name.clone());
366                for alias in &cmd.aliases {
367                    map.insert(alias.clone(), name.clone());
368                }
369                for alias in &cmd.hidden_aliases {
370                    map.insert(alias.clone(), name.clone());
371                }
372            }
373            map
374        });
375        let name = sl.get(name)?;
376        self.subcommands.get(name)
377    }
378
379    pub(crate) fn mount(&mut self, global_flag_args: &[String]) -> Result<(), UsageErr> {
380        for mount in self.mounts.iter().cloned().collect_vec() {
381            let cmd = if global_flag_args.is_empty() {
382                mount.run.clone()
383            } else {
384                // Parse the mount command into tokens, insert global flags after the first token
385                // e.g., "mise tasks ls" becomes "mise --cd dir2 tasks ls"
386                // Handles quoted arguments correctly: "cmd 'arg with spaces'" stays correct
387                let mut tokens = shell_words::split(&mount.run)
388                    .expect("mount command should be valid shell syntax");
389                if !tokens.is_empty() {
390                    // Insert global flags after the first token (the command name)
391                    tokens.splice(1..1, global_flag_args.iter().cloned());
392                }
393                // Join tokens back into a properly quoted command string
394                shell_words::join(tokens)
395            };
396            let output = sh(&cmd)?;
397            let spec: Spec = output.parse()?;
398            self.merge(spec.cmd);
399        }
400        Ok(())
401    }
402}
403
404impl From<&SpecCommand> for KdlNode {
405    fn from(cmd: &SpecCommand) -> Self {
406        let mut node = Self::new("cmd");
407        node.entries_mut().push(cmd.name.clone().into());
408        if cmd.hide {
409            node.entries_mut().push(KdlEntry::new_prop("hide", true));
410        }
411        if cmd.subcommand_required {
412            node.entries_mut()
413                .push(KdlEntry::new_prop("subcommand_required", true));
414        }
415        if !cmd.aliases.is_empty() {
416            let mut aliases = KdlNode::new("alias");
417            for alias in &cmd.aliases {
418                aliases.entries_mut().push(alias.clone().into());
419            }
420            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
421            children.nodes_mut().push(aliases);
422        }
423        if !cmd.hidden_aliases.is_empty() {
424            let mut aliases = KdlNode::new("alias");
425            for alias in &cmd.hidden_aliases {
426                aliases.entries_mut().push(alias.clone().into());
427            }
428            aliases.entries_mut().push(KdlEntry::new_prop("hide", true));
429            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
430            children.nodes_mut().push(aliases);
431        }
432        if let Some(help) = &cmd.help {
433            node.entries_mut()
434                .push(KdlEntry::new_prop("help", help.clone()));
435        }
436        if let Some(help) = &cmd.help_long {
437            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
438            let mut node = KdlNode::new("long_help");
439            node.insert(0, KdlValue::String(help.clone()));
440            children.nodes_mut().push(node);
441        }
442        if let Some(help) = &cmd.help_md {
443            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
444            let mut node = KdlNode::new("help_md");
445            node.insert(0, KdlValue::String(help.clone()));
446            children.nodes_mut().push(node);
447        }
448        if let Some(help) = &cmd.before_help {
449            node.entries_mut()
450                .push(KdlEntry::new_prop("before_help", help.clone()));
451        }
452        if let Some(help) = &cmd.before_help_long {
453            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
454            let mut node = KdlNode::new("before_long_help");
455            node.insert(0, KdlValue::String(help.clone()));
456            children.nodes_mut().push(node);
457        }
458        if let Some(help) = &cmd.before_help_md {
459            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
460            let mut node = KdlNode::new("before_help_md");
461            node.insert(0, KdlValue::String(help.clone()));
462            children.nodes_mut().push(node);
463        }
464        if let Some(help) = &cmd.after_help {
465            node.entries_mut()
466                .push(KdlEntry::new_prop("after_help", help.clone()));
467        }
468        if let Some(help) = &cmd.after_help_long {
469            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
470            let mut node = KdlNode::new("after_long_help");
471            node.insert(0, KdlValue::String(help.clone()));
472            children.nodes_mut().push(node);
473        }
474        if let Some(help) = &cmd.after_help_md {
475            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
476            let mut node = KdlNode::new("after_help_md");
477            node.insert(0, KdlValue::String(help.clone()));
478            children.nodes_mut().push(node);
479        }
480        for flag in &cmd.flags {
481            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
482            children.nodes_mut().push(flag.into());
483        }
484        for arg in &cmd.args {
485            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
486            children.nodes_mut().push(arg.into());
487        }
488        for mount in &cmd.mounts {
489            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
490            children.nodes_mut().push(mount.into());
491        }
492        for cmd in cmd.subcommands.values() {
493            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
494            children.nodes_mut().push(cmd.into());
495        }
496        for complete in cmd.complete.values() {
497            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
498            children.nodes_mut().push(complete.into());
499        }
500        node
501    }
502}
503
504#[cfg(feature = "clap")]
505impl From<&clap::Command> for SpecCommand {
506    fn from(cmd: &clap::Command) -> Self {
507        let mut spec = Self {
508            name: cmd.get_name().to_string(),
509            hide: cmd.is_hide_set(),
510            help: cmd.get_about().map(|s| s.to_string()),
511            help_long: cmd.get_long_about().map(|s| s.to_string()),
512            before_help: cmd.get_before_help().map(|s| s.to_string()),
513            before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
514            after_help: cmd.get_after_help().map(|s| s.to_string()),
515            after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
516            ..Default::default()
517        };
518        for alias in cmd.get_visible_aliases() {
519            spec.aliases.push(alias.to_string());
520        }
521        for alias in cmd.get_all_aliases() {
522            if spec.aliases.contains(&alias.to_string()) {
523                continue;
524            }
525            spec.hidden_aliases.push(alias.to_string());
526        }
527        for arg in cmd.get_arguments() {
528            if arg.is_positional() {
529                spec.args.push(arg.into())
530            } else {
531                spec.flags.push(arg.into())
532            }
533        }
534        spec.subcommand_required = cmd.is_subcommand_required_set();
535        for subcmd in cmd.get_subcommands() {
536            let mut scmd: SpecCommand = subcmd.into();
537            scmd.name = subcmd.get_name().to_string();
538            spec.subcommands.insert(scmd.name.clone(), scmd);
539        }
540        spec
541    }
542}
543
544#[cfg(feature = "clap")]
545impl From<clap::Command> for Spec {
546    fn from(cmd: clap::Command) -> Self {
547        (&cmd).into()
548    }
549}