Skip to content

Commit 5c77ef2

Browse files
committed
feat(man): add check/generate CLI with roff and man subcommands
Replace the simple stdout-only binary with a proper CLI that supports: - `pdu-man-page generate roff 1` — render roff source to stdout - `pdu-man-page generate man 1` — render man page via man(1) to stdout - `pdu-man-page check roff 1` — verify exports/pdu.1 is up-to-date - `pdu-man-page check man 1` — verify exports/pdu.1.man is up-to-date The sync test now spawns the binary with `check` (like sync_ai_instructions), and the `man` test is gated behind a `man-test` feature since it requires man(1) to be installed. https://claude.ai/code/session_01CrXuWDMVQsiUBoy6ceACsF
1 parent bdd1451 commit 5c77ef2

4 files changed

Lines changed: 158 additions & 13 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ default = ["cli"]
6464
json = ["serde/derive", "serde_json"]
6565
cli = ["clap/derive", "clap_complete", "clap-utilities", "json"]
6666
cli-completions = ["cli"]
67+
man-test = []
6768
ai-instructions = ["clap/derive"]
6869

6970
[dependencies]

cli/man_page.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,120 @@
1+
use clap::{Parser, ValueEnum};
12
use parallel_disk_usage::man_page::render_man_page;
3+
use std::process::{Command, ExitCode};
24

