1#[derive(Clone, Debug, Default, PartialEq, Eq)]
5pub enum ValueHint {
6 #[default]
8 None,
9 Any,
11 File,
13 Dir,
15 Path,
17 Command,
19 Url,
21 Choices(Vec<Choice>),
23}
24
25#[derive(Clone, Debug, Default, PartialEq, Eq)]
27pub struct Choice {
28 pub value: String,
29 pub help: String,
31}
32
33impl Choice {
34 pub fn new(value: impl Into<String>) -> Self {
35 Choice {
36 value: value.into(),
37 help: String::new(),
38 }
39 }
40 pub fn help(mut self, help: impl Into<String>) -> Self {
41 self.help = help.into();
42 self
43 }
44}
45
46impl From<&str> for Choice {
47 fn from(value: &str) -> Self {
48 Choice::new(value)
49 }
50}
51
52impl<V: Into<String>, H: Into<String>> From<(V, H)> for Choice {
53 fn from((value, help): (V, H)) -> Self {
54 Choice::new(value).help(help)
55 }
56}
57
58#[derive(Clone, Debug, Default, PartialEq, Eq)]
73pub struct ArgSpec {
74 pub id: String,
76 pub long: Option<String>,
78 pub short: Option<char>,
79 pub short_aliases: Vec<char>,
81 pub aliases: Vec<String>,
83 pub value_name: Option<String>,
85 pub value: ValueHint,
86 pub help: String,
87 pub long_help: Option<String>,
89 pub default: Option<String>,
90 pub env: Option<String>,
92 pub config_key: Option<String>,
94 pub required: bool,
95 pub multiple: bool,
97 pub hidden: bool,
99 pub heading: Option<String>,
102 pub positional: bool,
103 pub global: bool,
107}
108
109impl ArgSpec {
110 pub fn new(id: impl Into<String>) -> Self {
112 ArgSpec {
113 id: id.into(),
114 ..Default::default()
115 }
116 }
117
118 pub fn flag(long: impl Into<String>) -> Self {
120 let long = long.into();
121 ArgSpec {
122 id: long.clone(),
123 long: Some(long),
124 ..Default::default()
125 }
126 }
127
128 pub fn option(long: impl Into<String>) -> Self {
130 let long = long.into();
131 ArgSpec {
132 id: long.clone(),
133 value_name: Some(long.to_uppercase().replace('-', "_")),
134 long: Some(long),
135 value: ValueHint::Any,
136 ..Default::default()
137 }
138 }
139
140 pub fn positional(name: impl Into<String>) -> Self {
142 let name = name.into();
143 ArgSpec {
144 value_name: Some(name.to_uppercase().replace('-', "_")),
145 id: name,
146 value: ValueHint::Any,
147 positional: true,
148 ..Default::default()
149 }
150 }
151
152 pub fn long(mut self, long: impl Into<String>) -> Self {
153 self.long = Some(long.into());
154 self
155 }
156 pub fn short(mut self, short: char) -> Self {
157 self.short = Some(short);
158 self
159 }
160 pub fn alias(mut self, alias: impl Into<String>) -> Self {
161 self.aliases.push(alias.into());
162 self
163 }
164 pub fn short_alias(mut self, short: char) -> Self {
166 self.short_aliases.push(short);
167 self
168 }
169 pub fn value_name(mut self, name: impl Into<String>) -> Self {
171 self.value_name = Some(name.into());
172 if self.value == ValueHint::None {
173 self.value = ValueHint::Any;
174 }
175 self
176 }
177 pub fn value(mut self, hint: ValueHint) -> Self {
179 if hint != ValueHint::None && self.value_name.is_none() {
180 self.value_name = Some(self.id.to_uppercase().replace('-', "_"));
181 }
182 self.value = hint;
183 self
184 }
185 pub fn choices<C: Into<Choice>>(self, choices: impl IntoIterator<Item = C>) -> Self {
187 self.value(ValueHint::Choices(
188 choices.into_iter().map(Into::into).collect(),
189 ))
190 }
191 pub fn choice(mut self, value: impl Into<String>, help: impl Into<String>) -> Self {
193 let choice = Choice::new(value).help(help);
194 if let ValueHint::Choices(choices) = &mut self.value {
195 choices.push(choice);
196 return self;
197 }
198 self.value(ValueHint::Choices(vec![choice]))
199 }
200 pub fn help(mut self, help: impl Into<String>) -> Self {
201 self.help = help.into();
202 self
203 }
204 pub fn long_help(mut self, help: impl Into<String>) -> Self {
205 self.long_help = Some(help.into());
206 self
207 }
208 pub fn default_value(mut self, value: impl Into<String>) -> Self {
209 self.default = Some(value.into());
210 self
211 }
212 pub fn env(mut self, name: impl Into<String>) -> Self {
213 self.env = Some(name.into());
214 self
215 }
216 pub fn config_key(mut self, key: impl Into<String>) -> Self {
217 self.config_key = Some(key.into());
218 self
219 }
220 pub fn required(mut self, required: bool) -> Self {
221 self.required = required;
222 self
223 }
224 pub fn multiple(mut self, multiple: bool) -> Self {
225 self.multiple = multiple;
226 self
227 }
228 pub fn hidden(mut self, hidden: bool) -> Self {
229 self.hidden = hidden;
230 self
231 }
232 pub fn global(mut self, global: bool) -> Self {
234 self.global = global;
235 self
236 }
237 pub fn heading(mut self, heading: impl Into<String>) -> Self {
238 self.heading = Some(heading.into());
239 self
240 }
241
242 pub fn takes_value(&self) -> bool {
244 self.value_name.is_some()
245 }
246
247 pub fn choice_list(&self) -> &[Choice] {
249 match &self.value {
250 ValueHint::Choices(choices) => choices,
251 _ => &[],
252 }
253 }
254
255 pub fn group(&self) -> &str {
257 match (&self.heading, self.positional) {
258 (Some(heading), _) => heading,
259 (None, true) => "Arguments",
260 (None, false) => "Options",
261 }
262 }
263
264 pub fn metavar(&self) -> Option<String> {
267 let name = self.value_name.as_deref()?;
268 let mut out = if name.starts_with(['<', '[']) {
269 name.to_string()
270 } else if self.positional && !self.required {
271 format!("[{name}]")
272 } else {
273 format!("<{name}>")
274 };
275 if self.multiple {
276 out.push_str("...");
277 }
278 Some(out)
279 }
280
281 pub fn switches(&self) -> Vec<String> {
284 let mut out = Vec::new();
285 if let Some(short) = self.short {
286 out.push(format!("-{short}"));
287 }
288 out.extend(self.short_aliases.iter().map(|short| format!("-{short}")));
289 if let Some(long) = &self.long {
290 out.push(format!("--{long}"));
291 }
292 out.extend(self.aliases.iter().map(|alias| format!("--{alias}")));
293 out
294 }
295
296 pub fn names(&self) -> String {
299 if self.positional {
300 return self.metavar().unwrap_or_else(|| self.id.clone());
301 }
302 let mut out = self.switches().join(", ");
303 if let Some(metavar) = self.metavar() {
304 out.push(' ');
305 out.push_str(&metavar);
306 }
307 out
308 }
309
310 pub fn primary(&self) -> String {
312 match (&self.long, self.short) {
313 (Some(long), _) => format!("--{long}"),
314 (None, Some(short)) => format!("-{short}"),
315 _ => self.id.clone(),
316 }
317 }
318}
319
320#[derive(Clone, Debug, Default, PartialEq, Eq)]
322pub struct Example {
323 pub command: String,
324 pub description: String,
325}
326
327#[derive(Clone, Debug, Default, PartialEq, Eq)]
330pub struct Section {
331 pub title: String,
332 pub body: String,
333}
334
335#[derive(Clone, Debug, Default, PartialEq, Eq)]
337pub struct HeadingNote {
338 pub heading: String,
339 pub text: String,
340}
341
342#[derive(Clone, Debug, Default, PartialEq, Eq)]
356pub struct CommandSpec {
357 pub name: String,
358 pub bin_name: Option<String>,
360 pub version: Option<String>,
361 pub about: String,
362 pub long_about: Option<String>,
363 pub aliases: Vec<String>,
365 pub usage: Vec<String>,
367 pub args: Vec<ArgSpec>,
368 pub subcommands: Vec<CommandSpec>,
369 pub examples: Vec<Example>,
370 pub sections: Vec<Section>,
371 pub heading_notes: Vec<HeadingNote>,
372 pub subcommand_heading: Option<String>,
374 pub subcommand_required: bool,
377 pub hidden: bool,
379}
380
381impl CommandSpec {
382 pub fn new(name: impl Into<String>) -> Self {
383 CommandSpec {
384 name: name.into(),
385 ..Default::default()
386 }
387 }
388 pub fn bin_name(mut self, name: impl Into<String>) -> Self {
389 self.bin_name = Some(name.into());
390 self
391 }
392 pub fn version(mut self, version: impl Into<String>) -> Self {
393 self.version = Some(version.into());
394 self
395 }
396 pub fn about(mut self, about: impl Into<String>) -> Self {
397 self.about = about.into();
398 self
399 }
400 pub fn long_about(mut self, about: impl Into<String>) -> Self {
401 self.long_about = Some(about.into());
402 self
403 }
404 pub fn alias(mut self, alias: impl Into<String>) -> Self {
405 self.aliases.push(alias.into());
406 self
407 }
408 pub fn usage(mut self, line: impl Into<String>) -> Self {
410 self.usage.push(line.into());
411 self
412 }
413 pub fn arg(mut self, arg: ArgSpec) -> Self {
414 self.args.push(arg);
415 self
416 }
417 pub fn args(mut self, args: impl IntoIterator<Item = ArgSpec>) -> Self {
418 self.args.extend(args);
419 self
420 }
421 pub fn subcommand(mut self, command: CommandSpec) -> Self {
422 self.subcommands.push(command);
423 self
424 }
425 pub fn example(mut self, command: impl Into<String>, description: impl Into<String>) -> Self {
426 self.examples.push(Example {
427 command: command.into(),
428 description: description.into(),
429 });
430 self
431 }
432 pub fn section(mut self, title: impl Into<String>, body: impl Into<String>) -> Self {
433 self.sections.push(Section {
434 title: title.into(),
435 body: body.into(),
436 });
437 self
438 }
439 pub fn heading_note(mut self, heading: impl Into<String>, text: impl Into<String>) -> Self {
441 self.heading_notes.push(HeadingNote {
442 heading: heading.into(),
443 text: text.into(),
444 });
445 self
446 }
447 pub fn subcommand_heading(mut self, heading: impl Into<String>) -> Self {
448 self.subcommand_heading = Some(heading.into());
449 self
450 }
451 pub fn subcommand_required(mut self, required: bool) -> Self {
452 self.subcommand_required = required;
453 self
454 }
455 pub fn hidden(mut self, hidden: bool) -> Self {
456 self.hidden = hidden;
457 self
458 }
459
460 pub fn display_name(&self) -> &str {
462 self.bin_name.as_deref().unwrap_or(&self.name)
463 }
464
465 pub fn visible_args(&self) -> impl Iterator<Item = &ArgSpec> {
467 self.args.iter().filter(|arg| !arg.hidden)
468 }
469
470 pub fn visible_subcommands(&self) -> impl Iterator<Item = &CommandSpec> {
472 self.subcommands.iter().filter(|command| !command.hidden)
473 }
474
475 pub fn find_subcommand(&self, name: &str) -> Option<&CommandSpec> {
477 self.subcommands
478 .iter()
479 .find(|c| c.name == name || c.aliases.iter().any(|a| a == name))
480 }
481
482 pub fn groups(&self) -> Vec<(String, Vec<&ArgSpec>)> {
485 let mut groups: Vec<(String, Vec<&ArgSpec>)> = Vec::new();
486 let mut positionals = Vec::new();
487 for arg in self.visible_args() {
488 if arg.positional && arg.heading.is_none() {
489 positionals.push(arg);
490 continue;
491 }
492 match groups.iter_mut().find(|(title, _)| title == arg.group()) {
493 Some((_, members)) => members.push(arg),
494 None => groups.push((arg.group().to_string(), vec![arg])),
495 }
496 }
497 if !positionals.is_empty() {
498 groups.push(("Arguments".to_string(), positionals));
499 }
500 groups
501 }
502
503 pub fn note_for(&self, heading: &str) -> Option<&str> {
505 self.heading_notes
506 .iter()
507 .find(|note| note.heading == heading)
508 .map(|note| note.text.as_str())
509 }
510
511 pub fn usage_lines(&self) -> Vec<String> {
513 self.usage_lines_as(self.display_name())
514 }
515
516 pub fn usage_lines_as(&self, prefix: &str) -> Vec<String> {
521 if !self.usage.is_empty() {
522 let own = self.display_name();
523 return self
524 .usage
525 .iter()
526 .map(|line| match line.strip_prefix(own) {
527 Some(rest) if rest.is_empty() || rest.starts_with(' ') => {
528 format!("{prefix}{rest}")
529 }
530 _ => line.clone(),
531 })
532 .collect();
533 }
534 let mut parts = vec![prefix.to_string()];
535 if self
536 .visible_args()
537 .any(|arg| !arg.positional && !arg.required)
538 {
539 parts.push("[OPTIONS]".into());
540 }
541 for arg in self.visible_args().filter(|a| !a.positional && a.required) {
542 match arg.metavar() {
543 Some(metavar) => parts.push(format!("{} {metavar}", arg.primary())),
544 None => parts.push(arg.primary()),
545 }
546 }
547 for arg in self.visible_args().filter(|a| a.positional) {
548 parts.push(arg.names());
549 }
550 if self.visible_subcommands().next().is_some() {
551 parts.push(
552 if self.subcommand_required {
553 "<COMMAND>"
554 } else {
555 "[COMMAND]"
556 }
557 .into(),
558 );
559 }
560 vec![parts.join(" ")]
561 }
562
563 pub fn switch_names(&self) -> Vec<String> {
565 self.visible_args()
566 .filter(|arg| !arg.positional)
567 .flat_map(ArgSpec::switches)
568 .collect()
569 }
570
571 pub fn subcommand_names(&self) -> Vec<String> {
573 self.visible_subcommands()
574 .flat_map(|c| std::iter::once(c.name.clone()).chain(c.aliases.iter().cloned()))
575 .collect()
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn names_and_metavars() {
585 let arg = ArgSpec::option("width").short('w').value_name("SIZE");
586 assert_eq!(arg.names(), "-w, --width <SIZE>");
587 let files = ArgSpec::positional("file").multiple(true).required(true);
588 assert_eq!(files.names(), "<FILE>...");
589 assert_eq!(ArgSpec::positional("x").names(), "[X]");
590 assert_eq!(
591 ArgSpec::flag("no-color").alias("no-colour").names(),
592 "--no-color, --no-colour"
593 );
594 }
595
596 #[test]
597 fn groups_keep_first_seen_order() {
598 let spec = CommandSpec::new("x")
599 .arg(ArgSpec::flag("a").heading("B"))
600 .arg(ArgSpec::positional("p"))
601 .arg(ArgSpec::flag("b"))
602 .arg(ArgSpec::flag("c").heading("B"))
603 .arg(ArgSpec::flag("h").hidden(true));
604 let titles: Vec<_> = spec
605 .groups()
606 .into_iter()
607 .map(|(t, m)| (t, m.len()))
608 .collect();
609 assert_eq!(
610 titles,
611 vec![
612 ("B".into(), 2),
613 ("Options".into(), 1),
614 ("Arguments".into(), 1)
615 ]
616 );
617 }
618
619 #[test]
620 fn explicit_usage_takes_the_prefix() {
621 let spec = CommandSpec::new("show").usage("show [KEY]").usage("other");
622 assert_eq!(
623 spec.usage_lines_as("rich config show"),
624 vec!["rich config show [KEY]", "other"]
625 );
626 }
627
628 #[test]
629 fn generated_usage_lists_required_options() {
630 let spec = CommandSpec::new("x")
631 .arg(ArgSpec::option("out").short('o').required(true))
632 .arg(ArgSpec::positional("in").required(true));
633 assert_eq!(spec.usage_lines(), vec!["x --out <OUT> <IN>"]);
634 }
635}