diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 25faf101d..2165e0dcc 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -12,8 +12,8 @@ use uucore::{encoding::Format, error::UResult, help_section, help_usage}; pub mod base_common; -const ABOUT: &str = help_section!("about"); -const USAGE: &str = help_usage!(); +const ABOUT: &str = help_section!("about", "base32.md"); +const USAGE: &str = help_usage!("base32.md"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 0b5dcb5b5..932a1f0c7 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -13,8 +13,8 @@ use uucore::{encoding::Format, error::UResult, help_section, help_usage}; use std::io::{stdin, Read}; -const ABOUT: &str = help_section!("about"); -const USAGE: &str = help_usage!(); +const ABOUT: &str = help_section!("about", "base64.md"); +const USAGE: &str = help_usage!("base64.md"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index b026bec2d..54ea17a63 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -22,9 +22,9 @@ pub mod format; pub mod options; mod units; -const ABOUT: &str = help_section!("about"); -const LONG_HELP: &str = help_section!("long help"); -const USAGE: &str = help_usage!(); +const ABOUT: &str = help_section!("about", "numfmt.md"); +const LONG_HELP: &str = help_section!("long help", "numfmt.md"); +const USAGE: &str = help_usage!("numfmt.md"); fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult<()> { for l in args { diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index 26667d9d7..ce63b0130 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -37,35 +37,114 @@ pub fn main(_args: TokenStream, stream: TokenStream) -> TokenStream { TokenStream::from(new) } -fn parse_help(section: &str) -> String { +/// Get the usage from the "Usage" section in the help file. +/// +/// The usage is assumed to be surrounded by markdown code fences. It may span +/// multiple lines. The first word of each line is assumed to be the name of +/// the util and is replaced by "{}" so that the output of this function can be +/// used with `uucore::format_usage`. +#[proc_macro] +pub fn help_usage(input: TokenStream) -> TokenStream { + let input: Vec = input.into_iter().collect(); + let filename = get_argument(&input, 0, "filename"); + let text: String = parse_usage(&parse_help("usage", &filename)); + TokenTree::Literal(Literal::string(&text)).into() +} + +/// Reads a section from a file of the util as a `str` literal. +/// +/// It reads from the file specified as the second argument, relative to the +/// crate root. The contents of this file are read verbatim, without parsing or +/// escaping. The name of the help file should match the name of the util. +/// I.e. numfmt should have a file called `numfmt.md`. By convention, the file +/// should start with a top-level section with the name of the util. The other +/// sections must start with 2 `#` characters. Capitalization of the sections +/// does not matter. Leading and trailing whitespace of each section will be +/// removed. +/// +/// Example: +/// ```md +/// # numfmt +/// ## About +/// Convert numbers from/to human-readable strings +/// +/// ## Long help +/// This text will be the long help +/// ``` +/// +/// ```rust,ignore +/// help_section!("about", "numfmt.md"); +/// ``` +#[proc_macro] +pub fn help_section(input: TokenStream) -> TokenStream { + let input: Vec = input.into_iter().collect(); + let section = get_argument(&input, 0, "section"); + let filename = get_argument(&input, 1, "filename"); + let text = parse_help(§ion, &filename); + TokenTree::Literal(Literal::string(&text)).into() +} + +/// Get an argument from the input vector of `TokenTree`. +/// +/// Asserts that the argument is a string literal and returns the string value, +/// otherwise it panics with an error. +fn get_argument(input: &[TokenTree], index: usize, name: &str) -> String { + // Multiply by two to ignore the `','` in between the arguments + let string = match &input.get(index * 2) { + Some(TokenTree::Literal(lit)) => lit.to_string(), + Some(_) => panic!("Argument {} should be a string literal.", index), + None => panic!("Missing argument at index {} for {}", index, name), + }; + + string + .parse::() + .unwrap() + .strip_prefix('"') + .unwrap() + .strip_suffix('"') + .unwrap() + .to_string() +} + +/// Read the help file and extract a section +fn parse_help(section: &str, filename: &str) -> String { let section = section.to_lowercase(); let section = section.trim_matches('"'); let mut content = String::new(); let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - // The package name will be something like uu_numfmt, hence we split once - // on '_' and take the second element. The help section should then be in a - // file called numfmt.md - path.push(format!( - "{}.md", - std::env::var("CARGO_PKG_NAME") - .unwrap() - .split_once('_') - .unwrap() - .1, - )); + path.push(filename); File::open(path) .unwrap() .read_to_string(&mut content) .unwrap(); + parse_help_section(section, &content) +} + +/// Get a single section from content +/// +/// The section must be a second level section (i.e. start with `##`). +fn parse_help_section(section: &str, content: &str) -> String { + fn is_section_header(line: &str, section: &str) -> bool { + line.strip_prefix("##") + .map_or(false, |l| l.trim().to_lowercase() == section) + } + + // We cannot distinguish between an empty or non-existing section below, + // so we do a quick test to check whether the section exists to provide + // a nice error message. + if content.lines().all(|l| !is_section_header(l, section)) { + panic!( + "The section '{}' could not be found in the help file. Maybe it is spelled wrong?", + section + ) + } + content .lines() - .skip_while(|&l| { - l.strip_prefix("##") - .map_or(true, |l| l.trim().to_lowercase() != section) - }) + .skip_while(|&l| !is_section_header(l, section)) .skip(1) .take_while(|l| !l.starts_with("##")) .collect::>() @@ -74,15 +153,13 @@ fn parse_help(section: &str) -> String { .to_string() } -/// Get the usage from the "Usage" section in the help file. +/// Parses a markdown code block into a usage string /// -/// The usage is assumed to be surrounded by markdown code fences. It may span -/// multiple lines. The first word of each line is assumed to be the name of -/// the util and is replaced by "{}" so that the output of this function can be -/// used with `uucore::format_usage`. -#[proc_macro] -pub fn help_usage(_input: TokenStream) -> TokenStream { - let text: String = parse_help("usage") +/// The code fences are removed and the name of the util is replaced +/// with `{}` so that it can be replaced with the appropriate name +/// at runtime. +fn parse_usage(content: &str) -> String { + content .strip_suffix("```") .unwrap() .lines() @@ -96,34 +173,74 @@ pub fn help_usage(_input: TokenStream) -> TokenStream { "{}".to_string() } }) - .collect(); - TokenTree::Literal(Literal::string(&text)).into() + .collect() } -/// Reads a section from the help file of the util as a `str` literal. -/// -/// It is read verbatim, without parsing or escaping. The name of the help file -/// should match the name of the util. I.e. numfmt should have a file called -/// `numfmt.md`. By convention, the file should start with a top-level section -/// with the name of the util. The other sections must start with 2 `#` -/// characters. Capitalization of the sections does not matter. Leading and -/// trailing whitespace will be removed. Example: -/// ```md -/// # numfmt -/// ## About -/// Convert numbers from/to human-readable strings -/// -/// ## Long help -/// This text will be the long help -/// ``` -#[proc_macro] -pub fn help_section(input: TokenStream) -> TokenStream { - let input: Vec = input.into_iter().collect(); - let value = match &input.get(0) { - Some(TokenTree::Literal(literal)) => literal.to_string(), - _ => panic!("Input to help_section should be a string literal!"), - }; - let input_str: String = value.parse().unwrap(); - let text = parse_help(&input_str); - TokenTree::Literal(Literal::string(&text)).into() +#[cfg(test)] +mod tests { + use super::{parse_help_section, parse_usage}; + + #[test] + fn section_parsing() { + let input = "\ + # ls\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert_eq!( + parse_help_section("some section", input), + "This is some section" + ); + assert_eq!( + parse_help_section("another section", input), + "This is the other section\nwith multiple lines" + ); + } + + #[test] + #[should_panic] + fn section_parsing_panic() { + let input = "\ + # ls\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + parse_help_section("non-existent section", input); + } + + #[test] + fn usage_parsing() { + let input = "\ + # ls\n\ + ## Usage\n\ + ```\n\ + ls -l\n\ + ```\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert_eq!(parse_usage(&parse_help_section("usage", input)), "{} -l",); + + assert_eq!( + parse_usage( + "\ + ```\n\ + util [some] [options]\n\ + ```\ + " + ), + "{} [some] [options]" + ) + } }