1use std::ffi::OsString;
2
3use clap::builder::styling::{AnsiColor, Styles};
4use clap::{ColorChoice, Command, CommandFactory, Error, FromArgMatches};
5
6const FAMILY_STYLES: Styles = Styles::styled()
7 .header(AnsiColor::Cyan.on_default().bold())
8 .usage(AnsiColor::Green.on_default().bold())
9 .literal(AnsiColor::Cyan.on_default().bold())
10 .placeholder(AnsiColor::Yellow.on_default())
11 .error(AnsiColor::Red.on_default().bold())
12 .valid(AnsiColor::Green.on_default())
13 .invalid(AnsiColor::Yellow.on_default().bold())
14 .context(AnsiColor::BrightBlack.on_default())
15 .context_value(AnsiColor::Yellow.on_default());
16
17const MAX_HELP_WIDTH: usize = 100;
18
19#[must_use]
21pub fn command<P>() -> Command
22where
23 P: CommandFactory,
24{
25 configure(P::command(), color_choice())
26}
27
28pub fn parse<P>() -> P
30where
31 P: CommandFactory + FromArgMatches,
32{
33 try_parse().unwrap_or_else(|error| error.exit())
34}
35
36pub fn try_parse<P>() -> Result<P, Error>
38where
39 P: CommandFactory + FromArgMatches,
40{
41 try_parse_from(std::env::args_os())
42}
43
44pub fn try_parse_from<P, I, T>(arguments: I) -> Result<P, Error>
46where
47 P: CommandFactory + FromArgMatches,
48 I: IntoIterator<Item = T>,
49 T: Into<OsString> + Clone,
50{
51 let mut command = command::<P>();
52 let mut matches = command.try_get_matches_from_mut(arguments)?;
53 P::from_arg_matches_mut(&mut matches).map_err(|error| error.format(&mut command))
54}
55
56fn color_choice() -> ColorChoice {
57 if std::env::var_os("NO_COLOR").is_some() {
58 ColorChoice::Never
59 } else {
60 ColorChoice::Auto
61 }
62}
63
64fn configure(command: Command, color: ColorChoice) -> Command {
65 command
66 .styles(FAMILY_STYLES)
67 .color(color)
68 .max_term_width(MAX_HELP_WIDTH)
69 .disable_help_subcommand(false)
70 .mut_subcommands(|subcommand| configure(subcommand, color))
71}
72
73#[cfg(test)]
74mod tests {
75 use clap::{Args, Parser, Subcommand, error::ErrorKind};
76
77 use super::*;
78
79 #[derive(Debug, Parser)]
80 #[command(
81 name = "rsomics-example",
82 version = "1.2.3",
83 about = "Example product",
84 subcommand_required = true
85 )]
86 struct ExampleCli {
87 #[command(subcommand)]
88 command: ExampleCommand,
89 }
90
91 #[derive(Debug, Subcommand)]
92 enum ExampleCommand {
93 Inspect(InspectArgs),
95 }
96
97 #[derive(Debug, Args)]
98 struct InspectArgs {
99 #[arg(value_name = "FASTA")]
101 input: String,
102
103 #[arg(short, long)]
105 all: bool,
106 }
107
108 #[derive(Debug, Parser)]
109 #[command(name = "value-example")]
110 struct ValueCli {
111 value: String,
112 }
113
114 #[test]
115 fn decorated_command_tree_is_valid() {
116 command::<ExampleCli>().debug_assert();
117 }
118
119 #[test]
120 fn nested_help_is_derived_from_the_real_command_tree() {
121 let error = try_parse_from::<ExampleCli, _, _>(["rsomics-example", "inspect", "--help"])
122 .unwrap_err();
123 assert_eq!(error.kind(), ErrorKind::DisplayHelp);
124 let help = error.to_string();
125 assert!(help.contains("Inspect an input"), "{help}");
126 assert!(help.contains("<FASTA>"), "{help}");
127 assert!(help.contains("--all"), "{help}");
128 }
129
130 #[test]
131 fn clap_help_subcommand_navigates_the_same_tree() {
132 let error =
133 try_parse_from::<ExampleCli, _, _>(["rsomics-example", "help", "inspect"]).unwrap_err();
134 assert_eq!(error.kind(), ErrorKind::DisplayHelp);
135 assert!(error.to_string().contains("--all"));
136 }
137
138 #[test]
139 fn parse_errors_keep_clap_context_and_suggestions() {
140 let error =
141 try_parse_from::<ExampleCli, _, _>(["rsomics-example", "inspect", "--al", "input.fa"])
142 .unwrap_err();
143 assert_eq!(error.kind(), ErrorKind::UnknownArgument);
144 let message = error.to_string();
145 assert!(message.contains("--all"), "{message}");
146 assert!(message.contains("Usage:"), "{message}");
147 }
148
149 #[test]
150 fn ordinary_help_value_is_not_intercepted() {
151 let parsed = try_parse_from::<ValueCli, _, _>(["value-example", "help"]).unwrap();
152 assert_eq!(parsed.value, "help");
153 }
154
155 #[test]
156 fn explicit_iterator_builds_the_derived_type() {
157 let parsed =
158 try_parse_from::<ExampleCli, _, _>(["rsomics-example", "inspect", "--all", "reads.fa"])
159 .unwrap();
160 let ExampleCommand::Inspect(args) = parsed.command;
161 assert_eq!(args.input, "reads.fa");
162 assert!(args.all);
163 }
164}