1#[cfg(feature = "help")]
28use std::ffi::OsString;
29
30#[cfg(feature = "help")]
31use clap::ArgMatches;
32use clap::{Arg, ArgAction, Command};
33
34#[cfg(feature = "help")]
35use crate::{ColorMode, OutputFormat};
36
37#[cfg(feature = "help")]
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub(crate) struct ParsedOutput {
42 pub(crate) format: OutputFormat,
43 pub(crate) color: ColorMode,
44}
45
46#[cfg(feature = "help")]
51pub(crate) fn parsed_output<C: clap::CommandFactory>(raw: &[OsString]) -> ParsedOutput {
52 let command = C::command()
53 .arg_required_else_help(false)
54 .args_override_self(true)
55 .disable_help_flag(true)
56 .disable_version_flag(true)
57 .ignore_errors(true);
58 let Ok(matches) = command.try_get_matches_from(raw) else {
59 return ParsedOutput {
60 format: OutputFormat::Pretty,
61 color: ColorMode::Auto,
62 };
63 };
64 let format = matches
65 .try_get_one::<OutputFormat>("format")
66 .ok()
67 .flatten()
68 .copied()
69 .unwrap_or_default();
70 let color = matches
71 .try_get_one::<ColorMode>("color")
72 .ok()
73 .flatten()
74 .copied()
75 .unwrap_or_default();
76 let no_color = matches
77 .try_get_one::<bool>("no_color")
78 .ok()
79 .flatten()
80 .copied()
81 .unwrap_or(false);
82 ParsedOutput {
83 format,
84 color: crate::flags::resolve_color(color, no_color),
85 }
86}
87
88#[cfg(feature = "help")]
96pub(crate) fn wants_help<C: clap::CommandFactory>(raw: &[OsString]) -> bool {
97 let declared = C::command();
98 let probe = lift_requirements(apply_defaults(declared.clone()));
99 match probe.try_get_matches_from(raw) {
100 Ok(matches) => names_help(&declared, &matches),
101 Err(error) => error.kind() == clap::error::ErrorKind::DisplayHelp,
102 }
103}
104
105#[cfg(feature = "help")]
108fn lift_requirements(command: Command) -> Command {
109 command
110 .subcommand_negates_reqs(true)
111 .mut_subcommands(lift_requirements)
112}
113
114#[cfg(feature = "help")]
117fn names_help(declared: &Command, matches: &ArgMatches) -> bool {
118 let Some((name, selected)) = matches.subcommand() else {
119 return false;
120 };
121 match declared.find_subcommand(name) {
122 Some(next) => names_help(next, selected),
123 None => name == "help",
124 }
125}
126
127#[cfg(feature = "app")]
129pub(crate) fn requires_input<C: clap::CommandFactory>() -> bool {
130 let command = C::command();
131 command.is_arg_required_else_help_set() || command.is_subcommand_required_set()
132}
133
134#[cfg_attr(feature = "app", doc = "", doc = "[`App`]: crate::App")]
152#[cfg_attr(not(feature = "app"), doc = "", doc = "[`App`]: crate#cargo-features")]
153#[cfg_attr(
154 feature = "help",
155 doc = "",
156 doc = "[`help::try_emit`]: crate::help::try_emit"
157)]
158#[cfg_attr(
159 not(feature = "help"),
160 doc = "",
161 doc = "[`help::try_emit`]: crate#cargo-features"
162)]
163#[must_use]
164pub fn apply_defaults(command: Command) -> Command {
165 assert_help_enabled(&command);
166 with_help(command.args_override_self(true).disable_version_flag(false))
167}
168
169const HELP_ABOUT: &str = "Print this message or the help of the given subcommand(s)";
171
172#[cfg(feature = "surface-model")]
175pub(crate) fn is_generated_help(command: &Command) -> bool {
176 command.get_name() == "help"
177 && command
178 .get_about()
179 .is_some_and(|about| about.to_string() == HELP_ABOUT)
180}
181
182pub(crate) fn with_help(command: Command) -> Command {
188 with_help_below(command, false)
189}
190
191fn with_help_below(mut command: Command, inherited_long: bool) -> Command {
195 let long = inherited_long || has_long_help(&command);
196 let passed_down = inherited_long
197 || command
198 .get_arguments()
199 .any(|arg| arg.is_global_set() && shows_long_help(arg));
200 let help_subcommand = (command.has_subcommands()
201 && !command.is_disable_help_subcommand_set()
202 && command.find_subcommand("help").is_none())
203 .then(|| help_subcommand(&command));
204 if !command.is_disable_help_flag_set()
205 && command.get_arguments().all(|arg| arg.get_id() != "help")
206 {
207 command = command.disable_help_flag(true).arg(help_flag(long));
208 }
209 command = command.mut_subcommands(|subcommand| with_help_below(subcommand, passed_down));
210 match help_subcommand {
211 Some(help) => command.disable_help_subcommand(true).subcommand(help),
212 None => command,
213 }
214}
215
216fn help_flag(long: bool) -> Arg {
218 let flag = Arg::new("help")
219 .short('h')
220 .long("help")
221 .action(ArgAction::Help);
222 if long {
223 flag.help("Print help (see more with '--help')")
224 .long_help("Print help (see a summary with '-h')")
225 } else {
226 flag.help("Print help")
227 }
228}
229
230fn help_subcommand(parent: &Command) -> Command {
235 let help_help = Command::new("help")
236 .about(HELP_ABOUT)
237 .disable_help_flag(true)
238 .disable_version_flag(true);
239 Command::new("help")
240 .about(HELP_ABOUT)
241 .disable_help_flag(true)
242 .disable_version_flag(true)
243 .disable_help_subcommand(true)
244 .subcommands(parent.get_subcommands().map(copy_for_help))
245 .subcommand(help_help)
246}
247
248fn copy_for_help(command: &Command) -> Command {
250 let copy = Command::new(command.get_name().to_owned())
251 .hide(command.is_hide_set())
252 .disable_help_flag(true)
253 .disable_version_flag(true)
254 .disable_help_subcommand(true)
255 .subcommands(command.get_subcommands().map(copy_for_help));
256 match command.get_about() {
257 Some(about) => copy.about(about.clone()),
258 None => copy,
259 }
260}
261
262fn has_long_help(command: &Command) -> bool {
267 command.get_long_about().is_some()
268 || command.get_before_long_help().is_some()
269 || command.get_after_long_help().is_some()
270 || command.get_arguments().any(shows_long_help)
271}
272
273fn shows_long_help(arg: &Arg) -> bool {
275 !arg.is_hide_set()
276 && (arg.get_long_help().is_some()
277 || arg.is_hide_long_help_set()
278 || arg.is_hide_short_help_set()
279 || (!arg.is_hide_possible_values_set()
280 && arg
281 .get_possible_values()
282 .iter()
283 .any(|value| !value.is_hide_set() && value.get_help().is_some())))
284}
285
286pub fn verify<C: clap::CommandFactory>() {
297 C::command().debug_assert();
298 assert_help_enabled(&C::command());
299}
300
301pub(crate) fn assert_help_enabled(command: &Command) {
307 assert!(
308 !command.is_disable_help_flag_set(),
309 "disable_help_flag is forbidden; ctl-core owns -h/--help"
310 );
311 assert!(
312 !command.is_disable_version_flag_set(),
313 "disable_version_flag is forbidden; ctl-core owns -V/--version"
314 );
315}
316
317#[cfg(test)]
318mod tests {
319 use clap::{Command, CommandFactory, Parser};
320
321 use super::{apply_defaults, assert_help_enabled};
322
323 #[derive(Parser)]
324 #[command(version, about = "toy", arg_required_else_help = true)]
325 struct Toy {
326 #[command(subcommand)]
327 command: ToyCmd,
328 }
329
330 #[derive(clap::Subcommand)]
331 enum ToyCmd {
332 Status,
333 }
334
335 #[test]
336 fn verify_toy() {
337 super::verify::<Toy>();
338 }
339
340 #[test]
341 fn derive_keeps_help_and_version() {
342 let mut command = apply_defaults(Toy::command());
343 assert!(command.is_args_override_self());
344 command.build();
345 let flags = command
346 .get_arguments()
347 .filter_map(|arg| arg.get_short().zip(arg.get_long()))
348 .collect::<Vec<_>>();
349 assert!(flags.contains(&('h', "help")), "{flags:?}");
350 assert!(flags.contains(&('V', "version")), "{flags:?}");
351 let status = command.find_subcommand("status").map(|status| {
352 status
353 .get_arguments()
354 .any(|arg| arg.get_short() == Some('h'))
355 });
356 assert_eq!(status, Some(true));
357 assert!(command.find_subcommand("help").is_some());
358 }
359
360 #[test]
361 fn defaults_apply_once() {
362 let once = apply_defaults(Toy::command());
363 let twice = super::with_help(once.clone());
364 assert_eq!(once.get_arguments().count(), twice.get_arguments().count());
365 assert_eq!(
366 once.get_subcommands().count(),
367 twice.get_subcommands().count()
368 );
369 }
370
371 #[test]
372 fn preserves_bare_invocation_policy() {
373 let command = apply_defaults(Toy::command().arg_required_else_help(false));
374 assert!(!command.is_arg_required_else_help_set());
375 }
376
377 #[test]
378 #[should_panic(expected = "disable_help_flag is forbidden")]
379 fn rejects_disabled_help() {
380 let command = Command::new("x").disable_help_flag(true);
381 assert_help_enabled(&command);
382 }
383}