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 SpecCommand {
108 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
109 node.ensure_arg_len(1..=1)?;
110 let mut cmd = Self {
111 name: node.arg(0)?.ensure_string()?.to_string(),
112 ..Default::default()
113 };
114 for (k, v) in node.props() {
115 match k {
116 "help" => cmd.help = Some(v.ensure_string()?),
117 "long_help" => cmd.help_long = Some(v.ensure_string()?),
118 "help_long" => cmd.help_long = Some(v.ensure_string()?),
119 "help_md" => cmd.help_md = Some(v.ensure_string()?),
120 "before_help" => cmd.before_help = Some(v.ensure_string()?),
121 "before_long_help" => cmd.before_help_long = Some(v.ensure_string()?),
122 "before_help_long" => cmd.before_help_long = Some(v.ensure_string()?),
123 "before_help_md" => cmd.before_help_md = Some(v.ensure_string()?),
124 "after_help" => cmd.after_help = Some(v.ensure_string()?),
125 "after_long_help" => {
126 cmd.after_help_long = Some(v.ensure_string()?);
127 }
128 "after_help_long" => {
129 cmd.after_help_long = Some(v.ensure_string()?);
130 }
131 "after_help_md" => cmd.after_help_md = Some(v.ensure_string()?),
132 "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?,
133 "hide" => cmd.hide = v.ensure_bool()?,
134 "deprecated" => {
135 cmd.deprecated = match v.value.as_bool() {
136 Some(true) => Some("deprecated".to_string()),
137 Some(false) => None,
138 None => Some(v.ensure_string()?),
139 }
140 }
141 k => bail_parse!(ctx, v.entry.span(), "unsupported cmd prop {k}"),
142 }
143 }
144 for child in node.children() {
145 match child.name() {
146 "flag" => cmd.flags.push(SpecFlag::parse(ctx, &child)?),
147 "arg" => cmd.args.push(SpecArg::parse(ctx, &child)?),
148 "mount" => cmd.mounts.push(SpecMount::parse(ctx, &child)?),
149 "cmd" => {
150 let node = SpecCommand::parse(ctx, &child)?;
151 cmd.subcommands.insert(node.name.to_string(), node);
152 }
153 "alias" => {
154 let alias = child
155 .ensure_arg_len(1..)?
156 .args()
157 .map(|e| e.ensure_string())
158 .collect::<Result<Vec<_>, _>>()?;
159 let hide = child
160 .get("hide")
161 .map(|n| n.ensure_bool())
162 .unwrap_or(Ok(false))?;
163 if hide {
164 cmd.hidden_aliases.extend(alias);
165 } else {
166 cmd.aliases.extend(alias);
167 }
168 }
169 "example" => {
170 let code = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?;
171 let mut example = SpecExample::new(code.trim().to_string());
172 for (k, v) in child.props() {
173 match k {
174 "header" => example.header = Some(v.ensure_string()?),
175 "help" => example.help = Some(v.ensure_string()?),
176 "lang" => example.lang = v.ensure_string()?,
177 k => bail_parse!(ctx, v.entry.span(), "unsupported example key {k}"),
178 }
179 }
180 cmd.examples.push(example);
181 }
182 "help" => {
183 cmd.help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
184 }
185 "long_help" => {
186 cmd.help_long = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
187 }
188 "before_help" => {
189 cmd.before_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
190 }
191 "before_long_help" => {
192 cmd.before_help_long =
193 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
194 }
195 "after_help" => {
196 cmd.after_help = Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
197 }
198 "after_long_help" => {
199 cmd.after_help_long =
200 Some(child.ensure_arg_len(1..=1)?.arg(0)?.ensure_string()?);
201 }
202 "subcommand_required" => {
203 cmd.subcommand_required = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?
204 }
205 "hide" => cmd.hide = child.ensure_arg_len(1..=1)?.arg(0)?.ensure_bool()?,
206 "deprecated" => {
207 cmd.deprecated = match child.arg(0)?.value.as_bool() {
208 Some(true) => Some("deprecated".to_string()),
209 Some(false) => None,
210 None => Some(child.arg(0)?.ensure_string()?),
211 }
212 }
213 "complete" => {
214 let complete = SpecComplete::parse(ctx, &child)?;
215 cmd.complete.insert(complete.name.clone(), complete);
216 }
217 k => bail_parse!(ctx, child.node.name().span(), "unsupported cmd key {k}"),
218 }
219 }
220 Ok(cmd)
221 }
222 pub(crate) fn is_empty(&self) -> bool {
223 self.args.is_empty()
224 && self.flags.is_empty()
225 && self.mounts.is_empty()
226 && self.subcommands.is_empty()
227 }
228 pub fn usage(&self) -> String {
229 let mut usage = self.full_cmd.join(" ");
230 let flags = self.flags.iter().filter(|f| !f.hide).collect_vec();
231 let args = self.args.iter().filter(|a| !a.hide).collect_vec();
232 if !flags.is_empty() {
233 if flags.len() <= 2 {
234 let inlines = flags
235 .iter()
236 .map(|f| {
237 if f.required {
238 format!("<{}>", f.usage())
239 } else {
240 format!("[{}]", f.usage())
241 }
242 })
243 .join(" ");
244 usage = format!("{usage} {inlines}").trim().to_string();
245 } else if flags.iter().any(|f| f.required) {
246 usage = format!("{usage} <FLAGS>");
247 } else {
248 usage = format!("{usage} [FLAGS]");
249 }
250 }
251 if !args.is_empty() {
252 if args.len() <= 2 {
253 let inlines = args.iter().map(|a| a.usage()).join(" ");
254 usage = format!("{usage} {inlines}").trim().to_string();
255 } else if args.iter().any(|a| a.required) {
256 usage = format!("{usage} <ARGS>…");
257 } else {
258 usage = format!("{usage} [ARGS]…");
259 }
260 }
261 if !self.subcommands.is_empty() {
266 usage = format!("{usage} <SUBCOMMAND>");
267 }
268 usage.trim().to_string()
269 }
270 pub(crate) fn merge(&mut self, other: Self) {
271 if !other.name.is_empty() {
272 self.name = other.name;
273 }
274 if other.help.is_some() {
275 self.help = other.help;
276 }
277 if other.help_long.is_some() {
278 self.help_long = other.help_long;
279 }
280 if other.help_md.is_some() {
281 self.help_md = other.help_md;
282 }
283 if other.before_help.is_some() {
284 self.before_help = other.before_help;
285 }
286 if other.before_help_long.is_some() {
287 self.before_help_long = other.before_help_long;
288 }
289 if other.before_help_md.is_some() {
290 self.before_help_md = other.before_help_md;
291 }
292 if other.after_help.is_some() {
293 self.after_help = other.after_help;
294 }
295 if other.after_help_long.is_some() {
296 self.after_help_long = other.after_help_long;
297 }
298 if other.after_help_md.is_some() {
299 self.after_help_md = other.after_help_md;
300 }
301 if !other.args.is_empty() {
302 self.args = other.args;
303 }
304 if !other.flags.is_empty() {
305 self.flags = other.flags;
306 }
307 if !other.mounts.is_empty() {
308 self.mounts = other.mounts;
309 }
310 if !other.aliases.is_empty() {
311 self.aliases = other.aliases;
312 }
313 if !other.hidden_aliases.is_empty() {
314 self.hidden_aliases = other.hidden_aliases;
315 }
316 if !other.examples.is_empty() {
317 self.examples = other.examples;
318 }
319 self.hide = other.hide;
320 self.subcommand_required = other.subcommand_required;
321 for (name, cmd) in other.subcommands {
322 self.subcommands.insert(name, cmd);
323 }
324 for (name, complete) in other.complete {
325 self.complete.insert(name, complete);
326 }
327 }
328
329 pub fn all_subcommands(&self) -> Vec<&SpecCommand> {
330 let mut cmds = vec![];
331 for cmd in self.subcommands.values() {
332 cmds.push(cmd);
333 cmds.extend(cmd.all_subcommands());
334 }
335 cmds
336 }
337
338 pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> {
339 let sl = self.subcommand_lookup.get_or_init(|| {
340 let mut map = HashMap::new();
341 for (name, cmd) in &self.subcommands {
342 map.insert(name.clone(), name.clone());
343 for alias in &cmd.aliases {
344 map.insert(alias.clone(), name.clone());
345 }
346 for alias in &cmd.hidden_aliases {
347 map.insert(alias.clone(), name.clone());
348 }
349 }
350 map
351 });
352 let name = sl.get(name)?;
353 self.subcommands.get(name)
354 }
355
356 pub(crate) fn mount(&mut self) -> Result<(), UsageErr> {
357 for mount in self.mounts.iter().cloned().collect_vec() {
358 let output = sh(&mount.run)?;
359 let spec: Spec = output.parse()?;
360 self.merge(spec.cmd);
361 }
362 Ok(())
363 }
364}
365
366impl From<&SpecCommand> for KdlNode {
367 fn from(cmd: &SpecCommand) -> Self {
368 let mut node = Self::new("cmd");
369 node.entries_mut().push(cmd.name.clone().into());
370 if cmd.hide {
371 node.entries_mut().push(KdlEntry::new_prop("hide", true));
372 }
373 if cmd.subcommand_required {
374 node.entries_mut()
375 .push(KdlEntry::new_prop("subcommand_required", true));
376 }
377 if !cmd.aliases.is_empty() {
378 let mut aliases = KdlNode::new("alias");
379 for alias in &cmd.aliases {
380 aliases.entries_mut().push(alias.clone().into());
381 }
382 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
383 children.nodes_mut().push(aliases);
384 }
385 if !cmd.hidden_aliases.is_empty() {
386 let mut aliases = KdlNode::new("alias");
387 for alias in &cmd.hidden_aliases {
388 aliases.entries_mut().push(alias.clone().into());
389 }
390 aliases.entries_mut().push(KdlEntry::new_prop("hide", true));
391 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
392 children.nodes_mut().push(aliases);
393 }
394 if let Some(help) = &cmd.help {
395 node.entries_mut()
396 .push(KdlEntry::new_prop("help", help.clone()));
397 }
398 if let Some(help) = &cmd.help_long {
399 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
400 let mut node = KdlNode::new("long_help");
401 node.insert(0, KdlValue::String(help.clone()));
402 children.nodes_mut().push(node);
403 }
404 if let Some(help) = &cmd.help_md {
405 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
406 let mut node = KdlNode::new("help_md");
407 node.insert(0, KdlValue::String(help.clone()));
408 children.nodes_mut().push(node);
409 }
410 if let Some(help) = &cmd.before_help {
411 node.entries_mut()
412 .push(KdlEntry::new_prop("before_help", help.clone()));
413 }
414 if let Some(help) = &cmd.before_help_long {
415 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
416 let mut node = KdlNode::new("before_long_help");
417 node.insert(0, KdlValue::String(help.clone()));
418 children.nodes_mut().push(node);
419 }
420 if let Some(help) = &cmd.before_help_md {
421 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
422 let mut node = KdlNode::new("before_help_md");
423 node.insert(0, KdlValue::String(help.clone()));
424 children.nodes_mut().push(node);
425 }
426 if let Some(help) = &cmd.after_help {
427 node.entries_mut()
428 .push(KdlEntry::new_prop("after_help", help.clone()));
429 }
430 if let Some(help) = &cmd.after_help_long {
431 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
432 let mut node = KdlNode::new("after_long_help");
433 node.insert(0, KdlValue::String(help.clone()));
434 children.nodes_mut().push(node);
435 }
436 if let Some(help) = &cmd.after_help_md {
437 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
438 let mut node = KdlNode::new("after_help_md");
439 node.insert(0, KdlValue::String(help.clone()));
440 children.nodes_mut().push(node);
441 }
442 for flag in &cmd.flags {
443 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
444 children.nodes_mut().push(flag.into());
445 }
446 for arg in &cmd.args {
447 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
448 children.nodes_mut().push(arg.into());
449 }
450 for mount in &cmd.mounts {
451 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
452 children.nodes_mut().push(mount.into());
453 }
454 for cmd in cmd.subcommands.values() {
455 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
456 children.nodes_mut().push(cmd.into());
457 }
458 for complete in cmd.complete.values() {
459 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
460 children.nodes_mut().push(complete.into());
461 }
462 node
463 }
464}
465
466#[cfg(feature = "clap")]
467impl From<&clap::Command> for SpecCommand {
468 fn from(cmd: &clap::Command) -> Self {
469 let mut spec = Self {
470 name: cmd.get_name().to_string(),
471 hide: cmd.is_hide_set(),
472 help: cmd.get_about().map(|s| s.to_string()),
473 help_long: cmd.get_long_about().map(|s| s.to_string()),
474 before_help: cmd.get_before_help().map(|s| s.to_string()),
475 before_help_long: cmd.get_before_long_help().map(|s| s.to_string()),
476 after_help: cmd.get_after_help().map(|s| s.to_string()),
477 after_help_long: cmd.get_after_long_help().map(|s| s.to_string()),
478 ..Default::default()
479 };
480 for alias in cmd.get_visible_aliases() {
481 spec.aliases.push(alias.to_string());
482 }
483 for alias in cmd.get_all_aliases() {
484 if spec.aliases.contains(&alias.to_string()) {
485 continue;
486 }
487 spec.hidden_aliases.push(alias.to_string());
488 }
489 for arg in cmd.get_arguments() {
490 if arg.is_positional() {
491 spec.args.push(arg.into())
492 } else {
493 spec.flags.push(arg.into())
494 }
495 }
496 spec.subcommand_required = cmd.is_subcommand_required_set();
497 for subcmd in cmd.get_subcommands() {
498 let mut scmd: SpecCommand = subcmd.into();
499 scmd.name = subcmd.get_name().to_string();
500 spec.subcommands.insert(scmd.name.clone(), scmd);
501 }
502 spec
503 }
504}
505
506#[cfg(feature = "clap")]
507impl From<clap::Command> for Spec {
508 fn from(cmd: clap::Command) -> Self {
509 (&cmd).into()
510 }
511}