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::{string_entry, 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};
15use serde::Serialize;
16
17#[derive(Debug, Serialize, Clone)]
36pub struct SpecCommand {
37 pub full_cmd: Vec<String>,
39 pub usage: String,
41 pub subcommands: IndexMap<String, SpecCommand>,
43 pub args: Vec<SpecArg>,
45 pub flags: Vec<SpecFlag>,
47 pub mounts: Vec<SpecMount>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub deprecated: Option<String>,
52 pub hide: bool,
54 #[serde(skip)]
64 pub mounted: bool,
65 #[serde(skip)]
72 pub flags_from_mount: bool,
73 #[serde(skip_serializing_if = "is_false")]
75 pub subcommand_required: bool,
76 #[serde(skip_serializing_if = "Option::is_none")]
79 pub restart_token: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub help: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub help_long: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub help_md: Option<String>,
89 pub name: String,
91 pub aliases: Vec<String>,
93 pub hidden_aliases: Vec<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub before_help: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub before_help_long: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub before_help_md: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub after_help: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub after_help_long: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub after_help_md: Option<String>,
113 pub examples: Vec<SpecExample>,
115 #[serde(skip_serializing_if = "IndexMap::is_empty")]
117 pub complete: IndexMap<String, SpecComplete>,
118
119 #[serde(skip)]
121 subcommand_lookup: OnceLock<HashMap<String, String>>,
122}
123
124impl Default for SpecCommand {
125 fn default() -> Self {
126 Self {
127 full_cmd: vec![],
128 usage: "".to_string(),
129 subcommands: IndexMap::new(),
130 args: vec![],
131 flags: vec![],
132 mounts: vec![],
133 deprecated: None,
134 hide: false,
135 mounted: false,
136 flags_from_mount: false,
137 subcommand_required: false,
138 restart_token: None,
139 help: None,
140 help_long: None,
141 help_md: None,
142 name: "".to_string(),
143 aliases: vec![],
144 hidden_aliases: vec![],
145 before_help: None,
146 before_help_long: None,
147 before_help_md: None,
148 after_help: None,
149 after_help_long: None,
150 after_help_md: None,
151 examples: vec![],
152 subcommand_lookup: OnceLock::new(),
153 complete: IndexMap::new(),
154 }
155 }
156}
157
158#[derive(Debug, Default, Serialize, Clone)]
159pub struct SpecExample {
160 pub code: String,
161 pub header: Option<String>,
162 pub help: Option<String>,
163 pub lang: String,
164}
165
166impl SpecExample {
167 pub(crate) fn new(code: String) -> Self {
168 Self {
169 code,
170 ..Default::default()
171 }
172 }
173}
174
175impl From<&SpecExample> for KdlNode {
176 fn from(example: &SpecExample) -> KdlNode {
177 let mut node = KdlNode::new("example");
178 node.push(string_entry(None, &example.code));
179 if let Some(header) = &example.header {
180 node.push(string_entry(Some("header"), header));
181 }
182 if let Some(help) = &example.help {
183 node.push(string_entry(Some("help"), help));
184 }
185 if !example.lang.is_empty() {
186 node.push(string_entry(Some("lang"), &example.lang));
187 }
188 node
189 }
190}
191
192impl SpecCommand {
193 pub fn builder() -> SpecCommandBuilder {
195 SpecCommandBuilder::new()
196 }
197
198 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
199 node.ensure_arg_len(1..=1)?;
200 let mut cmd = Self {
201 name: node.arg(0)?.ensure_string()?.to_string(),
202 ..Default::default()
203 };
204 for (k, v) in node.props() {
205 match k {
206 "help" => cmd.help = Some(v.ensure_string()?),
207 "long_help" => cmd.help_long = Some(v.ensure_string()?),
208 "help_long" => cmd.help_long = Some(v.ensure_string()?),
209 "help_md" => cmd.help_md = Some(v.ensure_string()?),
210 "before_help" => cmd.before_help = Some(v.ensure_string()?),
211 "before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
212 "before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
213 "before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
214 "after_help" => cmd.after_help = Some(v.ensure_string()?),
215 "after_long_help" => {
216 cmd.after_help_long = Some(v.ensure_string()?);
217 }
218 "after_help_long" => {
219 cmd.after_help_long = Some(v.ensure_string()?);
220 }
221 "after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
222 "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
223 "hide" => cmd.hide = v.ensure_bool()?,
224 "restart_token" => cmd.restart_token = Some(v.ensure_string()?),
225 "deprecated" => {
226 cmd.deprecated = match v.value.as_bool() {
227 Some(true) => Some("deprecated".to_string()),
228 Some(false) => None,
229 None => Some(v.ensure_string()?),
230 }
231 }
232 k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
233 }
234 }
235 for child in node.children() {
236 match child.name() {
237 "flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
238 "arg" => cmd.args.push(SpecArg::parse(ctx, &child)?),
239 "mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
240 "cmd" => {
241 let node = SpecCommand::parse(ctx, &child)?;
242 cmd.subcommands.insert(node.name.to_string(), node);
243 }
244 "alias" => {
245 let alias = child
246 .ensure_arg_len(1..)?
247 .args()
248 .map(|e| e.ensure_string())
249 .collect::<Result<Vec<_>, _>>()?;
250 let hide = child
251 .get("hide")
252 .map(|n| n.ensure_bool())
253 .unwrap_or(Ok(false))?;
254 if hide {
255 cmd.hidden_aliases.extend(alias);
256 } else {
257 cmd.aliases.extend(alias);
258 }
259 }
260 "example" => {
261 let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
262 let mut example = SpecExample::new(code.trim().to_string());
263 for (k, v) in child.props() {
264 match k {
265 "header" => example.header = Some(v.ensure_string()?),
266 "help" => example.help = Some(v.ensure_string()?),
267 "lang" => example.lang = v.ensure_string()?,
268 k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
269 }
270 }
271 cmd.examples.push(example);
272 }
273 "help" => {
274 cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
275 }
276 "long_help" => {
277 cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
278 }
279 "before_help" => {
280 cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
281 }
282 "before_long_help" => {
283 cmd.before_help_long =
284 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
285 }
286 "after_help" => {
287 cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
288 }
289 "after_long_help" => {
290 cmd.after_help_long =
291 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
292 }
293 "subcommand_required" => {
294 cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
295 }
296 "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
297 "restart_token" => {
298 cmd.restart_token = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?)
299 }
300 "deprecated" => {
301 cmd.deprecated = match child.arg(0)?.value.as_bool() {
302 Some(true) => Some("deprecated".to_string()),
303 Some(false) => None,
304 None => Some(child.arg(0)?.ensure_string()?),
305 }
306 }
307 "complete" => {
308 let complete = SpecComplete::parse(ctx, &child)?;
309 cmd.complete.insert(complete.name.clone(), complete);
310 }
311 k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
312 }
313 }
314 Ok(cmd)
315 }
316 pub(crate) fn is_empty(&self) -> bool {
317 self.args.is_empty()
318 && self.flags.is_empty()
319 && self.mounts.is_empty()
320 && self.subcommands.is_empty()
321 }
322 pub fn usage(&self) -> String {
323 let mut usage = self.full_cmd.join(" ");
324 let flags = self.flags.iter().filter(|f| !f.hide).collect_vec();
325 let args = self.args.iter().filter(|a| !a.hide).collect_vec();
326 if !flags.is_empty() {
327 if flags.len() <= 2 {
328 let inlines = flags
329 .iter()
330 .map(|f| {
331 if f.required {
332 format!("<{}>", f.usage())
333 } else {
334 format!("[{}]", f.usage())
335 }
336 })
337 .join(" ");
338 usage = format!("{usage} {inlines}").trim().to_string();
339 } else if flags.iter().any(|f| f.required) {
340 usage = format!("{usage} <FLAGS>");
341 } else {
342 usage = format!("{usage} [FLAGS]");
343 }
344 }
345 if !args.is_empty() {
346 if args.len() <= 2 {
347 let inlines = args.iter().map(|a| a.usage()).join(" ");
348 usage = format!("{usage} {inlines}").trim().to_string();
349 } else if args.iter().any(|a| a.required) {
350 usage = format!("{usage} <ARGS>…");
351 } else {
352 usage = format!("{usage} [ARGS]…");
353 }
354 }
355 if !self.subcommands.is_empty() {
360 usage = format!("{usage} <SUBCOMMAND>");
361 }
362 usage.trim().to_string()
363 }
364 pub(crate) fn merge(&mut self, other: Self) {
365 if !other.name.is_empty() {
366 self.name = other.name;
367 }
368 if other.help.is_some() {
369 self.help = other.help;
370 }
371 if other.help_long.is_some() {
372 self.help_long = other.help_long;
373 }
374 if other.help_md.is_some() {
375 self.help_md = other.help_md;
376 }
377 if other.before_help.is_some() {
378 self.before_help = other.before_help;
379 }
380 if other.before_help_long.is_some() {
381 self.before_help_long = other.before_help_long;
382 }
383 if other.before_help_md.is_some() {
384 self.before_help_md = other.before_help_md;
385 }
386 if other.after_help.is_some() {
387 self.after_help = other.after_help;
388 }
389 if other.after_help_long.is_some() {
390 self.after_help_long = other.after_help_long;
391 }
392 if other.after_help_md.is_some() {
393 self.after_help_md = other.after_help_md;
394 }
395 if !other.args.is_empty() {
396 self.args = other.args;
397 }
398 if !other.flags.is_empty() {
399 self.flags = other.flags;
400 }
401 if !other.mounts.is_empty() {
402 self.mounts = other.mounts;
403 }
404 if !other.aliases.is_empty() {
405 self.aliases = other.aliases;
406 }
407 if !other.hidden_aliases.is_empty() {
408 self.hidden_aliases = other.hidden_aliases;
409 }
410 if !other.examples.is_empty() {
411 self.examples = other.examples;
412 }
413 self.hide = other.hide;
414 self.subcommand_required = other.subcommand_required;
415 if other.restart_token.is_some() {
416 self.restart_token = other.restart_token;
417 }
418 for (name, cmd) in other.subcommands {
419 self.subcommands.insert(name, cmd);
420 }
421 for (name, complete) in other.complete {
422 self.complete.insert(name, complete);
423 }
424 }
425
426 pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
427 let mut cmds = vec![];
428 for cmd in self.subcommands.values() {
429 cmds.push(cmd);
430 cmds.extend(cmd.all_subcommands());
431 }
432 cmds
433 }
434
435 pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
436 let sl = self.subcommand_lookup.get_or_init(|| {
437 let mut map = HashMap::new();
438 for (name, cmd) in &self.subcommands {
439 map.insert(name.clone(), name.clone());
440 for alias in &cmd.aliases {
441 map.insert(alias.clone(), name.clone());
442 }
443 for alias in &cmd.hidden_aliases {
444 map.insert(alias.clone(), name.clone());
445 }
446 }
447 map
448 });
449 let name = sl.get(name)?;
450 self.subcommands.get(name)
451 }
452
453 pub(crate) fn mount(&mut self, global_flag_args: &[String]) -> Result<(), UsageErr> {
454 for mount in self.mounts.iter().cloned().collect_vec() {
455 let cmd = if global_flag_args.is_empty() {
456 mount.run.clone()
457 } else {
458 let mut tokens = shell_words::split(&mount.run)
462 .expect("mount command should be valid shell syntax");
463 if !tokens.is_empty() {
464 tokens.splice(1..1, global_flag_args.iter().cloned());
466 }
467 shell_words::join(tokens)
469 };
470 let output = sh(&cmd)?;
471 let mut spec: Spec = output.parse()?;
472 for cmd in spec.cmd.subcommands.values_mut() {
475 cmd.mark_mounted();
476 }
477 self.flags_from_mount |= !spec.cmd.flags.is_empty();
480 self.merge(spec.cmd);
481 }
482 Ok(())
483 }
484
485 pub(crate) fn mark_mounted(&mut self) {
487 self.mounted = true;
488 for cmd in self.subcommands.values_mut() {
489 cmd.mark_mounted();
490 }
491 }
492}
493
494impl From<&SpecCommand> for KdlNode {
495 fn from(cmd: &SpecCommand) -> Self {
496 let mut node = Self::new("cmd");
497 node.entries_mut().push(cmd.name.clone().into());
498 if cmd.hide {
499 node.entries_mut().push(KdlEntry::new_prop("hide", true));
500 }
501 if cmd.subcommand_required {
502 node.entries_mut()
503 .push(KdlEntry::new_prop("subcommand_required", true));
504 }
505 if let Some(restart_token) = &cmd.restart_token {
506 node.entries_mut()
507 .push(KdlEntry::new_prop("restart_token", restart_token.clone()));
508 }
509 if !cmd.aliases.is_empty() {
510 let mut aliases = KdlNode::new("alias");
511 for alias in &cmd.aliases {
512 aliases.entries_mut().push(alias.clone().into());
513 }
514 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
515 children.nodes_mut().push(aliases);
516 }
517 if !cmd.hidden_aliases.is_empty() {
518 let mut aliases = KdlNode::new("alias");
519 for alias in &cmd.hidden_aliases {
520 aliases.entries_mut().push(alias.clone().into());
521 }
522 aliases.entries_mut().push(KdlEntry::new_prop("hide", true));
523 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
524 children.nodes_mut().push(aliases);
525 }
526 if let Some(help) = &cmd.help {
527 node.entries_mut().push(string_entry(Some("help"), help));
528 }
529 if let Some(help) = &cmd.help_long {
530 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
531 let mut node = KdlNode::new("long_help");
532 node.push(string_entry(None, help));
533 children.nodes_mut().push(node);
534 }
535 if let Some(help) = &cmd.help_md {
536 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
537 let mut node = KdlNode::new("help_md");
538 node.push(string_entry(None, help));
539 children.nodes_mut().push(node);
540 }
541 if let Some(help) = &cmd.before_help {
542 node.entries_mut()
543 .push(string_entry(Some("before_help"), help));
544 }
545 if let Some(help) = &cmd.before_help_long {
546 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
547 let mut node = KdlNode::new("before_long_help");
548 node.push(string_entry(None, help));
549 children.nodes_mut().push(node);
550 }
551 if let Some(help) = &cmd.before_help_md {
552 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
553 let mut node = KdlNode::new("before_help_md");
554 node.push(string_entry(None, help));
555 children.nodes_mut().push(node);
556 }
557 if let Some(help) = &cmd.after_help {
558 node.entries_mut()
559 .push(string_entry(Some("after_help"), help));
560 }
561 if let Some(help) = &cmd.after_help_long {
562 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
563 let mut node = KdlNode::new("after_long_help");
564 node.push(string_entry(None, help));
565 children.nodes_mut().push(node);
566 }
567 if let Some(help) = &cmd.after_help_md {
568 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
569 let mut node = KdlNode::new("after_help_md");
570 node.push(string_entry(None, help));
571 children.nodes_mut().push(node);
572 }
573 if let Some(deprecated) = &cmd.deprecated {
574 node.entries_mut()
575 .push(string_entry(Some("deprecated"), deprecated));
576 }
577 for flag in &cmd.flags {
578 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
579 children.nodes_mut().push(flag.into());
580 }
581 for arg in &cmd.args {
582 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
583 children.nodes_mut().push(arg.into());
584 }
585 for mount in &cmd.mounts {
586 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
587 children.nodes_mut().push(mount.into());
588 }
589 for cmd in cmd.subcommands.values() {
590 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
591 children.nodes_mut().push(cmd.into());
592 }
593 for complete in cmd.complete.values() {
594 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
595 children.nodes_mut().push(complete.into());
596 }
597 node
598 }
599}
600
601#[cfg(feature = "clap")]
602impl From<&clap::Command> for SpecCommand {
603 fn from(cmd: &clap::Command) -> Self {
604 let mut spec = Self {
605 name: cmd.get_name().to_string(),
606 hide: cmd.is_hide_set(),
607 help: cmd.get_about().map(|s| s.to_string()),
608 help_long: cmd.get_long_about().map(|s| s.to_string()),
609 before_help: cmd.get_before_help().map(|s| s.to_string()),
610 before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
611 after_help: cmd.get_after_help().map(|s| s.to_string()),
612 after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
613 ..Default::default()
614 };
615 for alias in cmd.get_visible_aliases() {
616 spec.aliases.push(alias.to_string());
617 }
618 for alias in cmd.get_all_aliases() {
619 if spec.aliases.contains(&alias.to_string()) {
620 continue;
621 }
622 spec.hidden_aliases.push(alias.to_string());
623 }
624 for arg in cmd.get_arguments() {
625 if arg.is_positional() {
626 spec.args.push(arg.into())
627 } else {
628 spec.flags.push(arg.into())
629 }
630 }
631 spec.subcommand_required = cmd.is_subcommand_required_set();
632 for subcmd in cmd.get_subcommands() {
633 let mut scmd: SpecCommand = subcmd.into();
634 scmd.name = subcmd.get_name().to_string();
635 spec.subcommands.insert(scmd.name.clone(), scmd);
636 }
637 spec
638 }
639}
640
641#[cfg(feature = "clap")]
642impl From<clap::Command> for Spec {
643 fn from(cmd: clap::Command) -> Self {
644 (&cmd).into()
645 }
646}