Skip to main content

parallel_disk_usage/
usage_md.rs

1use crate::args::Args;
2use clap::builder::PossibleValue;
3use clap::{Arg, ArgAction, Command, CommandFactory};
4use derive_more::{Display, Error};
5use itertools::Itertools;
6use pipe_trait::Pipe;
7use std::borrow::Cow;
8
9/// Error produced when generating the usage Markdown.
10#[derive(Debug, Display, Error)]
11#[non_exhaustive]
12pub enum RenderUsageMdError {
13    /// A `visible_alias` duplicates the argument's own long flag name.
14    #[display("--{_0} has a visible_alias that duplicates its own flag name")]
15    RedundantVisibleLongAlias(#[error(not(source))] String),
16    /// A `visible_short_alias` duplicates the argument's own short flag name.
17    #[display("-{_0} has a visible_short_alias that duplicates its own flag name")]
18    RedundantVisibleShortAlias(#[error(not(source))] char),
19}
20
21/// Renders a Markdown reference page for `pdu`'s CLI.
22pub fn render_usage_md() -> Result<String, RenderUsageMdError> {
23    let mut command: Command = Args::command();
24    reject_redundant_aliases(&command)?;
25    let mut out = String::new();
26
27    let usage = command.render_usage().to_string();
28    if let Some(usage) = usage.strip_prefix("Usage:") {
29        out.push_str("# Usage\n\n```sh\n");
30        out.push_str(usage.trim());
31        out.push_str("\n```\n\n");
32    }
33
34    let mut arguments_heading_written = false;
35    for arg in command.get_arguments() {
36        if !arg.is_positional() || arg.is_hide_set() || arg.is_hide_long_help_set() {
37            continue;
38        }
39        if !arguments_heading_written {
40            arguments_heading_written = true;
41            out.push_str("## Arguments\n\n");
42        }
43        render_argument(&mut out, arg);
44    }
45    if arguments_heading_written {
46        out.push('\n');
47    }
48
49    let mut options_heading_written = false;
50    for arg in command.get_arguments() {
51        if arg.is_positional() || arg.is_hide_set() || arg.is_hide_long_help_set() {
52            continue;
53        }
54        if !options_heading_written {
55            options_heading_written = true;
56            out.push_str("## Options\n\n");
57        }
58        render_option(&mut out, arg);
59    }
60
61    if let Some(after_help) = command.get_after_long_help() {
62        let text = after_help.to_string();
63        let mut lines_iter = text.lines();
64        let mut has_examples = false;
65        for line in lines_iter.by_ref() {
66            if line.trim() == "Examples:" {
67                has_examples = true;
68                break;
69            }
70        }
71        if has_examples {
72            out.push_str("## Examples\n\n");
73            render_examples_section(&mut out, lines_iter);
74        }
75    }
76
77    Ok(out)
78}
79
80fn render_argument(out: &mut String, arg: &Arg) {
81    let name = arg
82        .get_value_names()
83        .and_then(|names| names.first())
84        .map(|n| n.as_str())
85        .unwrap_or_else(|| arg.get_id().as_str());
86    let is_multiple = arg
87        .get_num_args()
88        .map(|r| r.max_values() > 1)
89        .unwrap_or(false);
90    let display_name = if arg.is_required_set() {
91        if is_multiple {
92            format!("<{name}>...")
93        } else {
94            format!("<{name}>")
95        }
96    } else if is_multiple {
97        format!("[{name}]...")
98    } else {
99        format!("[{name}]")
100    };
101    let desc = get_help_text(arg);
102    let desc = ensure_ends_with_punctuation(&desc);
103    out.push_str(&format!("* `{display_name}`: {desc}\n"));
104}
105
106fn render_option(out: &mut String, arg: &Arg) {
107    let Some(primary_long) = arg.get_long() else {
108        return;
109    };
110
111    write_option_anchors(out, arg, primary_long);
112    out.push_str(&format!("### `--{primary_long}`\n\n"));
113
114    let aliases = collect_option_display_aliases(arg);
115    let default_values = collect_option_default_values(arg);
116    let possible_values = collect_option_possible_values(arg);
117
118    let has_metadata =
119        !aliases.is_empty() || !default_values.is_empty() || !possible_values.is_empty();
120
121    if !aliases.is_empty() {
122        let aliases_str = aliases.iter().map(|alias| format!("`{alias}`")).join(", ");
123        out.push_str(&format!("* _Aliases:_ {aliases_str}.\n"));
124    }
125    if !default_values.is_empty() {
126        let default_values_str = default_values.join(", ");
127        out.push_str(&format!("* _Default:_ `{default_values_str}`.\n"));
128    }
129    if !possible_values.is_empty() {
130        out.push_str("* _Choices:_\n");
131        for possible_value in &possible_values {
132            let name = possible_value.get_name();
133            if let Some(help) = possible_value.get_help() {
134                out.push_str(&format!("  - `{name}`: {help}\n"));
135            } else {
136                out.push_str(&format!("  - `{name}`\n"));
137            }
138        }
139    }
140
141    if has_metadata {
142        out.push('\n');
143    }
144
145    write_option_description(out, arg);
146}
147
148fn write_option_anchors(out: &mut String, arg: &Arg, primary_long: &str) {
149    let append_anchor = |out: &mut String, id: &str| {
150        out.push_str(&format!(r#"<a id="{id}" name="{id}"></a>"#));
151    };
152    let append_anchor_for_short = |out: &mut String, short: char| {
153        append_anchor(out, &format!("option-{short}"));
154    };
155    if let Some(short) = arg.get_short() {
156        append_anchor_for_short(out, short);
157    }
158    append_anchor(out, primary_long);
159    for alias in arg.get_visible_aliases().unwrap_or_default() {
160        append_anchor(out, alias);
161    }
162    for short in arg.get_visible_short_aliases().unwrap_or_default() {
163        append_anchor_for_short(out, short);
164    }
165    out.push('\n');
166}
167
168fn collect_option_display_aliases(arg: &Arg) -> Vec<String> {
169    let long_aliases = arg
170        .get_visible_aliases()
171        .into_iter()
172        .flatten()
173        .map(|alias| format!("--{alias}"));
174    let short_aliases = arg
175        .get_visible_short_aliases()
176        .into_iter()
177        .flatten()
178        .map(|alias| format!("-{alias}"));
179    arg.get_short()
180        .map(|short| format!("-{short}"))
181        .into_iter()
182        .chain(long_aliases)
183        .chain(short_aliases)
184        .collect()
185}
186
187fn collect_option_default_values(arg: &Arg) -> Vec<Cow<'_, str>> {
188    if arg.is_hide_default_value_set() {
189        return Vec::new();
190    }
191    if !arg.is_positional() && matches!(arg.get_action(), ArgAction::SetTrue) {
192        return Vec::new();
193    }
194    arg.get_default_values()
195        .iter()
196        .map(|value| value.to_string_lossy())
197        .collect()
198}
199
200fn collect_option_possible_values(arg: &Arg) -> Vec<PossibleValue> {
201    if arg.is_hide_possible_values_set() {
202        return Vec::new();
203    }
204    arg.get_possible_values()
205        .into_iter()
206        .filter(|possible_value| !possible_value.is_hide_set())
207        .collect()
208}
209
210fn write_option_description(out: &mut String, arg: &Arg) {
211    let description = get_help_text(arg);
212    if !description.is_empty() {
213        let description = ensure_ends_with_punctuation(&description);
214        out.push_str(&format!("{description}\n\n"));
215    } else {
216        out.push('\n');
217    }
218}
219
220fn get_help_text(arg: &Arg) -> Cow<'static, str> {
221    if !arg.is_positional() && arg.get_id() == "help" {
222        return Cow::Borrowed("Print help");
223    }
224    match (arg.get_help(), arg.get_long_help()) {
225        (None, None) => Cow::Borrowed(""),
226        (Some(help), None) | (_, Some(help)) => Cow::Owned(help.to_string()),
227    }
228}
229
230fn render_examples_section<'a>(out: &mut String, lines: impl Iterator<Item = &'a str>) {
231    for line in lines {
232        let line = line.trim();
233
234        if line.is_empty() {
235            continue;
236        }
237
238        if let Some(command) = line.strip_prefix('$') {
239            let command = command.trim();
240            out.push_str(&format!("```sh\n{command}\n```\n\n"));
241            continue;
242        }
243
244        out.push_str(&format!("### {line}\n\n"));
245    }
246}
247
248fn ensure_ends_with_punctuation(line: &str) -> Cow<'_, str> {
249    if line.is_empty() || line.ends_with('.') || line.ends_with('!') || line.ends_with('?') {
250        Cow::Borrowed(line)
251    } else {
252        Cow::Owned(format!("{line}."))
253    }
254}
255
256/// Rejects any argument whose visible alias duplicates its own primary flag name.
257///
258/// A visible alias matching the argument's own long name, or a visible short alias
259/// matching its own short flag, is a coding mistake that produces redundant output in
260/// USAGE.md.
261fn reject_redundant_aliases(command: &Command) -> Result<(), RenderUsageMdError> {
262    for arg in command.get_arguments() {
263        if let Some(primary_long) = arg.get_long() {
264            for alias in arg.get_visible_aliases().unwrap_or_default() {
265                if alias == primary_long {
266                    return primary_long
267                        .to_owned()
268                        .pipe(RenderUsageMdError::RedundantVisibleLongAlias)
269                        .pipe(Err);
270                }
271            }
272        }
273
274        if let Some(primary_short) = arg.get_short() {
275            for alias in arg.get_visible_short_aliases().unwrap_or_default() {
276                if alias == primary_short {
277                    return primary_short
278                        .pipe(RenderUsageMdError::RedundantVisibleShortAlias)
279                        .pipe(Err);
280                }
281            }
282        }
283    }
284
285    Ok(())
286}