Add example to manpage (#7841)

This commit is contained in:
n4n5
2025-11-07 23:47:32 +01:00
committed by GitHub
parent b2d0773356
commit 477b37ad06
4 changed files with 124 additions and 67 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ 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 = ["clap_complete", "clap_mangen", "fluent-syntax", "zip"]
uudoc = ["dep:clap_complete", "dep:clap_mangen", "dep:fluent-syntax", "dep:zip"]
## features
## Optional feature for stdbuf
# "feat_external_libstdbuf" == use an external libstdbuf.so for stdbuf instead of embedding it
+10 -5
View File
@@ -3,15 +3,13 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::Command;
use coreutils::validation;
use std::cmp;
use std::ffi::OsString;
use std::io::{self, Write};
use std::process;
use clap::Command;
use coreutils::validation;
const VERSION: &str = env!("CARGO_PKG_VERSION");
include!(concat!(env!("OUT_DIR"), "/uutils_map.rs"));
@@ -19,7 +17,14 @@ include!(concat!(env!("OUT_DIR"), "/uutils_map.rs"));
fn usage<T>(utils: &UtilityMap<T>, name: &str) {
println!("{name} {VERSION} (multi-call binary)\n");
println!("Usage: {name} [function [arguments...]]");
println!(" {name} --list\n");
println!(" {name} --list");
println!();
#[cfg(feature = "feat_common_core")]
{
println!("Functions:");
println!(" '<uutils>' [arguments...]");
println!();
}
println!("Options:");
println!(" --list lists all defined functions, one per row\n");
println!("Currently defined functions:\n");
+107 -52
View File
@@ -61,7 +61,11 @@ fn gen_coreutils_app<T: Args>(util_map: &UtilityMap<T>) -> clap::Command {
}
/// Generate the manpage for the utility in the first parameter
fn gen_manpage<T: Args>(args: impl Iterator<Item = OsString>, util_map: &UtilityMap<T>) -> ! {
fn gen_manpage<T: Args>(
tldr: &mut Option<ZipArchive<File>>,
args: impl Iterator<Item = OsString>,
util_map: &UtilityMap<T>,
) -> ! {
let all_utilities = validation::get_all_utilities(util_map);
let matches = Command::new("manpage")
@@ -78,7 +82,13 @@ fn gen_manpage<T: Args>(args: impl Iterator<Item = OsString>, util_map: &Utility
gen_coreutils_app(util_map)
} else {
validation::setup_localization_or_exit(utility);
util_map.get(utility).unwrap().1()
let mut cmd = util_map.get(utility).unwrap().1();
if let Some(zip) = tldr {
if let Ok(examples) = write_zip_examples(zip, utility, false) {
cmd = cmd.after_help(examples);
}
}
cmd
};
let man = Man::new(command);
@@ -122,19 +132,41 @@ fn gen_completions<T: Args>(args: impl Iterator<Item = OsString>, util_map: &Uti
process::exit(0);
}
/// print tldr error
fn print_tldr_error() {
eprintln!("Warning: No tldr archive found, so the documentation will not include examples.");
eprintln!(
"To include examples in the documentation, download the tldr archive and put it in the docs/ folder."
);
eprintln!();
eprintln!(" curl https://tldr.sh/assets/tldr.zip -o docs/tldr.zip");
eprintln!();
}
/// # Errors
/// Returns an error if the writer fails.
#[allow(clippy::too_many_lines)]
fn main() -> io::Result<()> {
let args: Vec<OsString> = uucore::args_os_filtered().collect();
let mut tldr_zip = File::open("docs/tldr.zip")
.ok()
.and_then(|f| ZipArchive::new(f).ok());
// Check for manpage/completion commands first
if args.len() > 1 {
let command = args.get(1).and_then(|s| s.to_str()).unwrap_or_default();
match command {
"manpage" => {
let args_iter = args.into_iter().skip(2);
gen_manpage(args_iter, &util_map::<Box<dyn Iterator<Item = OsString>>>());
if tldr_zip.is_none() {
print_tldr_error();
}
gen_manpage(
&mut tldr_zip,
args_iter,
&util_map::<Box<dyn Iterator<Item = OsString>>>(),
);
}
"completion" => {
let args_iter = args.into_iter().skip(2);
@@ -151,20 +183,9 @@ fn main() -> io::Result<()> {
}
}
}
let mut tldr_zip = File::open("docs/tldr.zip")
.ok()
.and_then(|f| ZipArchive::new(f).ok());
if tldr_zip.is_none() {
println!("Warning: No tldr archive found, so the documentation will not include examples.");
println!(
"To include examples in the documentation, download the tldr archive and put it in the docs/ folder."
);
println!();
println!(" curl https://tldr.sh/assets/tldr.zip -o docs/tldr.zip");
println!();
print_tldr_error();
}
let utils = util_map::<Box<dyn Iterator<Item = OsString>>>();
match std::fs::create_dir("docs/src/utils/") {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
@@ -431,44 +452,9 @@ impl MDWriter<'_, '_> {
/// Returns an error if the writer fails.
fn examples(&mut self) -> io::Result<()> {
if let Some(zip) = self.tldr_zip {
let content = if let Some(f) =
get_zip_content(zip, &format!("pages/common/{}.md", self.name))
{
f
} else if let Some(f) = get_zip_content(zip, &format!("pages/linux/{}.md", self.name)) {
f
} else {
println!(
"Warning: Could not find tldr examples for page '{}'",
self.name
);
return Ok(());
};
writeln!(self.w, "## Examples")?;
writeln!(self.w)?;
for line in content.lines().skip_while(|l| !l.starts_with('-')) {
if let Some(l) = line.strip_prefix("- ") {
writeln!(self.w, "{l}")?;
} else if line.starts_with('`') {
writeln!(self.w, "```shell\n{}\n```", line.trim_matches('`'))?;
} else if line.is_empty() {
writeln!(self.w)?;
} else {
println!("Not sure what to do with this line:");
println!("{line}");
}
if let Ok(examples) = write_zip_examples(zip, self.name, true) {
writeln!(self.w, "{examples}")?;
}
writeln!(self.w)?;
writeln!(
self.w,
"> The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md)."
)?;
writeln!(self.w, ">")?;
writeln!(
self.w,
"> Please note that, as uutils is a work in progress, some examples might fail."
)?;
}
Ok(())
}
@@ -548,3 +534,72 @@ fn get_zip_content(archive: &mut ZipArchive<impl Read + Seek>, name: &str) -> Op
archive.by_name(name).ok()?.read_to_string(&mut s).unwrap();
Some(s)
}
/// Extract examples for tldr.zip. The file docs/tldr.zip must exists
///
/// ```sh
/// curl https://tldr.sh/assets/tldr.zip -o docs/tldr.zip
/// ```
///
/// # Errors
///
/// Returns an error if the tldr.zip file cannot be opened or read
fn write_zip_examples(
archive: &mut ZipArchive<impl Read + Seek>,
name: &str,
output_markdown: bool,
) -> io::Result<String> {
let content = if let Some(f) = get_zip_content(archive, &format!("pages/common/{name}.md")) {
f
} else if let Some(f) = get_zip_content(archive, &format!("pages/linux/{name}.md")) {
f
} else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Could not find tldr examples for {name}"),
));
};
match format_examples(content, output_markdown) {
Err(e) => Err(std::io::Error::other(format!(
"Failed to format the tldr examples of {name}: {e}"
))),
Ok(s) => Ok(s),
}
}
/// Format examples using std::fmt::Write
fn format_examples(content: String, output_markdown: bool) -> Result<String, std::fmt::Error> {
use std::fmt::Write;
let mut s = String::new();
writeln!(s)?;
writeln!(s, "Examples")?;
writeln!(s)?;
for line in content.lines().skip_while(|l| !l.starts_with('-')) {
if let Some(l) = line.strip_prefix("- ") {
writeln!(s, "{l}")?;
} else if line.starts_with('`') {
if output_markdown {
writeln!(s, "```shell\n{}\n```", line.trim_matches('`'))?;
} else {
writeln!(s, "{}", line.trim_matches('`'))?;
}
} else if line.is_empty() {
writeln!(s)?;
} else {
// println!("Not sure what to do with this line:");
// println!("{line}");
}
}
writeln!(s)?;
writeln!(
s,
"> The examples are provided by the [tldr-pages project](https://tldr.sh) under the [CC BY 4.0 License](https://github.com/tldr-pages/tldr/blob/main/LICENSE.md)."
)?;
writeln!(s, ">")?;
writeln!(
s,
"> Please note that, as uutils is a work in progress, some examples might fail."
)?;
Ok(s)
}
+6 -9
View File
@@ -29,9 +29,8 @@ fn test_manpage_generation() {
output.status
);
assert!(
output.stderr.is_empty(),
"stderr should be empty but got: {}",
String::from_utf8_lossy(&output.stderr)
String::from_utf8_lossy(&output.stderr).contains("Warning: No tldr archive found"),
"stderr should contains tldr alert",
);
let output_str = String::from_utf8_lossy(&output.stdout);
@@ -53,9 +52,8 @@ fn test_manpage_coreutils() {
output.status
);
assert!(
output.stderr.is_empty(),
"stderr should be empty but got: {}",
String::from_utf8_lossy(&output.stderr)
String::from_utf8_lossy(&output.stderr).contains("Warning: No tldr archive found"),
"stderr should contains tldr alert",
);
let output_str = String::from_utf8_lossy(&output.stdout);
@@ -124,9 +122,8 @@ fn test_manpage_base64() {
output.status
);
assert!(
output.stderr.is_empty(),
"stderr should be empty but got: {}",
String::from_utf8_lossy(&output.stderr)
String::from_utf8_lossy(&output.stderr).contains("Warning: No tldr archive found"),
"stderr should contains tldr alert",
);
let output_str = String::from_utf8_lossy(&output.stdout);