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