usage/spec/
cmd.rs

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