clap_markdown/
lib.rs

1//! Autogenerate Markdown documentation for clap command-line tools
2//!
3//! See [**Examples**][Examples] for examples of the content `clap-markdown`
4//! generates.
5//!
6//! [Examples]: https://github.com/ConnorGray/clap-markdown#Examples
7//!
8
9// Ensure that doc tests in the README.md file get run.
10#[doc(hidden)]
11mod test_readme {
12    #![doc = include_str!("../README.md")]
13}
14
15mod utils;
16
17use std::fmt::{self, Write};
18
19use clap::builder::PossibleValue;
20
21use utils::pluralize;
22
23//======================================
24// Public API types
25//======================================
26
27/// Options to customize the structure of the output Markdown document.
28///
29/// Used with [`help_markdown_custom()`].
30#[non_exhaustive]
31pub struct MarkdownOptions {
32    title: Option<String>,
33    show_footer: bool,
34    show_table_of_contents: bool,
35    show_aliases: bool,
36}
37
38impl MarkdownOptions {
39    /// Construct a default instance of `MarkdownOptions`.
40    pub fn new() -> Self {
41        return Self {
42            title: None,
43            show_footer: true,
44            show_table_of_contents: true,
45            show_aliases: true,
46        };
47    }
48
49    /// Set a custom title to use in the generated document.
50    pub fn title(mut self, title: String) -> Self {
51        self.title = Some(title);
52
53        return self;
54    }
55
56    /// Whether to show the default footer advertising `clap-markdown`.
57    pub fn show_footer(mut self, show: bool) -> Self {
58        self.show_footer = show;
59
60        return self;
61    }
62
63    /// Whether to show the default table of contents.
64    pub fn show_table_of_contents(mut self, show: bool) -> Self {
65        self.show_table_of_contents = show;
66
67        return self;
68    }
69
70    /// Whether to show aliases for arguments and commands.
71    pub fn show_aliases(mut self, show: bool) -> Self {
72        self.show_aliases = show;
73
74        return self;
75    }
76}
77
78impl Default for MarkdownOptions {
79    fn default() -> Self {
80        return Self::new();
81    }
82}
83
84//======================================
85// Public API functions
86//======================================
87
88/// Format the help information for `command` as Markdown.
89pub fn help_markdown<C: clap::CommandFactory>() -> String {
90    let command = C::command();
91
92    help_markdown_command(&command)
93}
94
95/// Format the help information for `command` as Markdown, with custom options.
96pub fn help_markdown_custom<C: clap::CommandFactory>(
97    options: &MarkdownOptions,
98) -> String {
99    let command = C::command();
100
101    return help_markdown_command_custom(&command, options);
102}
103
104/// Format the help information for `command` as Markdown.
105pub fn help_markdown_command(command: &clap::Command) -> String {
106    return help_markdown_command_custom(command, &Default::default());
107}
108
109/// Format the help information for `command` as Markdown, with custom options.
110pub fn help_markdown_command_custom(
111    command: &clap::Command,
112    options: &MarkdownOptions,
113) -> String {
114    let mut buffer = String::with_capacity(100);
115
116    write_help_markdown(&mut buffer, &command, options);
117
118    buffer
119}
120
121//======================================
122// Markdown
123//======================================
124
125/// Format the help information for `command` as Markdown and print it.
126///
127/// Output is printed to the standard output, using [`println!`].
128pub fn print_help_markdown<C: clap::CommandFactory>() {
129    let command = C::command();
130
131    let mut buffer = String::with_capacity(100);
132
133    write_help_markdown(&mut buffer, &command, &Default::default());
134
135    println!("{}", buffer);
136}
137
138fn write_help_markdown(
139    buffer: &mut String,
140    command: &clap::Command,
141    options: &MarkdownOptions,
142) {
143    //----------------------------------
144    // Write the document title
145    //----------------------------------
146
147    let title_name = get_canonical_name(command);
148
149    let title = match options.title {
150        Some(ref title) => title.to_owned(),
151        None => format!("Command-Line Help for `{title_name}`"),
152    };
153    writeln!(buffer, "# {title}\n",).unwrap();
154
155    writeln!(
156        buffer,
157        "This document contains the help content for the `{}` command-line program.\n",
158        title_name
159    ).unwrap();
160
161    //----------------------------------
162    // Write the table of contents
163    //----------------------------------
164
165    // writeln!(buffer, r#"<div style="background: light-gray"><ul>"#).unwrap();
166    // build_table_of_contents_html(buffer, Vec::new(), command, 0).unwrap();
167    // writeln!(buffer, "</ul></div>").unwrap();
168
169    if options.show_table_of_contents {
170        writeln!(buffer, "**Command Overview:**\n").unwrap();
171
172        build_table_of_contents_markdown(buffer, Vec::new(), command, 0)
173            .unwrap();
174
175        write!(buffer, "\n").unwrap();
176    }
177
178    //----------------------------------------
179    // Write the commands/subcommands sections
180    //----------------------------------------
181
182    build_command_markdown(buffer, Vec::new(), command, 0, options).unwrap();
183
184    //-----------------
185    // Write the footer
186    //-----------------
187    if options.show_footer {
188        write!(buffer, r#"<hr/>
189
190<small><i>
191    This document was generated automatically by
192    <a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
193</i></small>
194"#).unwrap();
195    }
196}
197
198fn build_table_of_contents_markdown(
199    buffer: &mut String,
200    // Parent commands of `command`.
201    parent_command_path: Vec<String>,
202    command: &clap::Command,
203    depth: usize,
204) -> std::fmt::Result {
205    // Don't document commands marked with `clap(hide = true)` (which includes
206    // `print-all-help`).
207    if command.is_hide_set() {
208        return Ok(());
209    }
210
211    let title_name = get_canonical_name(command);
212
213    // Append the name of `command` to `command_path`.
214    let command_path = {
215        let mut command_path = parent_command_path;
216        command_path.push(title_name);
217        command_path
218    };
219
220    writeln!(
221        buffer,
222        "* [`{}`↴](#{})",
223        command_path.join(" "),
224        command_path.join("-"),
225    )?;
226
227    //----------------------------------
228    // Recurse to write subcommands
229    //----------------------------------
230
231    for subcommand in command.get_subcommands() {
232        build_table_of_contents_markdown(
233            buffer,
234            command_path.clone(),
235            subcommand,
236            depth + 1,
237        )?;
238    }
239
240    Ok(())
241}
242
243/*
244fn build_table_of_contents_html(
245    buffer: &mut String,
246    // Parent commands of `command`.
247    parent_command_path: Vec<String>,
248    command: &clap::Command,
249    depth: usize,
250) -> std::fmt::Result {
251    // Don't document commands marked with `clap(hide = true)` (which includes
252    // `print-all-help`).
253    if command.is_hide_set() {
254        return Ok(());
255    }
256
257    // Append the name of `command` to `command_path`.
258    let command_path = {
259        let mut command_path = parent_command_path;
260        command_path.push(command.get_name().to_owned());
261        command_path
262    };
263
264    writeln!(
265        buffer,
266        "<li><a href=\"#{}\"><code>{}</code>↴</a></li>",
267        command_path.join("-"),
268        command_path.join(" ")
269    )?;
270
271    //----------------------------------
272    // Recurse to write subcommands
273    //----------------------------------
274
275    for subcommand in command.get_subcommands() {
276        build_table_of_contents_html(
277            buffer,
278            command_path.clone(),
279            subcommand,
280            depth + 1,
281        )?;
282    }
283
284    Ok(())
285}
286*/
287
288fn build_command_markdown(
289    buffer: &mut String,
290    // Parent commands of `command`.
291    parent_command_path: Vec<String>,
292    command: &clap::Command,
293    depth: usize,
294    options: &MarkdownOptions,
295) -> std::fmt::Result {
296    // Don't document commands marked with `clap(hide = true)` (which includes
297    // `print-all-help`).
298    if command.is_hide_set() {
299        return Ok(());
300    }
301
302    let title_name = get_canonical_name(command);
303
304    // Append the name of `command` to `command_path`.
305    let command_path = {
306        let mut command_path = parent_command_path.clone();
307        command_path.push(title_name);
308        command_path
309    };
310
311    //----------------------------------
312    // Write the markdown heading
313    //----------------------------------
314
315    // TODO: `depth` is now unused. Remove if no other use for it appears.
316    /*
317    if depth >= 6 {
318        panic!(
319            "command path nesting depth is deeper than maximum markdown header depth: `{}`",
320            command_path.join(" ")
321        )
322    }
323    */
324    writeln!(buffer, "## `{}`\n", command_path.join(" "))?;
325
326    if let Some(long_about) = command.get_long_about() {
327        writeln!(buffer, "{}\n", long_about)?;
328    } else if let Some(about) = command.get_about() {
329        writeln!(buffer, "{}\n", about)?;
330    }
331
332    if let Some(help) = command.get_before_long_help() {
333        writeln!(buffer, "{}\n", help)?;
334    } else if let Some(help) = command.get_before_help() {
335        writeln!(buffer, "{}\n", help)?;
336    }
337
338    writeln!(
339        buffer,
340        "**Usage:** `{}{}`\n",
341        if parent_command_path.is_empty() {
342            String::new()
343        } else {
344            let mut s = parent_command_path.join(" ");
345            s.push_str(" ");
346            s
347        },
348        command
349            .clone()
350            .render_usage()
351            .to_string()
352            .replace("Usage: ", "")
353    )?;
354
355    if options.show_aliases {
356        let aliases = command.get_visible_aliases().collect::<Vec<&str>>();
357        if let Some(aliases_str) = get_alias_string(&aliases) {
358            writeln!(
359                buffer,
360                "**{}:** {aliases_str}\n",
361                pluralize(aliases.len(), "Command Alias", "Command Aliases")
362            )?;
363        }
364    }
365
366    if let Some(help) = command.get_after_long_help() {
367        writeln!(buffer, "{}\n", help)?;
368    } else if let Some(help) = command.get_after_help() {
369        writeln!(buffer, "{}\n", help)?;
370    }
371
372    //----------------------------------
373    // Subcommands
374    //----------------------------------
375
376    if command.get_subcommands().next().is_some() {
377        writeln!(buffer, "###### **Subcommands:**\n")?;
378
379        for subcommand in command.get_subcommands() {
380            if subcommand.is_hide_set() {
381                continue;
382            }
383
384            let title_name = get_canonical_name(subcommand);
385
386            let about = match subcommand.get_about() {
387                Some(about) => about.to_string(),
388                None => String::new(),
389            };
390
391            writeln!(buffer, "* `{title_name}` — {about}",)?;
392        }
393
394        write!(buffer, "\n")?;
395    }
396
397    //----------------------------------
398    // Arguments
399    //----------------------------------
400
401    if command.get_positionals().next().is_some() {
402        writeln!(buffer, "###### **Arguments:**\n")?;
403
404        for pos_arg in command.get_positionals() {
405            write_arg_markdown(buffer, pos_arg)?;
406        }
407
408        write!(buffer, "\n")?;
409    }
410
411    //----------------------------------
412    // Options
413    //----------------------------------
414
415    let non_pos: Vec<_> = command
416        .get_arguments()
417        .filter(|arg| !arg.is_positional() && !arg.is_hide_set())
418        .collect();
419
420    if !non_pos.is_empty() {
421        writeln!(buffer, "###### **Options:**\n")?;
422
423        for arg in non_pos {
424            write_arg_markdown(buffer, arg)?;
425        }
426
427        write!(buffer, "\n")?;
428    }
429
430    //----------------------------------
431    // Recurse to write subcommands
432    //----------------------------------
433
434    // Include extra space between commands. This is purely for the benefit of
435    // anyone reading the source .md file.
436    write!(buffer, "\n\n")?;
437
438    for subcommand in command.get_subcommands() {
439        build_command_markdown(
440            buffer,
441            command_path.clone(),
442            subcommand,
443            depth + 1,
444            options,
445        )?;
446    }
447
448    Ok(())
449}
450
451fn write_arg_markdown(buffer: &mut String, arg: &clap::Arg) -> fmt::Result {
452    // Markdown list item
453    write!(buffer, "* ")?;
454
455    let value_name: String = match arg.get_value_names() {
456        // TODO: What if multiple names are provided?
457        Some([name, ..]) => name.as_str().to_owned(),
458        Some([]) => unreachable!(
459            "clap Arg::get_value_names() returned Some(..) of empty list"
460        ),
461        None => arg.get_id().to_string().to_ascii_uppercase(),
462    };
463
464    match (arg.get_short(), arg.get_long()) {
465        (Some(short), Some(long)) => {
466            if arg.get_action().takes_values() {
467                write!(buffer, "`-{short}`, `--{long} <{value_name}>`")?
468            } else {
469                write!(buffer, "`-{short}`, `--{long}`")?
470            }
471        },
472        (Some(short), None) => {
473            if arg.get_action().takes_values() {
474                write!(buffer, "`-{short} <{value_name}>`")?
475            } else {
476                write!(buffer, "`-{short}`")?
477            }
478        },
479        (None, Some(long)) => {
480            if arg.get_action().takes_values() {
481                write!(buffer, "`--{} <{value_name}>`", long)?
482            } else {
483                write!(buffer, "`--{}`", long)?
484            }
485        },
486        (None, None) => {
487            debug_assert!(arg.is_positional(), "unexpected non-positional Arg with neither short nor long name: {arg:?}");
488
489            write!(buffer, "`<{value_name}>`",)?;
490        },
491    }
492
493    if let Some(aliases) = arg.get_visible_aliases().as_deref() {
494        if let Some(aliases_str) = get_alias_string(aliases) {
495            write!(
496                buffer,
497                " [{}: {aliases_str}]",
498                pluralize(aliases.len(), "alias", "aliases")
499            )?;
500        }
501    }
502
503    if let Some(help) = arg.get_long_help() {
504        // TODO: Parse formatting in the string
505        buffer.push_str(&indent(&help.to_string(), " — ", "   "))
506    } else if let Some(short_help) = arg.get_help() {
507        writeln!(buffer, " — {short_help}")?;
508    } else {
509        writeln!(buffer)?;
510    }
511
512    //--------------------
513    // Arg default values
514    //--------------------
515
516    if !arg.get_default_values().is_empty() {
517        let default_values: String = arg
518            .get_default_values()
519            .iter()
520            .map(|value| format!("`{}`", value.to_string_lossy()))
521            .collect::<Vec<String>>()
522            .join(", ");
523
524        if arg.get_default_values().len() > 1 {
525            // Plural
526            writeln!(buffer, "\n  Default values: {default_values}")?;
527        } else {
528            // Singular
529            writeln!(buffer, "\n  Default value: {default_values}")?;
530        }
531    }
532
533    //--------------------
534    // Arg possible values
535    //--------------------
536
537    let possible_values: Vec<PossibleValue> = arg
538        .get_possible_values()
539        .into_iter()
540        .filter(|pv| !pv.is_hide_set())
541        .collect();
542
543    // Print possible values for options that take a value, but not for flags
544    // that can only be either present or absent and do not take a value.
545    if !possible_values.is_empty()
546        && !matches!(arg.get_action(), clap::ArgAction::SetTrue)
547    {
548        let any_have_help: bool =
549            possible_values.iter().any(|pv| pv.get_help().is_some());
550
551        if any_have_help {
552            // If any of the possible values have help text, print them
553            // as a separate item in a bulleted list, and include the
554            // help text for those that have it. E.g.:
555            //
556            //     Possible values:
557            //     - `value1`:
558            //       The help text
559            //     - `value2`
560            //     - `value3`:
561            //       The help text
562
563            let text: String = possible_values
564                .iter()
565                .map(|pv| match pv.get_help() {
566                    Some(help) => {
567                        format!("  - `{}`:\n    {}\n", pv.get_name(), help)
568                    },
569                    None => format!("  - `{}`\n", pv.get_name()),
570                })
571                .collect::<Vec<String>>()
572                .join("");
573
574            writeln!(buffer, "\n  Possible values:\n{text}")?;
575        } else {
576            // If none of the possible values have any documentation, print
577            // them all inline on a single line.
578            let text: String = possible_values
579                .iter()
580                // TODO: Show PossibleValue::get_help(), and PossibleValue::get_name_and_aliases().
581                .map(|pv| format!("`{}`", pv.get_name()))
582                .collect::<Vec<String>>()
583                .join(", ");
584
585            writeln!(buffer, "\n  Possible values: {text}\n")?;
586        }
587    }
588
589    Ok(())
590}
591
592/// Utility function to get the canonical name of a command.
593///
594/// It's logic is to get the display name if it exists, otherwise get the bin
595/// name if it exists, otherwise get the package name.
596///
597/// Note that the default `Command.name` field of a clap command is typically
598/// meant for programmatic usage as well as for display (if no `display_name`
599/// override is set).
600fn get_canonical_name(command: &clap::Command) -> String {
601    command
602        .get_display_name()
603        .or_else(|| command.get_bin_name())
604        .map(|name| name.to_owned())
605        .unwrap_or_else(|| command.get_name().to_owned())
606}
607
608/// Indents non-empty lines. The output always ends with a newline.
609fn indent(s: &str, first: &str, rest: &str) -> String {
610    if s.is_empty() {
611        // For consistency. It's easiest to always add a newline at the end, and
612        // there's little reason not to.
613        return "\n".to_string();
614    }
615    let mut result = String::new();
616    let mut first_line = true;
617
618    for line in s.lines() {
619        if !line.is_empty() {
620            result.push_str(if first_line { first } else { rest });
621            result.push_str(line);
622            first_line = false;
623        }
624        result.push('\n');
625    }
626    result
627}
628
629fn get_alias_string(aliases: &[&str]) -> Option<String> {
630    if aliases.is_empty() {
631        return None;
632    }
633
634    Some(format!(
635        "{}",
636        aliases
637            .iter()
638            .map(|alias| format!("`{alias}`"))
639            .collect::<Vec<_>>()
640            .join(", ")
641    ))
642}
643
644#[cfg(test)]
645mod test {
646    use pretty_assertions::assert_eq;
647
648    #[test]
649    fn test_indent() {
650        use super::indent;
651        assert_eq!(
652            &indent("Header\n\nMore info", "___", "~~~~"),
653            "___Header\n\n~~~~More info\n"
654        );
655        assert_eq!(
656            &indent("Header\n\nMore info\n", "___", "~~~~"),
657            &indent("Header\n\nMore info", "___", "~~~~"),
658        );
659        assert_eq!(&indent("", "___", "~~~~"), "\n");
660        assert_eq!(&indent("\n", "___", "~~~~"), "\n");
661    }
662}