Skip to content

Commit 7a16661

Browse files
committed
refactor(man): simplify to just pdu.1 roff file
Remove all groff/man rendering complexity (pdu.1.man, strip_formatting, grotty flags, groff CI dependency). The binary now simply prints the roff content to stdout, and generate-completions.sh redirects it to exports/pdu.1. The sync test uses the library function directly. https://claude.ai/code/session_01CrXuWDMVQsiUBoy6ceACsF
1 parent 0ba4adf commit 7a16661

6 files changed

Lines changed: 13 additions & 345 deletions

File tree

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
if: runner.os == 'Linux'
4747
run: |
4848
sudo apt update
49-
sudo apt install -y squashfs-tools squashfuse fuse3 groff-base
49+
sudo apt install -y squashfs-tools squashfuse fuse3
5050
5151
- name: Test (dev)
5252
shell: bash

CONTRIBUTING.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ Some integration tests require external (non-Cargo) tools that are **not** manag
358358
- `squashfs-tools` (provides `mksquashfs`) — cross-device (`--one-file-system`) FUSE test
359359
- `squashfuse` (provides `squashfuse`) — cross-device (`--one-file-system`) FUSE test
360360
- `fuse3` (provides `fusermount3`, `/dev/fuse`) — cross-device (`--one-file-system`) FUSE test
361-
- `groff-base` (provides `groff`) — man page rendering sync test
362361

363362
Tests that need these tools will panic with a diagnostic message if they are missing. The panic message includes the specific `TEST_SKIP` variable to skip the test via `./test.sh`.
364363

cli/man_page.rs

Lines changed: 2 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1,5 @@
1-
use clap::{Parser, ValueEnum};
21
use parallel_disk_usage::man_page::render_man_page;
3-
use std::{
4-
fs,
5-
process::{Command, ExitCode},
6-
};
72

