Skip to main content

ctl_core/
parser.rs

1//! Parser defaults. `-h` / `--help` and `-V` / `--version` stay on.
2
3#[cfg(any(feature = "view", feature = "help"))]
4use std::ffi::OsString;
5
6use clap::{ColorChoice, Command};
7
8#[cfg(any(feature = "view", feature = "help"))]
9use crate::{ColorMode, OutputFormat};
10
11/// Output policy recovered with the authoritative Clap graph before a full
12/// parse succeeds.
13#[cfg(any(feature = "view", feature = "help"))]
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub(crate) struct ParsedOutput {
16    pub(crate) format: OutputFormat,
17    pub(crate) color: ColorMode,
18}
19
20/// Best-effort output policy for help and parse errors.
21///
22/// Clap still owns option placement, attached short values, domain option
23/// values, global propagation, and `--` semantics.
24#[cfg(any(feature = "view", feature = "help"))]
25pub(crate) fn parsed_output<C: clap::CommandFactory>(raw: &[OsString]) -> ParsedOutput {
26    let command = C::command()
27        .arg_required_else_help(false)
28        .args_override_self(true)
29        .disable_help_flag(true)
30        .disable_version_flag(true)
31        .ignore_errors(true)
32        .color(ColorChoice::Never);
33    let Ok(matches) = command.try_get_matches_from(raw) else {
34        return ParsedOutput {
35            format: OutputFormat::Pretty,
36            color: ColorMode::Auto,
37        };
38    };
39    let format = matches
40        .try_get_one::<OutputFormat>("format")
41        .ok()
42        .flatten()
43        .copied()
44        .unwrap_or_default();
45    let color = matches
46        .try_get_one::<ColorMode>("color")
47        .ok()
48        .flatten()
49        .copied()
50        .unwrap_or_default();
51    let no_color = matches
52        .try_get_one::<bool>("no_color")
53        .ok()
54        .flatten()
55        .copied()
56        .unwrap_or(false);
57    ParsedOutput {
58        format,
59        color: crate::flags::resolve_color(color, no_color),
60    }
61}
62
63/// Whether Clap interprets this invocation as an explicit help request.
64#[cfg(feature = "help")]
65pub(crate) fn wants_help<C: clap::CommandFactory>(raw: &[OsString]) -> bool {
66    let command = apply_defaults(C::command()).color(ColorChoice::Never);
67    matches!(
68        command.try_get_matches_from(raw),
69        Err(error) if error.kind() == clap::error::ErrorKind::DisplayHelp
70    )
71}
72
73/// Whether the command's Clap grammar rejects a bare invocation with help.
74#[cfg(feature = "help")]
75pub(crate) fn requires_input<C: clap::CommandFactory>() -> bool {
76    let command = C::command();
77    command.is_arg_required_else_help_set() || command.is_subcommand_required_set()
78}
79
80/// Apply the *ctl parser contract to a clap command.
81///
82/// `-h/--help` and `-V/--version` stay on. `disable_help_flag` is forbidden.
83/// Repeated flags last-win (`args_override_self`);
84/// [`crate::flags::chassis_warnings`] still reports the clash.
85#[must_use]
86pub fn apply_defaults(command: Command) -> Command {
87    assert_help_enabled(&command);
88    command
89        .args_override_self(true)
90        .color(ColorChoice::Auto)
91        .disable_help_flag(false)
92        .disable_version_flag(false)
93}
94
95/// clap's recommended `Command::debug_assert` plus the help-flag contract.
96///
97/// # Panics
98///
99/// Panics if help or version flags were disabled.
100pub fn verify<C: clap::CommandFactory>() {
101    C::command().debug_assert();
102    assert_help_enabled(&C::command());
103}
104
105/// # Panics
106///
107/// Panics if `disable_help_flag` or `disable_version_flag` is set.
108pub fn assert_help_enabled(command: &Command) {
109    assert!(
110        !command.is_disable_help_flag_set(),
111        "disable_help_flag is forbidden; ctl-core owns -h/--help"
112    );
113    assert!(
114        !command.is_disable_version_flag_set(),
115        "disable_version_flag is forbidden; ctl-core owns -V/--version"
116    );
117}
118
119#[cfg(test)]
120mod tests {
121    use clap::{Command, CommandFactory, Parser};
122
123    use super::{apply_defaults, assert_help_enabled};
124
125    #[derive(Parser)]
126    #[command(version, about = "toy", arg_required_else_help = true)]
127    struct Toy {
128        #[command(subcommand)]
129        command: ToyCmd,
130    }
131
132    #[derive(clap::Subcommand)]
133    enum ToyCmd {
134        Status,
135    }
136
137    #[test]
138    fn verify_toy() {
139        super::verify::<Toy>();
140    }
141
142    #[test]
143    fn derive_keeps_help_and_version() {
144        let mut command = apply_defaults(Toy::command());
145        assert_help_enabled(&command);
146        assert!(command.is_args_override_self());
147        let help = command.render_long_help().to_string();
148        assert!(help.contains("-h, --help"));
149        assert!(help.contains("-V, --version"));
150    }
151
152    #[test]
153    fn preserves_bare_invocation_policy() {
154        let command = apply_defaults(Toy::command().arg_required_else_help(false));
155        assert!(!command.is_arg_required_else_help_set());
156    }
157
158    #[test]
159    #[should_panic(expected = "disable_help_flag is forbidden")]
160    fn rejects_disabled_help() {
161        let command = Command::new("x").disable_help_flag(true);
162        assert_help_enabled(&command);
163    }
164}