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