Skip to main content

dev_prune/commands/
man.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Handler for `devp man`.
5//
6// The pages are rendered from the same clap definition the binary parses arguments
7// with — the same `long_about` texts `--help` prints — so a flag cannot exist in the
8// manual and be missing from the program, or the other way round. That is the whole
9// reason this is a subcommand rather than checked-in roff files that go stale.
10
11use std::fs;
12use std::path::Path;
13
14use anyhow::{Context, Result};
15use clap::CommandFactory;
16use clap_mangen::Man;
17
18use crate::Cli;
19use crate::output;
20
21/// Print the main page to stdout, or with `--dir` write the full set: `devp.1`,
22/// `dev-prune.1` (the same page under the binary's other name) and one
23/// `devp-<command>.1` per subcommand.
24pub fn run(dir: Option<&str>) -> Result<()> {
25    let mut command = Cli::command().name("devp");
26    command.build();
27
28    let Some(dir) = dir else {
29        // Piped, like `completions`: `devp man | man -l -` must see roff and nothing
30        // else, so no header and no attribution line.
31        let mut out = Vec::new();
32        Man::new(command).render(&mut out)?;
33        use std::io::Write;
34        std::io::stdout().write_all(&out)?;
35        return Ok(());
36    };
37
38    let dir = Path::new(dir);
39    fs::create_dir_all(dir)
40        .with_context(|| format!("could not create {}", output::clean_path(dir)))?;
41
42    let mut written = 0usize;
43    let mut render_to = |name: &str, man: Man| -> Result<()> {
44        let path = dir.join(format!("{name}.1"));
45        let mut buf = Vec::new();
46        man.render(&mut buf)?;
47        fs::write(&path, buf)
48            .with_context(|| format!("could not write {}", output::clean_path(&path)))?;
49        written += 1;
50        Ok(())
51    };
52
53    for sub in command.get_subcommands() {
54        // `help` documents itself; a `devp-help.1` would be a page about a page.
55        if sub.get_name() == "help" {
56            continue;
57        }
58        let name = format!("devp-{}", sub.get_name());
59        // clap's `Str` only converts from `&'static str` without its "string" feature;
60        // leaking a dozen page names in a process about to exit is the honest trade.
61        render_to(
62            &name,
63            Man::new(sub.clone().name(name.clone().leak() as &str)),
64        )?;
65    }
66    render_to("devp", Man::new(command.clone()))?;
67    // The same executable answers to both names, and `man dev-prune` should work for
68    // the person who never learned the short one.
69    render_to("dev-prune", Man::new(command.clone().name("dev-prune")))?;
70
71    output::print_success(&format!(
72        "{written} man pages written to {}",
73        output::clean_path(dir)
74    ));
75    output::print_info(
76        "Install them by copying into a directory on `manpath`, e.g. `/usr/local/share/man/man1/`.",
77    );
78    Ok(())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn the_full_set_covers_every_subcommand() {
87        let tmp = tempfile::tempdir().unwrap();
88        run(Some(tmp.path().to_str().unwrap())).unwrap();
89
90        // One page per visible subcommand, plus the two top-level names.
91        let mut command = Cli::command();
92        command.build();
93        for sub in command.get_subcommands() {
94            if sub.get_name() == "help" {
95                continue;
96            }
97            let page = tmp.path().join(format!("devp-{}.1", sub.get_name()));
98            assert!(page.exists(), "missing {}", page.display());
99        }
100        assert!(tmp.path().join("devp.1").exists());
101        assert!(tmp.path().join("dev-prune.1").exists());
102    }
103
104    #[test]
105    fn a_page_carries_the_long_about_text() {
106        let tmp = tempfile::tempdir().unwrap();
107        run(Some(tmp.path().to_str().unwrap())).unwrap();
108        let run_page = fs::read_to_string(tmp.path().join("devp-run.1")).unwrap();
109        // A phrase from help::RUN_LONG — the proof the manual and --help are one text.
110        assert!(run_page.contains("gauntlet"), "{run_page}");
111    }
112}