cli_xtask/subcommand/
dist_build_man.rs

1use 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/// Arguments definition of the `dist-build-man` subcommand.
10#[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    /// Execute `dist-build-man` subcommand workflow
23    #[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(&section)
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}