8-
const LINE_LENGTH: &str = "120";
9-
10-
/// Manage generated man pages.
11-
#[derive(Debug, Parser)]
12-
struct Args {
13-
/// Action to take.
14-
#[clap(value_enum)]
15-
action: Action,
16-
/// Type of file to target.
17-
#[clap(value_enum)]
18-
kind: Kind,
19-
/// Number of the man page.
20-
#[clap(value_enum)]
21-
page: Page,
22-
}
23-
24-
#[derive(Debug, Clone, ValueEnum)]
25-
enum Action {
26-
/// Check whether the man page is up-to-date.
27-
Check,
28-
/// Generate the man page.
29-
Generate,
30-
}
31-
32-
#[derive(Debug, Clone, ValueEnum)]
33-
enum Kind {
34-
/// Check or generate the roff file (`pdu.N`) from `Args`.
35-
Roff,
36-
/// Check or generate the man file (`pdu.N.man`) from the generated roff file (`pdu.N`).
37-
Man,
38-
}
39-
40-
#[derive(Debug, Clone, ValueEnum)]
41-
enum Page {
42-
#[clap(name = "1")]
43-
One,
44-
}
45-
46-
impl Page {
47-
fn number(&self) -> u8 {
48-
match self {
49-
Page::One => 1,
50-
}
51-
}
52-
}
53-
54-
fn roff_path(page_num: u8) -> String {
55-
format!("exports/pdu.{page_num}")
56-
}
57-
58-
fn man_path(page_num: u8) -> String {
59-
format!("exports/pdu.{page_num}.man")
60-
}
61-
62-
fn render_man_output(page_num: u8) -> Result<String, String> {
63-
let roff_file = roff_path(page_num);
64-
let output = Command::new("groff")
65-
.args(["-man", "-Tutf8", "-P-cbou"])
66-
.arg(format!("-rLL={LINE_LENGTH}n"))
67-
.arg(format!("./{roff_file}"))
68-
.output()
69-
.map_err(|error| format!("failed to run groff: {error}"))?;
70-
if !output.status.success() {
71-
let stderr = String::from_utf8_lossy(&output.stderr);
72-
return Err(format!("groff failed: {stderr}"));
73-
}
74-
let content = String::from_utf8(output.stdout)
75-
.map_err(|error| format!("groff output is not UTF-8: {error}"))?;
76-
Ok(normalize_text(&strip_formatting(&content)))
77-
}
78-
79-
/// Strips terminal formatting from grotty output.
80-
///
81-
/// Handles two styles grotty may use:
82-
/// - **SGR mode** (default): ANSI escape sequences like `\x1b[1m` (bold), `\x1b[0m` (reset).
83-
/// - **Legacy mode** (`-c`): Backspace overstrikes like `X\x08X` (bold), `_\x08X` (underline).
84-
fn strip_formatting(text: &str) -> String {
85-
let chars: Vec<char> = text.chars().collect();
86-
let mut result = String::with_capacity(text.len());
87-
let mut index = 0;
88-
while index < chars.len() {
89-
if chars[index] == '\x1b' && index + 1 < chars.len() && chars[index + 1] == '[' {
90-
// Skip ANSI escape: ESC [ ... m
91-
index += 2;
92-
while index < chars.len() && chars[index] != 'm' {
93-
index += 1;
94-
}
95-
if index < chars.len() {
96-
index += 1; // skip the 'm'
97-
}
98-
} else if index + 1 < chars.len() && chars[index + 1] == '\x08' {
99-
// Skip backspace overstrike: char + BS
100-
index += 2;
101-
} else {
102-
result.push(chars[index]);
103-
index += 1;
104-
}
105-
}
106-
result
107-
}
108-
109-
/// Strips trailing whitespace per line, trims trailing blank lines,
110-
/// and ensures the output ends with exactly one newline.
111-
fn normalize_text(text: &str) -> String {
112-
let mut result: String = text
113-
.lines()
114-
.map(str::trim_end)
115-
.collect::<Vec<_>>()
116-
.join("\n");
117-
let trimmed_len = result.trim_end().len();
118-
result.truncate(trimmed_len);
119-
result.push('\n');
120-
result
121-
}
122-
123-
fn write_file(path: &str, content: &str) -> ExitCode {
124-
match fs::write(path, content) {
125-
Ok(()) => ExitCode::SUCCESS,
126-
Err(error) => {
127-
eprintln!("error writing {path}: {error}");
128-
ExitCode::FAILURE
129-
}
130-
}
131-
}
132-
133-
fn check_file(path: &str, expected: &str) -> ExitCode {
134-
match fs::read_to_string(path) {
135-
Ok(actual) if actual == expected => ExitCode::SUCCESS,
136-
Ok(_) => {
137-
eprintln!("{path} is outdated, run ./generate-completions.sh to update it");
138-
ExitCode::FAILURE
139-
}
140-
Err(error) => {
141-
eprintln!("error reading {path}: {error}");
142-
ExitCode::FAILURE
143-
}
144-
}
145-
}
146-
147-
fn main() -> ExitCode {
148-
let args = Args::parse();
149-
let page_num = args.page.number();
150-
match (args.action, args.kind) {
151-
(Action::Generate, Kind::Roff) => write_file(&roff_path(page_num), &render_man_page()),
152-
(Action::Generate, Kind::Man) => match render_man_output(page_num) {
153-
Ok(content) => write_file(&man_path(page_num), &content),
154-
Err(error) => {
155-
eprintln!("error: {error}");
156-
ExitCode::FAILURE
157-
}
158-
},
159-
(Action::Check, Kind::Roff) => check_file(&roff_path(page_num), &render_man_page()),
160-
(Action::Check, Kind::Man) => match render_man_output(page_num) {
161-
Ok(expected) => check_file(&man_path(page_num), &expected),
162-
Err(error) => {
163-
eprintln!("error: {error}");
164-
ExitCode::FAILURE
165-
}
166-
},
167-
}
3+
fn main() {
4+
print!("{}", render_man_page());
1685
}

exports/pdu.1.man

Lines changed: 0 additions & 130 deletions
This file was deleted.

generate-completions.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,4 @@ 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 generate roff 1
21-
./run.sh pdu-man-page generate man 1
20+
./run.sh pdu-man-page > exports/pdu.1

tests/sync_man_page.rs

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,20 @@
1-
//! The following tests check whether the man page files are outdated.
1+
//! The following test checks whether the man page file is outdated.
22
//!
3-
//! If the tests fail, run `./generate-completions.sh` on the root of the repo to update the man page.
3+
//! If the test fails, 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 tests would only run in UNIX-like environment.
6+
// of man page files, the following test would only run in UNIX-like environment.
77
#![cfg(unix)]
88
#![cfg(feature = "cli")]
99

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

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-
}
12+
#[test]
13+
fn man_page() {
14+
let received = render_man_page();
15+
let expected = include_str!("../exports/pdu.1");
3116
assert!(
32-
output.status.success(),
17+
received == expected,
3318
"man page is outdated, run ./generate-completions.sh to update it",
3419
);
3520
}
36-
37-
#[test]
38-
fn roff() {
39-
check("roff", "1");
40-
}
41-
42-
#[test]
43-
#[cfg_attr(
44-
not(target_os = "linux"),
45-
ignore = "groff is only installed on Linux CI"
46-
)]
47-
fn man() {
48-
if which::which("groff").is_err() {
49-
panic!(
50-
"{}\n{}",
51-
"error: This test requires `groff` but it was not found.",
52-
"hint: Install `groff` (or `groff-base`) for your platform, \
53-
or rerun via `TEST_SKIP='man' ./test.sh` to skip this test.",
54-
);
55-
}
56-
check("man", "1");
57-
}

0 commit comments

Comments
 (0)