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 #[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 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 let mut tokens = shell_words::split(&mount.run)
382 .expect("mount command should be valid shell syntax");
383 if !tokens.is_empty() {
384 tokens.splice(1..1, global_flag_args.iter().cloned());
386 }
387 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}