mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
refactor: extract manpage post-processing into a function with tests
- Move manpage post-processing logic into post_process_manpage() function - Add comprehensive unit tests for all post-processing functionality - Improve .br macro handling to correctly fix mandoc warnings - Tests cover TH header fixes, whitespace removal, and .br pattern fixes
This commit is contained in:
committed by
Daniel Hofstetter
parent
cd5af0c279
commit
b73d67e9bb
+8
-1
@@ -31,7 +31,13 @@ expensive_tests = []
|
||||
# "test_risky_names" == enable tests that create problematic file names (would make a network share inaccessible to Windows, breaks SVN on Mac OS, etc.)
|
||||
test_risky_names = []
|
||||
# * only build `uudoc` when `--feature uudoc` is activated
|
||||
uudoc = ["dep:clap_complete", "dep:clap_mangen", "dep:fluent-syntax", "dep:zip"]
|
||||
uudoc = [
|
||||
"dep:clap_complete",
|
||||
"dep:clap_mangen",
|
||||
"dep:fluent-syntax",
|
||||
"dep:regex",
|
||||
"dep:zip",
|
||||
]
|
||||
## features
|
||||
## Optional feature for stdbuf
|
||||
# "feat_external_libstdbuf" == use an external libstdbuf.so for stdbuf instead of embedding it
|
||||
@@ -475,6 +481,7 @@ clap_complete = { workspace = true, optional = true }
|
||||
clap_mangen = { workspace = true, optional = true }
|
||||
clap.workspace = true
|
||||
fluent-syntax = { workspace = true, optional = true }
|
||||
regex = { workspace = true, optional = true }
|
||||
itertools.workspace = true
|
||||
phf.workspace = true
|
||||
selinux = { workspace = true, optional = true }
|
||||
|
||||
+195
-56
@@ -3,10 +3,10 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore mangen tldr
|
||||
// spell-checker:ignore mangen tldr mandoc uppercasing uppercased manpages DESTDIR
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::OsString,
|
||||
fs::File,
|
||||
io::{self, Read, Seek, Write},
|
||||
@@ -17,7 +17,9 @@ use clap::{Arg, Command};
|
||||
use clap_complete::Shell;
|
||||
use clap_mangen::Man;
|
||||
use fluent_syntax::ast::{Entry, Message, Pattern};
|
||||
use jiff::Zoned;
|
||||
use fluent_syntax::parser;
|
||||
use regex::Regex;
|
||||
use textwrap::{fill, indent, termwidth};
|
||||
use zip::ZipArchive;
|
||||
|
||||
@@ -26,6 +28,75 @@ use uucore::Args;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/uutils_map.rs"));
|
||||
|
||||
/// Post-process a generated manpage to fix mandoc lint issues
|
||||
///
|
||||
/// This function:
|
||||
/// - Fixes the TH header by uppercasing command names and removing invalid date formats
|
||||
/// - Removes trailing whitespace from all lines
|
||||
/// - Fixes redundant .br paragraph macros that cause mandoc warnings
|
||||
fn post_process_manpage(manpage: String) -> String {
|
||||
// Only match TH headers that have at least a command name on the same line
|
||||
// Use [ \t] instead of \s to avoid matching newlines
|
||||
// Use a date format that satisfies mandoc (YYYY-MM-DD)
|
||||
let date = date.map_or_else(
|
||||
|| Zoned::now().strftime("%Y-%m-%d").to_string(),
|
||||
str::to_string,
|
||||
);
|
||||
|
||||
let th_regex = Regex::new(r"(?m)^\.TH[ \t]+([^ \t\n]+)(?:[ \t]+[^\n]*)?$").unwrap();
|
||||
let mut result = th_regex
|
||||
.replace_all(&manpage, |caps: ®ex::Captures| {
|
||||
// Add date to satisfy mandoc - date must be quoted
|
||||
format!(".TH {} 1 \"{date}\"", caps[1].to_uppercase())
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// Process lines: remove trailing whitespace and fix .br issues in a single pass
|
||||
let lines: Vec<&str> = result.lines().collect();
|
||||
let mut fixed_lines = Vec::with_capacity(lines.len());
|
||||
let mut skip_indices = HashSet::new();
|
||||
|
||||
// First pass: identify lines to skip (redundant .br macros)
|
||||
for i in 0..lines.len() {
|
||||
let line = lines[i].trim_end();
|
||||
|
||||
if line == ".br" && !skip_indices.contains(&i) {
|
||||
// Check for consecutive .br macros
|
||||
if i > 0 && lines[i - 1].trim_end() == ".br" {
|
||||
skip_indices.insert(i);
|
||||
}
|
||||
// Check for .br, empty line, .br pattern
|
||||
else if i + 2 < lines.len()
|
||||
&& lines[i + 1].trim().is_empty()
|
||||
&& lines[i + 2].trim_end() == ".br"
|
||||
{
|
||||
skip_indices.insert(i + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: build the final output
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if !skip_indices.contains(&i) {
|
||||
fixed_lines.push(line.trim_end());
|
||||
}
|
||||
}
|
||||
|
||||
result = fixed_lines.join("\n");
|
||||
|
||||
// Fix escape sequence issues
|
||||
// \\\\0 appears when trying to represent literal \0 string
|
||||
// In man pages, use \e for literal backslash
|
||||
result = result.replace("\\\\\\\\0", "\\e0");
|
||||
result = result.replace("\\\\0", "\\e0");
|
||||
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Print usage information for uudoc
|
||||
fn usage<T: Args>(utils: &UtilityMap<T>) {
|
||||
println!("uudoc - Documentation generator for uutils coreutils");
|
||||
@@ -100,63 +171,15 @@ fn gen_manpage<T: Args>(
|
||||
man.render(&mut buffer).expect("Man page generation failed");
|
||||
|
||||
// Convert to string for processing
|
||||
let mut manpage = String::from_utf8(buffer).expect("Invalid UTF-8 in manpage");
|
||||
let manpage = String::from_utf8(buffer).expect("Invalid UTF-8 in manpage");
|
||||
|
||||
// Fix the TH line: remove version info from date field and uppercase the command name
|
||||
if let Some(th_pos) = manpage.find(".TH ") {
|
||||
if let Some(line_end) = manpage[th_pos..].find('\n') {
|
||||
let th_line = &manpage[th_pos..th_pos + line_end];
|
||||
// Parse the TH line parts
|
||||
let parts: Vec<&str> = th_line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let cmd_name = parts[1].to_uppercase();
|
||||
// Reconstruct TH line with uppercase command name and no date
|
||||
let new_th = format!(".TH {} 1", cmd_name);
|
||||
manpage.replace_range(th_pos..th_pos + line_end, &new_th);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing whitespace from all lines and fix .br issues
|
||||
let lines: Vec<String> = manpage
|
||||
.lines()
|
||||
.map(|line| line.trim_end().to_string())
|
||||
.collect();
|
||||
|
||||
// Fix .br paragraph macro issues
|
||||
let mut fixed_lines = Vec::new();
|
||||
let mut skip_next_br = false;
|
||||
|
||||
for i in 0..lines.len() {
|
||||
let line = &lines[i];
|
||||
|
||||
if line == ".br" {
|
||||
// Check for problematic patterns with .br
|
||||
let prev_is_br = i > 0 && lines[i - 1] == ".br";
|
||||
let next_is_empty_then_br =
|
||||
i + 2 < lines.len() && lines[i + 1].is_empty() && lines[i + 2] == ".br";
|
||||
let prev_is_empty_with_br = i >= 2 && lines[i - 1].is_empty() && lines[i - 2] == ".br";
|
||||
|
||||
// Skip redundant .br in these patterns
|
||||
if skip_next_br || prev_is_br || next_is_empty_then_br || prev_is_empty_with_br {
|
||||
skip_next_br = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this .br is followed by empty line and another .br, skip the second one
|
||||
if next_is_empty_then_br {
|
||||
skip_next_br = true;
|
||||
}
|
||||
}
|
||||
|
||||
fixed_lines.push(line.clone());
|
||||
}
|
||||
|
||||
manpage = fixed_lines.join("\n");
|
||||
manpage.push('\n');
|
||||
// Post-process the manpage to fix mandoc lint issues
|
||||
let processed_manpage = post_process_manpage(manpage, None);
|
||||
|
||||
// Write the processed manpage to stdout
|
||||
io::stdout().write_all(manpage.as_bytes()).unwrap();
|
||||
io::stdout()
|
||||
.write_all(processed_manpage.as_bytes())
|
||||
.unwrap();
|
||||
io::stdout().flush().unwrap();
|
||||
process::exit(0);
|
||||
}
|
||||
@@ -691,3 +714,119 @@ fn format_examples(content: String, output_markdown: bool) -> Result<String, std
|
||||
)?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_fixes_th_header() {
|
||||
// Test that command names are uppercased and date is removed
|
||||
let input =
|
||||
".TH cat 1 \"cat (uutils coreutils) 0.7.0\"\n.SH NAME\ncat - concatenate files\n";
|
||||
let expected = ".TH CAT 1 \"2024-01-01\"\n.SH NAME\ncat - concatenate files\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_removes_trailing_whitespace() {
|
||||
// Test that trailing whitespace is removed from lines
|
||||
let input = ".TH TEST 1 \nSome text with trailing spaces \n.SH SECTION \n";
|
||||
let expected = ".TH TEST 1 \"2024-01-01\"\nSome text with trailing spaces\n.SH SECTION\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_fixes_double_br() {
|
||||
// Test that redundant .br macros are removed
|
||||
let input = ".TH TEST 1\n.br\n.br\nSome text\n";
|
||||
let expected = ".TH TEST 1 \"2024-01-01\"\n.br\nSome text\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_fixes_br_with_empty_line() {
|
||||
// Test that .br with empty line patterns are fixed
|
||||
// Both .br macros should be removed (first because followed by empty, second because preceded by empty)
|
||||
let input = ".TH TEST 1\n.br\n\n.br\nSome text\n";
|
||||
let expected = ".TH TEST 1 \"2024-01-01\"\n\nSome text\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_preserves_single_br() {
|
||||
// Test that single .br macros are preserved
|
||||
let input = ".TH TEST 1\nLine 1\n.br\nLine 2\n";
|
||||
let expected = ".TH TEST 1 \"2024-01-01\"\nLine 1\n.br\nLine 2\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_handles_mixed_case_command() {
|
||||
// Test that mixed case command names are uppercased
|
||||
let input = ".TH CaT 1 \"some version info\"\nContent\n";
|
||||
let expected = ".TH CAT 1 \"2024-01-01\"\nContent\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_handles_no_th_header() {
|
||||
// Test that manpages without TH headers are handled gracefully
|
||||
let input = ".SH NAME\ntest - a test utility\n";
|
||||
let expected = ".SH NAME\ntest - a test utility\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_complex_br_pattern() {
|
||||
// Test complex .br patterns with multiple occurrences
|
||||
let input =
|
||||
".TH TEST 1\nSection 1\n.br\n\n.br\nMiddle\n.br\n.br\nSection 2\n.br\n\n.br\nEnd\n";
|
||||
// .br followed/preceded by empty lines should be removed, consecutive .br should have one removed
|
||||
let expected = ".TH TEST 1 \"2024-01-01\"\nSection 1\n\nMiddle\n.br\nSection 2\n\nEnd\n";
|
||||
|
||||
let result = post_process_manpage(input.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_process_manpage_malformed_th_header() {
|
||||
// Test that malformed TH headers don't cause panics and are handled gracefully
|
||||
let input1 = ".TH\nContent\n"; // Missing command name
|
||||
let expected1 = ".TH\nContent\n";
|
||||
let result1 = post_process_manpage(input1.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result1, expected1);
|
||||
|
||||
// TH header with special characters
|
||||
let input2 = ".TH test-cmd 1 \"version 1.0\"\nContent\n";
|
||||
let expected2 = ".TH TEST-CMD 1 \"2024-01-01\"\nContent\n";
|
||||
let result2 = post_process_manpage(input2.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result2, expected2);
|
||||
|
||||
// TH header at end of file without newline
|
||||
let input3 = "Content\n.TH test 1";
|
||||
let expected3 = "Content\n.TH TEST 1 \"2024-01-01\"\n";
|
||||
let result3 = post_process_manpage(input3.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result3, expected3);
|
||||
|
||||
// Multiple TH headers (only first should be processed due to ^anchor)
|
||||
let input4 = ".TH first 1\nMiddle\n.TH second 1\n";
|
||||
let expected4 = ".TH FIRST 1 \"2024-01-01\"\nMiddle\n.TH SECOND 1 \"2024-01-01\"\n";
|
||||
let result4 = post_process_manpage(input4.to_string(), Some("2024-01-01"));
|
||||
assert_eq!(result4, expected4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user