diff --git a/Cargo.toml b/Cargo.toml index 80ad3cf8c..ba3134c25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/src/bin/uudoc.rs b/src/bin/uudoc.rs index e7445ae22..5eb75629a 100644 --- a/src/bin/uudoc.rs +++ b/src/bin/uudoc.rs @@ -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(utils: &UtilityMap) { println!("uudoc - Documentation generator for uutils coreutils"); @@ -100,63 +171,15 @@ fn gen_manpage( 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 = 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