clap_doc/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use core::fmt::{self, Write as _};
4
5use clap_builder::{Command, CommandFactory};
6use stackstack::Stack;
7
8/// Create a markdown description for the given [`Command`].
9///
10/// See [module documentation](mod@self) for more.
11pub fn markdown(cmd: Command) -> String {
12    let mut buf = String::new();
13    _markdown(&mut buf, Stack::new(), &cmd).expect("fmt::write returned an error");
14    buf
15}
16
17/// Create a markdown description for the given [`CommandFactory`].
18///
19/// This is shorthand for [`markdown`].
20pub fn markdown_for<T: CommandFactory>() -> String {
21    markdown(T::command())
22}
23
24fn _markdown(buf: &mut String, path: Stack<&str>, cmd: &Command) -> fmt::Result {
25    if !path.is_empty() {
26        buf.push('\n')
27    }
28    let path = path.pushed(cmd.get_name());
29    for _ in 0..path.len() {
30        buf.push('#')
31    }
32    for component in &path {
33        buf.write_fmt(format_args!(" `{component}`"))?;
34    }
35
36    let mut cmd = cmd
37        .clone()
38        .disable_help_subcommand(true)
39        .disable_help_flag(true);
40    cmd.set_bin_name(join(&path)?); // hack the bin name get a nice `Usage`.
41    fmt::write(
42        buf,
43        format_args!(
44            "\n```text\n{}\n```",
45            cmd.render_long_help().to_string().trim()
46        ),
47    )?;
48    for sub in cmd.get_subcommands() {
49        _markdown(buf, path, sub)?
50    }
51    Ok(())
52}
53
54fn join<T: fmt::Display>(it: impl IntoIterator<Item = T>) -> Result<String, fmt::Error> {
55    let mut it = it.into_iter().peekable();
56    let mut s = String::new();
57    while let Some(comp) = it.next() {
58        s.write_fmt(format_args!("{comp}"))?;
59        if it.peek().is_some() {
60            s.push(' ');
61        }
62    }
63    Ok(s)
64}
65
66#[cfg(test)]
67mod tests {
68    include!("test-snippet.rs");
69
70    use clap::Parser;
71    use expect_test::{expect, expect_file};
72    use indoc::formatdoc;
73
74    use super::*;
75
76    #[test]
77    fn readme() {
78        let crate_name = env!("CARGO_PKG_NAME");
79        let test_snippet = include_str!("test-snippet.rs").trim();
80        let markdown = markdown(Cargo::command());
81        let readme = formatdoc! {
82            "
83            # `{crate_name}`
84            Create markdown descriptions for [`clap::Command`]s.
85
86            So given the following rust code:
87            ```rust
88            {test_snippet}
89            ```
90
91            You get the markdown that follows,
92            with subcommands handled as you'd expect.
93            ---
94            {markdown}
95            "
96        };
97        expect_file!["../README.md"].assert_eq(&readme);
98    }
99
100    /// This is a top-level description.
101    #[derive(Parser)]
102    #[command(name = "simple")]
103    struct Simple {
104        /// This is a flag.
105        #[arg(short, long)]
106        flag: bool,
107
108        /// This is a mandatory positional argument.
109        pos: String,
110        /// This is a mandatory switched argument.
111        #[arg(short, long)]
112        switch: String,
113
114        /// This is an optional positional argument.
115        opt_pos: Option<String>,
116        /// This is an optional switched argument.
117        #[arg(short, long)]
118        opt_switch: Option<String>,
119
120        /// This is a switched argument with a default
121        #[arg(short, long, default_value = "default")]
122        default_switch: String,
123    }
124
125    #[test]
126    fn simple() {
127        expect![[r#"
128            # `simple`
129            ```text
130            This is a top-level description
131
132            Usage: simple [OPTIONS] --switch <SWITCH> <POS> [OPT_POS]
133
134            Arguments:
135              <POS>
136                      This is a mandatory positional argument
137
138              [OPT_POS]
139                      This is an optional positional argument
140
141            Options:
142              -f, --flag
143                      This is a flag
144
145              -s, --switch <SWITCH>
146                      This is a mandatory switched argument
147
148              -o, --opt-switch <OPT_SWITCH>
149                      This is an optional switched argument
150
151              -d, --default-switch <DEFAULT_SWITCH>
152                      This is a switched argument with a default
153                      
154                      [default: default]
155            ```"#]]
156        .assert_eq(&markdown(Simple::command()));
157    }
158}