1use crate::core::command_policy::VisibilityMode;
27
28#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct CommandDef {
31 pub name: String,
33 pub about: Option<String>,
35 pub long_about: Option<String>,
37 pub usage: Option<String>,
39 pub before_help: Option<String>,
41 pub after_help: Option<String>,
43 pub aliases: Vec<String>,
45 pub hidden: bool,
47 pub sort_key: Option<String>,
49 pub policy: CommandPolicyDef,
51 pub args: Vec<ArgDef>,
53 pub flags: Vec<FlagDef>,
55 pub subcommands: Vec<CommandDef>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct CommandPolicyDef {
62 pub visibility: VisibilityMode,
64 pub required_capabilities: Vec<String>,
66 pub feature_flags: Vec<String>,
68}
69
70impl Default for CommandPolicyDef {
71 fn default() -> Self {
72 Self {
73 visibility: VisibilityMode::Public,
74 required_capabilities: Vec::new(),
75 feature_flags: Vec::new(),
76 }
77 }
78}
79
80impl CommandPolicyDef {
81 pub fn is_empty(&self) -> bool {
99 self.visibility == VisibilityMode::Public
100 && self.required_capabilities.is_empty()
101 && self.feature_flags.is_empty()
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Default)]
107pub struct ArgDef {
108 pub id: String,
110 pub value_name: Option<String>,
112 pub help: Option<String>,
114 pub help_heading: Option<String>,
116 pub required: bool,
118 pub multi: bool,
120 pub value_kind: Option<ValueKind>,
122 pub choices: Vec<ValueChoice>,
124 pub defaults: Vec<String>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Default)]
130pub struct FlagDef {
131 pub id: String,
133 pub short: Option<char>,
135 pub long: Option<String>,
137 pub aliases: Vec<String>,
139 pub help: Option<String>,
141 pub help_heading: Option<String>,
143 pub takes_value: bool,
145 pub value_name: Option<String>,
147 pub required: bool,
149 pub multi: bool,
151 pub hidden: bool,
153 pub value_kind: Option<ValueKind>,
155 pub choices: Vec<ValueChoice>,
157 pub defaults: Vec<String>,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum ValueKind {
164 Path,
166 Enum,
168 FreeText,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Default)]
174pub struct ValueChoice {
175 pub value: String,
177 pub help: Option<String>,
179 pub display: Option<String>,
181 pub sort_key: Option<String>,
183}
184
185impl CommandDef {
186 pub fn new(name: impl Into<String>) -> Self {
213 Self {
214 name: name.into(),
215 ..Self::default()
216 }
217 }
218
219 pub fn about(mut self, about: impl Into<String>) -> Self {
221 self.about = Some(about.into());
222 self
223 }
224
225 pub fn long_about(mut self, long_about: impl Into<String>) -> Self {
227 self.long_about = Some(long_about.into());
228 self
229 }
230
231 pub fn usage(mut self, usage: impl Into<String>) -> Self {
233 self.usage = Some(usage.into());
234 self
235 }
236
237 pub fn before_help(mut self, text: impl Into<String>) -> Self {
239 self.before_help = Some(text.into());
240 self
241 }
242
243 pub fn after_help(mut self, text: impl Into<String>) -> Self {
245 self.after_help = Some(text.into());
246 self
247 }
248
249 pub fn alias(mut self, alias: impl Into<String>) -> Self {
251 self.aliases.push(alias.into());
252 self
253 }
254
255 pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
257 self.aliases.extend(aliases.into_iter().map(Into::into));
258 self
259 }
260
261 pub fn hidden(mut self) -> Self {
263 self.hidden = true;
264 self
265 }
266
267 pub fn sort(mut self, sort_key: impl Into<String>) -> Self {
269 self.sort_key = Some(sort_key.into());
270 self
271 }
272
273 pub fn policy(mut self, policy: CommandPolicyDef) -> Self {
275 self.policy = policy;
276 self
277 }
278
279 pub fn arg(mut self, arg: ArgDef) -> Self {
281 self.args.push(arg);
282 self
283 }
284
285 pub fn args(mut self, args: impl IntoIterator<Item = ArgDef>) -> Self {
287 self.args.extend(args);
288 self
289 }
290
291 pub fn flag(mut self, flag: FlagDef) -> Self {
293 self.flags.push(flag);
294 self
295 }
296
297 pub fn flags(mut self, flags: impl IntoIterator<Item = FlagDef>) -> Self {
299 self.flags.extend(flags);
300 self
301 }
302
303 pub fn subcommand(mut self, subcommand: CommandDef) -> Self {
305 self.subcommands.push(subcommand);
306 self
307 }
308
309 pub fn subcommands(mut self, subcommands: impl IntoIterator<Item = CommandDef>) -> Self {
311 self.subcommands.extend(subcommands);
312 self
313 }
314}
315
316impl ArgDef {
317 pub fn new(id: impl Into<String>) -> Self {
319 Self {
320 id: id.into(),
321 ..Self::default()
322 }
323 }
324
325 pub fn value_name(mut self, value_name: impl Into<String>) -> Self {
327 self.value_name = Some(value_name.into());
328 self
329 }
330
331 pub fn help(mut self, help: impl Into<String>) -> Self {
333 self.help = Some(help.into());
334 self
335 }
336
337 pub fn required(mut self) -> Self {
339 self.required = true;
340 self
341 }
342
343 pub fn multi(mut self) -> Self {
345 self.multi = true;
346 self
347 }
348
349 pub fn value_kind(mut self, value_kind: ValueKind) -> Self {
351 self.value_kind = Some(value_kind);
352 self
353 }
354
355 pub fn choices(mut self, choices: impl IntoIterator<Item = ValueChoice>) -> Self {
357 self.choices.extend(choices);
358 self
359 }
360
361 pub fn defaults(mut self, defaults: impl IntoIterator<Item = impl Into<String>>) -> Self {
363 self.defaults.extend(defaults.into_iter().map(Into::into));
364 self
365 }
366}
367
368impl FlagDef {
369 pub fn new(id: impl Into<String>) -> Self {
371 Self {
372 id: id.into(),
373 ..Self::default()
374 }
375 }
376
377 pub fn short(mut self, short: char) -> Self {
379 self.short = Some(short);
380 self
381 }
382
383 pub fn long(mut self, long: impl Into<String>) -> Self {
385 self.long = Some(long.into());
386 self
387 }
388
389 pub fn alias(mut self, alias: impl Into<String>) -> Self {
391 self.aliases.push(alias.into());
392 self
393 }
394
395 pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
397 self.aliases.extend(aliases.into_iter().map(Into::into));
398 self
399 }
400
401 pub fn help(mut self, help: impl Into<String>) -> Self {
403 self.help = Some(help.into());
404 self
405 }
406
407 pub fn takes_value(mut self, value_name: impl Into<String>) -> Self {
409 self.takes_value = true;
410 self.value_name = Some(value_name.into());
411 self
412 }
413
414 pub fn required(mut self) -> Self {
416 self.required = true;
417 self
418 }
419
420 pub fn multi(mut self) -> Self {
422 self.multi = true;
423 self
424 }
425
426 pub fn hidden(mut self) -> Self {
428 self.hidden = true;
429 self
430 }
431
432 pub fn value_kind(mut self, value_kind: ValueKind) -> Self {
434 self.value_kind = Some(value_kind);
435 self
436 }
437
438 pub fn choices(mut self, choices: impl IntoIterator<Item = ValueChoice>) -> Self {
440 self.choices.extend(choices);
441 self
442 }
443
444 pub fn defaults(mut self, defaults: impl IntoIterator<Item = impl Into<String>>) -> Self {
446 self.defaults.extend(defaults.into_iter().map(Into::into));
447 self
448 }
449
450 pub fn takes_no_value(mut self) -> Self {
452 self.takes_value = false;
453 self.value_name = None;
454 self
455 }
456}
457
458impl ValueChoice {
459 pub fn new(value: impl Into<String>) -> Self {
477 Self {
478 value: value.into(),
479 ..Self::default()
480 }
481 }
482
483 pub fn help(mut self, help: impl Into<String>) -> Self {
485 self.help = Some(help.into());
486 self
487 }
488
489 pub fn display(mut self, display: impl Into<String>) -> Self {
491 self.display = Some(display.into());
492 self
493 }
494
495 pub fn sort(mut self, sort_key: impl Into<String>) -> Self {
497 self.sort_key = Some(sort_key.into());
498 self
499 }
500}
501
502#[cfg(feature = "clap")]
503impl CommandDef {
504 pub fn from_clap(command: clap::Command) -> Self {
506 clap_command_to_def(command)
507 }
508}
509
510#[cfg(feature = "clap")]
511fn clap_command_to_def(command: clap::Command) -> CommandDef {
512 let mut usage_command = command.clone();
513 let usage = normalize_usage_line(usage_command.render_usage().to_string());
514
515 CommandDef {
516 name: command.get_name().to_string(),
517 about: styled_to_plain(command.get_about()),
518 long_about: styled_to_plain(command.get_long_about()),
519 usage,
520 before_help: styled_to_plain(
521 command
522 .get_before_long_help()
523 .or_else(|| command.get_before_help()),
524 ),
525 after_help: styled_to_plain(
526 command
527 .get_after_long_help()
528 .or_else(|| command.get_after_help()),
529 ),
530 aliases: command
531 .get_visible_aliases()
532 .map(ToString::to_string)
533 .collect(),
534 hidden: command.is_hide_set(),
535 sort_key: None,
536 policy: CommandPolicyDef::default(),
537 args: command
538 .get_positionals()
539 .filter(|arg| !arg.is_hide_set())
540 .map(arg_def_from_clap)
541 .collect(),
542 flags: command
543 .get_arguments()
544 .filter(|arg| !arg.is_positional() && !arg.is_hide_set())
545 .map(flag_def_from_clap)
546 .collect(),
547 subcommands: command
548 .get_subcommands()
549 .filter(|subcommand| !subcommand.is_hide_set())
550 .map(|subcommand| clap_command_to_def(subcommand.clone()))
551 .collect(),
552 }
553}
554
555#[cfg(feature = "clap")]
556fn arg_def_from_clap(arg: &clap::Arg) -> ArgDef {
557 ArgDef {
558 id: arg.get_id().as_str().to_string(),
559 value_name: arg
560 .get_value_names()
561 .and_then(|names| names.first())
562 .map(ToString::to_string),
563 help: styled_to_plain(arg.get_long_help().or_else(|| arg.get_help())),
564 help_heading: arg.get_help_heading().map(ToString::to_string),
565 required: arg.is_required_set(),
566 multi: arg.get_num_args().is_some_and(range_is_multiple)
567 || matches!(arg.get_action(), clap::ArgAction::Append),
568 value_kind: value_kind_from_hint(arg.get_value_hint()),
569 choices: arg
570 .get_possible_values()
571 .into_iter()
572 .filter(|value| !value.is_hide_set())
573 .map(|value| {
574 let mut choice = ValueChoice::new(value.get_name());
575 if let Some(help) = value.get_help() {
576 choice = choice.help(help.to_string());
577 }
578 choice
579 })
580 .collect(),
581 defaults: arg
582 .get_default_values()
583 .iter()
584 .map(|value| value.to_string_lossy().to_string())
585 .collect(),
586 }
587}
588
589#[cfg(feature = "clap")]
590fn flag_def_from_clap(arg: &clap::Arg) -> FlagDef {
591 let aliases = arg
592 .get_long_and_visible_aliases()
593 .into_iter()
594 .flatten()
595 .filter(|alias| Some(*alias) != arg.get_long())
596 .map(|alias| format!("--{alias}"))
597 .chain(
598 arg.get_short_and_visible_aliases()
599 .into_iter()
600 .flatten()
601 .filter(|alias| Some(*alias) != arg.get_short())
602 .map(|alias| format!("-{alias}")),
603 )
604 .collect::<Vec<_>>();
605
606 FlagDef {
607 id: arg.get_id().as_str().to_string(),
608 short: arg.get_short(),
609 long: arg.get_long().map(ToString::to_string),
610 aliases,
611 help: styled_to_plain(arg.get_long_help().or_else(|| arg.get_help())),
612 help_heading: arg.get_help_heading().map(ToString::to_string),
613 takes_value: arg.get_action().takes_values(),
614 value_name: arg
615 .get_value_names()
616 .and_then(|names| names.first())
617 .map(ToString::to_string),
618 required: arg.is_required_set(),
619 multi: arg.get_num_args().is_some_and(range_is_multiple)
620 || matches!(arg.get_action(), clap::ArgAction::Append),
621 hidden: arg.is_hide_set(),
622 value_kind: value_kind_from_hint(arg.get_value_hint()),
623 choices: arg
624 .get_possible_values()
625 .into_iter()
626 .filter(|value| !value.is_hide_set())
627 .map(|value| {
628 let mut choice = ValueChoice::new(value.get_name());
629 if let Some(help) = value.get_help() {
630 choice = choice.help(help.to_string());
631 }
632 choice
633 })
634 .collect(),
635 defaults: arg
636 .get_default_values()
637 .iter()
638 .map(|value| value.to_string_lossy().to_string())
639 .collect(),
640 }
641}
642
643#[cfg(feature = "clap")]
644fn styled_to_plain(value: Option<&clap::builder::StyledStr>) -> Option<String> {
645 value
646 .map(ToString::to_string)
647 .map(|text| text.trim().to_string())
648 .filter(|text| !text.is_empty())
649}
650
651#[cfg(feature = "clap")]
652fn range_is_multiple(range: clap::builder::ValueRange) -> bool {
653 range.min_values() > 1 || range.max_values() > 1
654}
655
656#[cfg(feature = "clap")]
657fn value_kind_from_hint(hint: clap::ValueHint) -> Option<ValueKind> {
658 match hint {
659 clap::ValueHint::AnyPath
660 | clap::ValueHint::FilePath
661 | clap::ValueHint::DirPath
662 | clap::ValueHint::ExecutablePath => Some(ValueKind::Path),
663 _ => None,
664 }
665}
666
667#[cfg(feature = "clap")]
668fn normalize_usage_line(value: String) -> Option<String> {
669 value
670 .trim()
671 .strip_prefix("Usage:")
672 .map(str::trim)
673 .filter(|usage| !usage.is_empty())
674 .map(ToString::to_string)
675}
676
677#[cfg(test)]
678mod tests;