Skip to main content

usage/docs/cli/
mod.rs

1use crate::{Spec, SpecCommand};
2use std::sync::LazyLock;
3use tera::Tera;
4
5pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String {
6    // Convert to docs models to get layout calculations
7    let docs_spec = crate::docs::models::Spec::from(spec.clone());
8    let docs_cmd = crate::docs::models::SpecCommand::from(cmd);
9
10    let mut ctx = tera::Context::new();
11    ctx.insert("spec", &docs_spec);
12    ctx.insert("cmd", &docs_cmd);
13    ctx.insert("long", &long);
14    let template = if long {
15        "spec_template_long.tera"
16    } else {
17        "spec_template_short.tera"
18    };
19    TERA.render(template, &ctx).unwrap().trim().to_string() + "\n"
20}
21
22static TERA: LazyLock<Tera> = LazyLock::new(|| {
23    let mut tera = Tera::default();
24
25    // Register ljust filter for left-justifying text with padding
26    tera.register_filter(
27        "ljust",
28        |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
29            let value = value.as_str().unwrap_or("");
30            let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
31            Ok(format!("{:<width$}", value, width = width))
32        },
33    );
34    tera.register_filter(
35        "default",
36        |value: &tera::Value,
37         kwargs: tera::Kwargs,
38         _: &tera::State|
39         -> tera::TeraResult<tera::Value> {
40            let default_val = kwargs.must_get::<tera::Value>("value")?;
41            let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
42            if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
43                Ok(default_val)
44            } else {
45                Ok(value.clone())
46            }
47        },
48    );
49
50    #[rustfmt::skip]
51    tera.add_raw_templates([
52        ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
53        ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
54    ]).unwrap();
55
56    tera
57});
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use insta::assert_snapshot;
63
64    #[test]
65    fn test_render_help_with_env() {
66        let spec = crate::spec! { r#"
67bin "testcli"
68flag "--color" env="MYCLI_COLOR" help="Enable color output"
69flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
70flag "--debug" help="Debug mode"
71        "# }
72        .unwrap();
73
74        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
75        Usage: testcli [FLAGS]
76
77        Flags:
78          --color  Enable color output [env: MYCLI_COLOR]
79          --verbose  Verbose output [env: MYCLI_VERBOSE]
80          --debug  Debug mode
81        ");
82
83        assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
84        Usage: testcli [FLAGS]
85
86        Flags:
87          --color    Enable color output
88            [env: MYCLI_COLOR]
89          --verbose  Verbose output
90            [env: MYCLI_VERBOSE]
91          --debug    Debug mode
92        ");
93    }
94
95    #[test]
96    fn test_render_help_with_arg_env() {
97        let spec = crate::spec! { r#"
98bin "testcli"
99arg "<input>" env="MY_INPUT" help="Input file"
100arg "<output>" env="MY_OUTPUT" help="Output file"
101arg "<extra>" help="Extra arg without env"
102arg "[default]" help="Arg with default value" default="default value"
103        "# }
104        .unwrap();
105
106        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
107        Usage: testcli <ARGS>…
108
109        Arguments:
110          <input>  Input file [env: MY_INPUT]
111          <output>  Output file [env: MY_OUTPUT]
112          <extra>  Extra arg without env
113          [default]  Arg with default value (default: default value)
114        ");
115
116        assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
117        Usage: testcli <ARGS>…
118
119        Arguments:
120          <input>    Input file
121            [env: MY_INPUT]
122          <output>   Output file
123            [env: MY_OUTPUT]
124          <extra>    Extra arg without env
125          [default]  Arg with default value
126            (default: default value)
127        ");
128    }
129
130    #[test]
131    fn test_render_help_with_negated_flag() {
132        let spec = crate::spec! { r#"
133bin "testcli"
134flag "--compress" negate="--no-compress" default=#true help="Compress output"
135flag "--verbose" help="Verbose output"
136        "# }
137        .unwrap();
138
139        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
140        Usage: testcli [--compress] [--verbose]
141
142        Flags:
143          --compress / --no-compress  Compress output
144          --verbose  Verbose output
145        ");
146
147        assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
148        Usage: testcli [--compress] [--verbose]
149
150        Flags:
151          --compress / --no-compress  Compress output
152          --verbose                   Verbose output
153        ");
154    }
155
156    #[test]
157    fn test_render_help_with_before_after_help() {
158        let spec = crate::spec! { r#"
159bin "testcli"
160before_help "This text appears before the help"
161after_help "This text appears after the help"
162flag "--verbose" help="Enable verbose output"
163        "# }
164        .unwrap();
165
166        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
167        This text appears before the help
168
169        Usage: testcli [--verbose]
170
171        Flags:
172          --verbose  Enable verbose output
173
174        This text appears after the help
175        ");
176    }
177
178    #[test]
179    fn test_render_help_with_before_after_help_long() {
180        let spec = crate::spec! { r#"
181bin "testcli"
182before_help "short before"
183before_help_long "This is the long version of before help"
184after_help "short after"
185after_help_long "This is the long version of after help"
186flag "--verbose" help="Enable verbose output"
187        "# }
188        .unwrap();
189
190        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
191        short before
192
193        Usage: testcli [--verbose]
194
195        Flags:
196          --verbose  Enable verbose output
197
198        short after
199        ");
200
201        assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
202        This is the long version of before help
203
204        Usage: testcli [--verbose]
205
206        Flags:
207          --verbose  Enable verbose output
208
209        This is the long version of after help
210        ");
211    }
212
213    #[test]
214    fn test_render_help_with_examples() {
215        let spec = crate::spec! { r#"
216bin "testcli"
217flag "--verbose" help="Enable verbose output"
218example "testcli --verbose" header="Run with verbose output"
219example "testcli" header="Run normally" help="Just runs the tool"
220        "# }
221        .unwrap();
222
223        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
224        Usage: testcli [--verbose]
225
226        Flags:
227          --verbose  Enable verbose output
228
229        Examples:
230          Run with verbose output:
231            $ testcli --verbose
232          Run normally:
233            $ testcli
234        ");
235
236        assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
237        Usage: testcli [--verbose]
238
239        Flags:
240          --verbose  Enable verbose output
241
242        Examples:
243          Run with verbose output:
244            $ testcli --verbose
245          Run normally:
246            Just runs the tool
247            $ testcli
248        ");
249    }
250
251    #[test]
252    fn test_render_help_with_version() {
253        let spec = crate::spec! { r#"
254bin "testcli"
255name "TestCLI"
256version "1.2.3"
257flag "--verbose" help="Enable verbose output"
258        "# }
259        .unwrap();
260
261        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
262        TestCLI 1.2.3
263        Usage: testcli [--verbose]
264
265        Flags:
266          --verbose  Enable verbose output
267        ");
268    }
269
270    #[test]
271    fn test_render_help_with_author_license() {
272        let spec = crate::spec! { r#"
273bin "testcli"
274author "Test Author"
275license "MIT"
276flag "--verbose" help="Enable verbose output"
277        "# }
278        .unwrap();
279
280        // Short help should not show author/license
281        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
282        Usage: testcli [--verbose]
283
284        Flags:
285          --verbose  Enable verbose output
286        ");
287
288        // Long help should show author/license at the bottom
289        assert_snapshot!(render_help(&spec, &spec.cmd, true), @r"
290        Usage: testcli [--verbose]
291
292        Flags:
293          --verbose  Enable verbose output
294
295        Author: Test Author
296        License: MIT
297        ");
298    }
299
300    #[test]
301    fn test_render_help_with_deprecated_command() {
302        let spec = crate::spec! { r#"
303bin "testcli"
304cmd "old-cmd" help="Do something" deprecated="use new-cmd instead"
305cmd "new-cmd" help="Do something better"
306        "# }
307        .unwrap();
308
309        assert_snapshot!(render_help(&spec, &spec.cmd, false), @r"
310        Usage: testcli <SUBCOMMAND>
311
312        Commands:
313          new-cmd  Do something better
314          old-cmd [deprecated: use new-cmd instead]  Do something
315          help  Print this message or the help of the given subcommand(s)
316        ");
317    }
318}