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