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