3-
fn main() {
4-
print!("{}", render_man_page());
5+
const MANWIDTH: &str = "120";
6+
7+
/// Manage generated man pages.
8+
#[derive(Debug, Parser)]
9+
struct Args {
10+
/// Action to take.
11+
#[clap(value_enum)]
12+
action: Action,
13+
/// Type of file to target.
14+
#[clap(value_enum)]
15+
kind: Kind,
16+
/// Number of the man page.
17+
#[clap(value_enum)]
18+
page: Page,
19+
}
20+
21+
#[derive(Debug, Clone, ValueEnum)]
22+
enum Action {
23+
/// Check whether the man page is up-to-date.
24+
Check,
25+
/// Generate the man page.
26+
Generate,
27+
}
28+
29+
#[derive(Debug, Clone, ValueEnum)]
30+
enum Kind {
31+
/// Check or generate the roff file (`pdu.N`) from `Args`.
32+
Roff,
33+
/// Check or generate the man file (`pdu.N.man`) from the generated roff file (`pdu.N`).
34+
Man,
35+
}
36+
37+
#[derive(Debug, Clone, ValueEnum)]
38+
enum Page {
39+
#[clap(name = "1")]
40+
One,
41+
}
42+
43+
impl Page {
44+
fn number(&self) -> u8 {
45+
match self {
46+
Page::One => 1,
47+
}
48+
}
49+
}
50+
51+
fn roff_path(page_num: u8) -> String {
52+
format!("exports/pdu.{page_num}")
53+
}
54+
55+
fn man_path(page_num: u8) -> String {
56+
format!("exports/pdu.{page_num}.man")
57+
}
58+
59+
fn render_man_output(page_num: u8) -> Result<String, String> {
60+
let roff_file = roff_path(page_num);
61+
let output = Command::new("man")
62+
.env("MANWIDTH", MANWIDTH)
63+
.arg(format!("./{roff_file}"))
64+
.output()
65+
.map_err(|error| format!("failed to run man: {error}"))?;
66+
if !output.status.success() {
67+
let stderr = String::from_utf8_lossy(&output.stderr);
68+
return Err(format!("man failed: {stderr}"));
69+
}
70+
let content = String::from_utf8(output.stdout)
71+
.map_err(|error| format!("man output is not UTF-8: {error}"))?;
72+
Ok(content
73+
.lines()
74+
.map(str::trim_end)
75+
.collect::<Vec<_>>()
76+
.join("\n"))
77+
}
78+
79+
fn check_file(path: &str, expected: &str) -> ExitCode {
80+
match std::fs::read_to_string(path) {
81+
Ok(actual) if actual == *expected => ExitCode::SUCCESS,
82+
Ok(_) => {
83+
eprintln!("{path} is outdated, run ./generate-completions.sh to update it");
84+
ExitCode::FAILURE
85+
}
86+
Err(error) => {
87+
eprintln!("error reading {path}: {error}");
88+
ExitCode::FAILURE
89+
}
90+
}
91+
}
92+
93+
fn main() -> ExitCode {
94+
let args = Args::parse();
95+
let page_num = args.page.number();
96+
match (args.action, args.kind) {
97+
(Action::Generate, Kind::Roff) => {
98+
print!("{}", render_man_page());
99+
ExitCode::SUCCESS
100+
}
101+
(Action::Generate, Kind::Man) => match render_man_output(page_num) {
102+
Ok(content) => {
103+
print!("{content}");
104+
ExitCode::SUCCESS
105+
}
106+
Err(error) => {
107+
eprintln!("error: {error}");
108+
ExitCode::FAILURE
109+
}
110+
},
111+
(Action::Check, Kind::Roff) => check_file(&roff_path(page_num), &render_man_page()),
112+
(Action::Check, Kind::Man) => match render_man_output(page_num) {
113+
Ok(expected) => check_file(&man_path(page_num), &expected),
114+
Err(error) => {
115+
eprintln!("error: {error}");
116+
ExitCode::FAILURE
117+
}
118+
},
119+
}
5120
}

generate-completions.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ gen elvish completion.elv
1717
./run.sh pdu --help | sed 's/[[:space:]]*$//' > exports/long.help
1818
./run.sh pdu -h | sed 's/[[:space:]]*$//' > exports/short.help
1919
./run.sh pdu-usage-md > USAGE.md
20-
./run.sh pdu-man-page > exports/pdu.1
21-
MANWIDTH=120 man ./exports/pdu.1 | sed 's/[[:space:]]*$//' > exports/pdu.1.man
20+
./run.sh pdu-man-page generate roff 1 > exports/pdu.1
21+
./run.sh pdu-man-page generate man 1 > exports/pdu.1.man

tests/sync_man_page.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,49 @@
1-
//! The following test checks whether the man page file is outdated.
1+
//! The following tests check whether the man page files are outdated.
22
//!
3-
//! If the test fails, run `./generate-completions.sh` on the root of the repo to update the man page.
3+
//! If the tests fail, run `./generate-completions.sh` on the root of the repo to update the man page.
44
55
// Since the CLI in Windows looks a little different, and I am way too lazy to make two versions
6-
// of man page files, the following test would only run in UNIX-like environment.
6+
// of man page files, the following tests would only run in UNIX-like environment.
77
#![cfg(unix)]
88
#![cfg(feature = "cli")]
99

10-
use parallel_disk_usage::man_page::render_man_page;
10+
use command_extra::CommandExtra;
11+
use std::process::Command;
1112

12-
#[test]
13-
fn man_page() {
14-
let received = render_man_page();
15-
let expected = include_str!("../exports/pdu.1");
13+
const PDU_MAN_PAGE: &str = env!("CARGO_BIN_EXE_pdu-man-page");
14+
15+
fn check(kind: &str, page: &str) {
16+
let output = Command::new(PDU_MAN_PAGE)
17+
.with_args(["check", kind, page])
18+
.with_current_dir(env!("CARGO_MANIFEST_DIR"))
19+
.output()
20+
.expect("spawn pdu-man-page");
21+
let stdout = String::from_utf8_lossy(&output.stdout);
22+
let stdout = stdout.trim();
23+
if !stdout.is_empty() {
24+
eprintln!("STDOUT:\n{stdout}\n");
25+
}
26+
let stderr = String::from_utf8_lossy(&output.stderr);
27+
let stderr = stderr.trim();
28+
if !stderr.is_empty() {
29+
eprintln!("STDERR:\n{stderr}\n");
30+
}
1631
assert!(
17-
received == expected,
32+
output.status.success(),
1833
"man page is outdated, run ./generate-completions.sh to update it",
1934
);
2035
}
36+
37+
#[test]
38+
fn roff() {
39+
check("roff", "1");
40+
}
41+
42+
#[test]
43+
#[cfg_attr(
44+
not(feature = "man-test"),
45+
ignore = "requires man(1); enable with --features man-test"
46+
)]
47+
fn man() {
48+
check("man", "1");
49+
}

0 commit comments

Comments
 (0)