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