Skip to main content

fs_core/cli/
docs.rs

1//! Man pages and shell completions, generated from the clap commands the
2//! tools actually parse with, so the documentation cannot describe a flag
3//! a program does not take.
4//!
5//! Written into a `share/` directory in the layout every tarball in the
6//! family uses, which is also where Homebrew links them from:
7//!
8//! ```text
9//! share/man/man8/mkfs.<fs>.8             section 8: mkfs.* and fsck.*
10//! share/man/man1/fs.<fs>.1               section 1: everything else,
11//! share/man/man1/<repo>.1                  the repository entry point too
12//! share/zsh/site-functions/_<name>
13//! share/bash-completion/completions/<name>
14//! share/fish/vendor_completions.d/<name>.fish
15//! ```
16//!
17//! One page and one completion per name, the repository's included, and a
18//! page per subcommand (`fs.<fs>-ls.1`, `rust-fs-<fs>-doctor.1`).
19
20use std::io;
21use std::path::{Path, PathBuf};
22
23use clap::Command as Cmd;
24/// The shells [`completion_path`] knows, from the generator itself.
25pub use clap_complete::Shell;
26
27use super::family::Family;
28
29/// Every name and its command, the repository entry point last, with its
30/// manual section.
31fn named_commands(family: &'static Family) -> Vec<(String, u8, Cmd)> {
32    let mut all: Vec<(String, u8, Cmd)> = family
33        .tools
34        .iter()
35        .map(|tool| {
36            (
37                tool.name.to_string(),
38                tool.section,
39                super::tool_command(family, tool),
40            )
41        })
42        .collect();
43    all.push((family.repo.to_string(), 1, super::repo_command(family)));
44    all
45}
46
47/// Write a man page per name under `share/man/man<section>/`, and one per
48/// subcommand beside it (`fs.<fs>-ls.1`), which is what the parent page's
49/// SUBCOMMANDS list refers to. Returns the paths written.
50pub fn man_pages(family: &'static Family, share: &Path) -> io::Result<Vec<PathBuf>> {
51    let mut written = Vec::new();
52    for (name, section, cmd) in named_commands(family) {
53        let dir = share.join("man").join(format!("man{section}"));
54        std::fs::create_dir_all(&dir)?;
55        // The page's own VERSION line: the version alone, not the
56        // `(crate) version` clap prints after a name.
57        let cmd = cmd.version(family.version);
58        // The entry point's tool verbs are documented once, under their
59        // dotted names (its SUBCOMMANDS list says which); only its own
60        // subcommands (`doctor`) get pages of their own.
61        let skip: Vec<&str> = if name == family.repo {
62            family.tools.iter().map(|t| t.verb).collect()
63        } else {
64            Vec::new()
65        };
66        write_pages(family, &dir, section, &name, cmd, &skip, &mut written)?;
67    }
68    Ok(written)
69}
70
71fn write_pages(
72    family: &Family,
73    dir: &Path,
74    section: u8,
75    name: &str,
76    cmd: Cmd,
77    skip: &[&str],
78    written: &mut Vec<PathBuf>,
79) -> io::Result<()> {
80    let path = dir.join(format!("{name}.{section}"));
81    let mut page = Vec::new();
82    // clap names are 'static; a generator run leaks a few dozen bytes.
83    let leaked: &'static str = Box::leak(name.to_string().into_boxed_str());
84    clap_mangen::Man::new(cmd.clone().name(leaked))
85        .title(name.to_uppercase())
86        .section(section.to_string())
87        .source(format!("{} {}", family.crate_name, family.version))
88        .manual(family.repo)
89        .render(&mut page)?;
90    std::fs::write(&path, page)?;
91    written.push(path);
92    for sub in cmd.get_subcommands() {
93        if sub.is_hide_set() || sub.get_name() == "help" || skip.contains(&sub.get_name()) {
94            continue;
95        }
96        let sub_name = format!("{name}-{}", sub.get_name());
97        write_pages(family, dir, section, &sub_name, sub.clone(), &[], written)?;
98    }
99    Ok(())
100}
101
102/// Where each shell's completion for `name` goes under `share/`.
103pub fn completion_path(share: &Path, shell: Shell, name: &str) -> PathBuf {
104    match shell {
105        Shell::Zsh => share.join("zsh/site-functions").join(format!("_{name}")),
106        Shell::Bash => share.join("bash-completion/completions").join(name),
107        Shell::Fish => share
108            .join("fish/vendor_completions.d")
109            .join(format!("{name}.fish")),
110        other => share.join(other.to_string()).join(name),
111    }
112}
113
114/// The shells completions are written for.
115pub const SHELLS: [Shell; 3] = [Shell::Zsh, Shell::Bash, Shell::Fish];
116
117/// Write zsh, bash and fish completions per name. Returns the paths
118/// written.
119pub fn completions(family: &'static Family, share: &Path) -> io::Result<Vec<PathBuf>> {
120    let mut written = Vec::new();
121    for (name, _, mut cmd) in named_commands(family) {
122        for shell in SHELLS {
123            let path = completion_path(share, shell, &name);
124            std::fs::create_dir_all(path.parent().expect("a completion has a directory"))?;
125            let mut script = Vec::new();
126            clap_complete::generate(shell, &mut cmd, name.clone(), &mut script);
127            std::fs::write(&path, script)?;
128            written.push(path);
129        }
130    }
131    Ok(written)
132}