1#[allow(clippy::mixed_attributes_style)]
11#[doc(hidden)]
12mod test_readme {
13 #![doc = include_str!("../README.md")]
14}
15
16use std::{
17 default,
18 fmt::{self, Write},
19};
20
21use clap::builder::PossibleValue;
22
23pub fn help_markdown<C: clap::CommandFactory>() -> String {
25 let command = C::command();
26
27 help_markdown_command(&command)
28}
29
30pub fn custom_help_markdown<C: clap::CommandFactory>(
32 options: MarkdownOptions,
33) -> String {
34 let command = C::command();
35
36 let mut buffer = String::with_capacity(100);
37
38 write_help_markdown(&mut buffer, &command, options);
39
40 buffer
41}
42
43pub fn help_markdown_command(command: &clap::Command) -> String {
45 let mut buffer = String::with_capacity(100);
46
47 write_help_markdown(&mut buffer, command, default::Default::default());
48
49 buffer
50}
51
52pub fn print_help_markdown<C: clap::CommandFactory>(options: MarkdownOptions) {
60 let command = C::command();
61
62 let mut buffer = String::with_capacity(100);
63
64 write_help_markdown(&mut buffer, &command, options);
65
66 println!("{}", buffer);
67}
68
69#[derive(Default)]
70pub struct MarkdownOptions {
71 pub title: Option<String>,
72 pub hide_footer: bool,
73 pub disable_toc: bool,
74}
75
76fn write_help_markdown(
77 buffer: &mut String,
78 command: &clap::Command,
79 options: MarkdownOptions,
80) {
81 let title_name = get_canonical_name(command);
86
87 let title = match options.title {
88 Some(ref title) => title.to_owned(),
89 None => format!("Command-Line Help for `{title_name}`"),
90 };
91 writeln!(buffer, "# {title}\n",).unwrap();
92
93 writeln!(
94 buffer,
95 "This document contains the help content for the `{}` command-line program.\n",
96 title_name
97 ).unwrap();
98
99 if ! options.disable_toc {
108 writeln!(buffer, "**Command Overview:**\n").unwrap();
109
110 build_table_of_contents_markdown(buffer, Vec::new(), command, 0)
111 .unwrap();
112
113 writeln!(buffer).unwrap();
114 }
115
116 build_command_markdown(buffer, Vec::new(), command, 0).unwrap();
121
122 if !options.hide_footer {
126 write!(buffer, r#"<hr/>
127
128<small><i>
129 This document was generated automatically by
130 <a href="https://crates.io/crates/clap-markdown"><code>clap-markdown</code></a>.
131</i></small>
132"#).unwrap();
133 }
134}
135
136#[allow(clippy::only_used_in_recursion)]
137fn build_table_of_contents_markdown(
138 buffer: &mut String,
139 parent_command_path: Vec<String>,
141 command: &clap::Command,
142 depth: usize,
143) -> std::fmt::Result {
144 if command.is_hide_set() {
147 return Ok(());
148 }
149
150 let title_name = get_canonical_name(command);
151
152 let command_path = {
154 let mut command_path = parent_command_path;
155 command_path.push(title_name);
156 command_path
157 };
158
159 writeln!(
160 buffer,
161 "* [`{}`↴](#{})",
162 command_path.join(" "),
163 command_path.join("-"),
164 )?;
165
166 for subcommand in command.get_subcommands() {
171 build_table_of_contents_markdown(
172 buffer,
173 command_path.clone(),
174 subcommand,
175 depth + 1,
176 )?;
177 }
178
179 Ok(())
180}
181
182#[allow(clippy::only_used_in_recursion)]
228fn build_command_markdown(
229 buffer: &mut String,
230 parent_command_path: Vec<String>,
232 command: &clap::Command,
233 depth: usize,
234) -> std::fmt::Result {
235 if command.is_hide_set() {
238 return Ok(());
239 }
240
241 let title_name = get_canonical_name(command);
242
243 let command_path = {
245 let mut command_path = parent_command_path.clone();
246 command_path.push(title_name);
247 command_path
248 };
249
250 writeln!(
265 buffer,
266 "## `{}`\n",
267 command_path.join(" "),
268 )?;
269
270 if let Some(long_about) = command.get_long_about() {
271 writeln!(buffer, "{}\n", long_about)?;
272 } else if let Some(about) = command.get_about() {
273 writeln!(buffer, "{}\n", about)?;
274 }
275
276 if let Some(help) = command.get_before_long_help() {
277 writeln!(buffer, "{}\n", help)?;
278 } else if let Some(help) = command.get_before_help() {
279 writeln!(buffer, "{}\n", help)?;
280 }
281
282 writeln!(
283 buffer,
284 "**Usage:** `{}{}`\n",
285 if parent_command_path.is_empty() {
286 String::new()
287 } else {
288 let mut s = parent_command_path.join(" ");
289 s.push(' ');
290 s
291 },
292 command
293 .clone()
294 .render_usage()
295 .to_string()
296 .replace("Usage: ", "")
297 )?;
298
299 if let Some(help) = command.get_after_long_help() {
300 writeln!(buffer, "{}\n", help)?;
301 } else if let Some(help) = command.get_after_help() {
302 writeln!(buffer, "{}\n", help)?;
303 }
304
305 if command.get_subcommands().next().is_some() {
310 writeln!(buffer, "###### **Subcommands:**\n")?;
311
312 for subcommand in command.get_subcommands() {
313 if subcommand.is_hide_set() {
314 continue;
315 }
316
317 let title_name = get_canonical_name(subcommand);
318
319 writeln!(
320 buffer,
321 "* `{}` — {}",
322 title_name,
323 match subcommand.get_about() {
324 Some(about) => about.to_string(),
325 None => String::new(),
326 }
327 )?;
328 }
329
330 writeln!(buffer)?;
331 }
332
333 if command.get_positionals().next().is_some() {
338 writeln!(buffer, "###### **Arguments:**\n")?;
339
340 for pos_arg in command.get_positionals() {
341 write_arg_markdown(buffer, pos_arg)?;
342 }
343
344 writeln!(buffer)?;
345 }
346
347 let non_pos: Vec<_> = command
352 .get_arguments()
353 .filter(|arg| !arg.is_positional())
354 .collect();
355
356 if !non_pos.is_empty() {
357 writeln!(buffer, "###### **Options:**\n")?;
358
359 for arg in non_pos {
360 write_arg_markdown(buffer, arg)?;
361 }
362
363 writeln!(buffer)?;
364 }
365
366 write!(buffer, "\n\n")?;
373
374 for subcommand in command.get_subcommands() {
375 build_command_markdown(
376 buffer,
377 command_path.clone(),
378 subcommand,
379 depth + 1,
380 )?;
381 }
382
383 Ok(())
384}
385
386fn write_arg_markdown(buffer: &mut String, arg: &clap::Arg) -> fmt::Result {
387 write!(buffer, "* ")?;
389
390 let value_name: String = match arg.get_value_names() {
391 Some([name, ..]) => name.as_str().to_owned(),
393 Some([]) => unreachable!(
394 "clap Arg::get_value_names() returned Some(..) of empty list"
395 ),
396 None => arg.get_id().to_string().to_ascii_uppercase(),
397 };
398
399 match (arg.get_short(), arg.get_long()) {
400 (Some(short), Some(long)) => {
401 if arg.get_action().takes_values() {
402 write!(buffer, "`-{short}`, `--{long} <{value_name}>`")?
403 } else {
404 write!(buffer, "`-{short}`, `--{long}`")?
405 }
406 },
407 (Some(short), None) => {
408 if arg.get_action().takes_values() {
409 write!(buffer, "`-{short} <{value_name}>`")?
410 } else {
411 write!(buffer, "`-{short}`")?
412 }
413 },
414 (None, Some(long)) => {
415 if arg.get_action().takes_values() {
416 write!(buffer, "`--{} <{value_name}>`", long)?
417 } else {
418 write!(buffer, "`--{}`", long)?
419 }
420 },
421 (None, None) => {
422 debug_assert!(arg.is_positional(), "unexpected non-positional Arg with neither short nor long name: {arg:?}");
423
424 write!(buffer, "`<{value_name}>`",)?;
425 },
426 }
427
428 if let Some(help) = arg.get_help() {
429 writeln!(buffer, " — {help}")?;
430 } else {
431 writeln!(buffer)?;
432 }
433
434 if !arg.get_default_values().is_empty() {
439 let default_values: String = arg
440 .get_default_values()
441 .iter()
442 .map(|value| format!("`{}`", value.to_string_lossy()))
443 .collect::<Vec<String>>()
444 .join(", ");
445
446 if arg.get_default_values().len() > 1 {
447 writeln!(buffer, "\n Default values: {default_values}")?;
449 } else {
450 writeln!(buffer, "\n Default value: {default_values}")?;
452 }
453 }
454
455 let possible_values: Vec<PossibleValue> = arg
460 .get_possible_values()
461 .into_iter()
462 .filter(|pv| !pv.is_hide_set())
463 .collect();
464
465 if !possible_values.is_empty() {
466 let any_have_help: bool =
467 possible_values.iter().any(|pv| pv.get_help().is_some());
468
469 if any_have_help {
470 let text: String = possible_values
482 .iter()
483 .map(|pv| match pv.get_help() {
484 Some(help) => {
485 format!(" - `{}`:\n {}\n", pv.get_name(), help)
486 },
487 None => format!(" - `{}`\n", pv.get_name()),
488 })
489 .collect::<Vec<String>>()
490 .join("");
491
492 writeln!(buffer, "\n Possible values:\n{text}")?;
493 } else {
494 let text: String = possible_values
497 .iter()
498 .map(|pv| format!("`{}`", pv.get_name()))
500 .collect::<Vec<String>>()
501 .join(", ");
502
503 writeln!(buffer, "\n Possible values: {text}\n")?;
504 }
505 }
506
507 Ok(())
508}
509
510fn get_canonical_name(command: &clap::Command) -> String {
514 command
515 .get_display_name()
516 .or_else(|| command.get_bin_name())
517 .map(|name| name.to_owned())
518 .unwrap_or_else(|| command.get_name().to_owned())
519}