cli_xtask/subcommand/
dist_build_man.rs1use std::iter;
2
3use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
4use chrono::{Datelike, Utc};
5use clap_mangen::Man;
6
7use crate::{config::Config, Result, Run};
8
9#[cfg_attr(doc, doc = include_str!("../../doc/cargo-xtask-dist-build-man.md"))]
11#[derive(Debug, Clone, Default, clap::Args)]
12#[non_exhaustive]
13pub struct DistBuildMan {}
14
15impl Run for DistBuildMan {
16 fn run(&self, config: &Config) -> Result<()> {
17 self.run(config)
18 }
19}
20
21impl DistBuildMan {
22 #[tracing::instrument(name = "dist-build-man", skip_all, err)]
24 pub fn run(&self, config: &Config) -> Result<()> {
25 tracing::info!("Building man pages...");
26
27 let Self {} = self;
28 let config = config.dist()?;
29
30 let man_dir = config.dist_working_directory(None).join("man");
31 let section = "1";
32 crate::fs::remove_dir(&man_dir)?;
33
34 for package in config.packages() {
35 for target in package.targets() {
36 if let Some(cmd) = target.command() {
37 let it = dist_build_man_pages(&man_dir, package.name(), cmd.clone(), section)?;
38 for res in it {
39 let (path, man) = res?;
40 let mut file = crate::fs::create_file(path)?;
41 man.render(&mut file)?;
42 }
43 }
44 }
45 }
46
47 Ok(())
48 }
49}
50
51fn dist_build_man_pages(
52 man_dir: &Utf8Path,
53 package_name: &str,
54 cmd: clap::Command,
55 section: impl Into<String>,
56) -> Result<impl Iterator<Item = Result<(Utf8PathBuf, Man)>>> {
57 let capitalized_name = {
58 let mut cs = package_name.chars();
59 match cs.next() {
60 Some(c) => c.to_uppercase().collect::<String>() + cs.as_str(),
61 None => String::new(),
62 }
63 };
64 let section = section.into();
65
66 let now = Utc::now();
67 let manual_name = format!("{capitalized_name} Command Manual");
68 let date = format!("{:04}-{:02}-{:02}", now.year(), now.month(), now.day());
69 let source = format!(
70 "{} {}",
71 cmd.get_name().replace(' ', "-"),
72 cmd.get_version()
73 .or_else(|| cmd.get_long_version())
74 .unwrap_or_default()
75 );
76
77 let man_dir = man_dir.to_owned();
78 let it = iterate_commands(cmd).map(move |cmd| {
79 let command_name = cmd.get_name().replace(' ', "-");
80 let filename = format!("{command_name}.{}", section);
81 let path = man_dir.join(filename);
82 let man = Man::new(cmd)
83 .title(command_name.to_uppercase())
84 .section(§ion)
85 .date(&date)
86 .source(&source)
87 .manual(&manual_name);
88 Ok((path, man))
89 });
90
91 Ok(it)
92}
93
94fn iterate_commands(cmd: clap::Command) -> Box<dyn Iterator<Item = clap::Command>> {
95 #[allow(clippy::needless_collect)]
96 let subcommands = cmd.get_subcommands().cloned().collect::<Vec<_>>();
97 let command_name = cmd.get_name().to_string();
98 let command_version = cmd.get_version().map(str::to_string);
99 let command_long_version = cmd.get_long_version().map(str::to_string);
100 let command_author = cmd.get_author().map(str::to_string);
101
102 let it = iter::once(cmd).chain(
103 subcommands
104 .into_iter()
105 .map(move |mut subcommand| {
106 let name = format!("{command_name} {}", subcommand.get_name());
107 subcommand = subcommand.name(name);
108 if subcommand.get_version().is_none() {
109 if let Some(version) = &command_version {
110 subcommand = subcommand.version(version);
111 }
112 }
113 if subcommand.get_long_version().is_none() {
114 if let Some(long_version) = &command_long_version {
115 subcommand = subcommand.long_version(long_version);
116 }
117 }
118 if subcommand.get_author().is_none() {
119 if let Some(author) = &command_author {
120 subcommand = subcommand.author(author);
121 }
122 }
123 subcommand
124 })
125 .flat_map(iterate_commands),
126 );
127 Box::new(it)
128}