parallel_disk_usage/
usage_md.rs1use 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#[derive(Debug, Display, Error)]
11#[non_exhaustive]
12pub enum RenderUsageMdError {
13 #[display("--{_0} has a visible_alias that duplicates its own flag name")]
15 RedundantVisibleLongAlias(#[error(not(source))] String),
16 #[display("-{_0} has a visible_short_alias that duplicates its own flag name")]
18 RedundantVisibleShortAlias(#[error(not(source))] char),
19}
20
21pub 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
256fn 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}