From c4ba21f720710dc09dcf86b02020bd8d93ee7393 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 29 Dec 2021 20:44:50 +0100 Subject: [PATCH 1/7] move numfmt help to a separate file --- src/uu/numfmt/help.md | 42 +++++++++++++++++++++++++++++++++++++ src/uu/numfmt/src/numfmt.rs | 40 +++-------------------------------- src/uucore_procs/src/lib.rs | 41 +++++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 src/uu/numfmt/help.md diff --git a/src/uu/numfmt/help.md b/src/uu/numfmt/help.md new file mode 100644 index 00000000000..51ce1de1521 --- /dev/null +++ b/src/uu/numfmt/help.md @@ -0,0 +1,42 @@ + +# numfmt + +## About + +Convert numbers from/to human-readable strings + +## Long Help + +UNIT options: + none no auto-scaling is done; suffixes will trigger an error + + auto accept optional single/two letter suffix: + + 1K = 1000, 1Ki = 1024, 1M = 1000000, 1Mi = 1048576, + + si accept optional single letter suffix: + + 1K = 1000, 1M = 1000000, ... + + iec accept optional single letter suffix: + + 1K = 1024, 1M = 1048576, ... + + iec-i accept optional two-letter suffix: + + 1Ki = 1024, 1Mi = 1048576, ... + +FIELDS supports cut(1) style field ranges: + N N'th field, counted from 1 + N- from N'th field, to end of line + N-M from N'th to M'th field (inclusive) + -M from first to M'th field (inclusive) + - all fields +Multiple fields/ranges can be separated with commas + +FORMAT must be suitable for printing one floating-point argument '%f'. +Optional quote (%'f) will enable --grouping (if supported by current locale). +Optional width value (%10f) will pad output. Optional zero (%010f) width +will zero pad the number. Optional negative values (%-10f) will left align. +Optional precision (%.1f) will override the input determined precision. + diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index d259d001dd6..dbe769ed5c7 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -5,8 +5,6 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// spell-checker:ignore N'th M'th - use crate::errors::*; use crate::format::format_and_print; use crate::options::*; @@ -17,47 +15,15 @@ use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; use uucore::ranges::Range; -use uucore::{format_usage, InvalidEncodingHandling}; +use uucore::{format_usage, help_section, InvalidEncodingHandling}; pub mod errors; pub mod format; pub mod options; mod units; -static ABOUT: &str = "Convert numbers from/to human-readable strings"; -static LONG_HELP: &str = "UNIT options: - none no auto-scaling is done; suffixes will trigger an error - - auto accept optional single/two letter suffix: - - 1K = 1000, 1Ki = 1024, 1M = 1000000, 1Mi = 1048576, - - si accept optional single letter suffix: - - 1K = 1000, 1M = 1000000, ... - - iec accept optional single letter suffix: - - 1K = 1024, 1M = 1048576, ... - - iec-i accept optional two-letter suffix: - - 1Ki = 1024, 1Mi = 1048576, ... - -FIELDS supports cut(1) style field ranges: - N N'th field, counted from 1 - N- from N'th field, to end of line - N-M from N'th to M'th field (inclusive) - -M from first to M'th field (inclusive) - - all fields -Multiple fields/ranges can be separated with commas - -FORMAT must be suitable for printing one floating-point argument '%f'. -Optional quote (%'f) will enable --grouping (if supported by current locale). -Optional width value (%10f) will pad output. Optional zero (%010f) width -will zero pad the number. Optional negative values (%-10f) will left align. -Optional precision (%.1f) will override the input determined precision. -"; +const ABOUT: &str = help_section!("about"); +const LONG_HELP: &str = help_section!("long help"); const USAGE: &str = "{} [OPTION]... [NUMBER]..."; fn handle_args<'a>(args: impl Iterator, options: &NumfmtOptions) -> UResult<()> { diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index 3a32dab83c3..036c24faa08 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -1,7 +1,9 @@ // Copyright (C) ~ Roy Ivy III ; MIT license extern crate proc_macro; -use proc_macro::TokenStream; +use std::{fs::File, io::Read, path::PathBuf}; + +use proc_macro::{Literal, TokenStream, TokenTree}; use quote::quote; //## rust proc-macro background info @@ -34,3 +36,40 @@ pub fn main(_args: TokenStream, stream: TokenStream) -> TokenStream { TokenStream::from(new) } + +#[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 input_str = input_str.to_lowercase().trim_matches('"').to_string(); + + let mut content = String::new(); + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + path.push("help.md"); + + File::open(path) + .unwrap() + .read_to_string(&mut content) + .unwrap(); + + let text = content + .lines() + .skip_while(|&l| { + l.strip_prefix("##") + .map_or(true, |l| l.trim().to_lowercase() != input_str) + }) + .skip(1) + .take_while(|l| !l.starts_with("##")) + .collect::>() + .join("\n") + .trim() + .to_string(); + + let str = TokenTree::Literal(Literal::string(&text)); + str.into() +} From 5a9380616b3175337aa4a52b9cfbe73ff9ec0376 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 6 Apr 2022 23:39:36 +0200 Subject: [PATCH 2/7] Use a markdown file named after the utils for `help_section` --- src/uu/numfmt/{help.md => numfmt.md} | 0 src/uucore_procs/src/lib.rs | 28 +++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) rename src/uu/numfmt/{help.md => numfmt.md} (100%) diff --git a/src/uu/numfmt/help.md b/src/uu/numfmt/numfmt.md similarity index 100% rename from src/uu/numfmt/help.md rename to src/uu/numfmt/numfmt.md diff --git a/src/uucore_procs/src/lib.rs b/src/uucore_procs/src/lib.rs index 036c24faa08..3b40d1460b4 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -37,6 +37,22 @@ pub fn main(_args: TokenStream, stream: TokenStream) -> TokenStream { TokenStream::from(new) } +/// 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(); @@ -50,7 +66,17 @@ pub fn help_section(input: TokenStream) -> TokenStream { let mut content = String::new(); let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); - path.push("help.md"); + // 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, + )); File::open(path) .unwrap() From 2df00344f764ebcc195fd0a9ed988aba5706cdfc Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 6 Apr 2022 23:46:49 +0200 Subject: [PATCH 3/7] add help file for base32 --- src/uu/base32/base32.md | 16 ++++++++++++++++ src/uu/base32/src/base32.rs | 16 +++------------- 2 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 src/uu/base32/base32.md diff --git a/src/uu/base32/base32.md b/src/uu/base32/base32.md new file mode 100644 index 00000000000..442f300bf20 --- /dev/null +++ b/src/uu/base32/base32.md @@ -0,0 +1,16 @@ +# base32 + +## Usage + +{} [OPTION]... [FILE] + +## About + +encode/decode data and print to standard output +With no FILE, or when FILE is -, read standard input. + +The data are encoded as described for the base32 alphabet in RFC +4648. When decoding, the input may contain newlines in addition +to the bytes of the formal base32 alphabet. Use --ignore-garbage +to attempt to recover from any other non-alphabet bytes in the +encoded stream. diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index ce246af0843..281a9084d78 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -8,22 +8,12 @@ use std::io::{stdin, Read}; use clap::Command; -use uucore::{encoding::Format, error::UResult}; +use uucore::{encoding::Format, error::UResult, help_section}; pub mod base_common; -static ABOUT: &str = "\ -encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -The data are encoded as described for the base32 alphabet in RFC -4648. When decoding, the input may contain newlines in addition -to the bytes of the formal base32 alphabet. Use --ignore-garbage -to attempt to recover from any other non-alphabet bytes in the -encoded stream. -"; - -const USAGE: &str = "{} [OPTION]... [FILE]"; +const ABOUT: &str = help_section!("about"); +const USAGE: &str = help_section!("usage"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { From dedb969d75f2fedc466dab12cab98d1edfd90edd Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Wed, 6 Apr 2022 23:49:46 +0200 Subject: [PATCH 4/7] add help file for base64 --- src/uu/base64/base64.md | 15 +++++++++++++++ src/uu/base64/src/base64.rs | 16 +++------------- 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 src/uu/base64/base64.md diff --git a/src/uu/base64/base64.md b/src/uu/base64/base64.md new file mode 100644 index 00000000000..864948985c8 --- /dev/null +++ b/src/uu/base64/base64.md @@ -0,0 +1,15 @@ +# base64 + +## Usage +{} [OPTION]... [FILE] + +## About + +encode/decode data and print to standard output +With no FILE, or when FILE is -, read standard input. + +The data are encoded as described for the base64 alphabet in RFC +3548. When decoding, the input may contain newlines in addition +to the bytes of the formal base64 alphabet. Use --ignore-garbage +to attempt to recover from any other non-alphabet bytes in the +encoded stream. diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index 760142bb282..d0120e4a8ef 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -9,22 +9,12 @@ use uu_base32::base_common; pub use uu_base32::uu_app; -use uucore::{encoding::Format, error::UResult}; +use uucore::{encoding::Format, error::UResult, help_section}; use std::io::{stdin, Read}; -static ABOUT: &str = "\ -encode/decode data and print to standard output -With no FILE, or when FILE is -, read standard input. - -The data are encoded as described for the base64 alphabet in RFC -3548. When decoding, the input may contain newlines in addition -to the bytes of the formal base64 alphabet. Use --ignore-garbage -to attempt to recover from any other non-alphabet bytes in the -encoded stream. -"; - -const USAGE: &str = "{0} [OPTION]... [FILE]"; +const ABOUT: &str = help_section!("about"); +const USAGE: &str = help_section!("usage"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { From c8e88e1898cdef10ea40c52b79ecbe9047833d1f Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Apr 2022 00:12:47 +0200 Subject: [PATCH 5/7] add `help_usage` macro --- src/uu/base32/base32.md | 5 ++- src/uu/base32/src/base32.rs | 4 +- src/uu/base64/base64.md | 4 +- src/uu/base64/src/base64.rs | 4 +- src/uu/numfmt/numfmt.md | 5 +++ src/uu/numfmt/src/numfmt.rs | 4 +- src/uucore_procs/src/lib.rs | 90 ++++++++++++++++++++++++------------- 7 files changed, 76 insertions(+), 40 deletions(-) diff --git a/src/uu/base32/base32.md b/src/uu/base32/base32.md index 442f300bf20..0889292ce8a 100644 --- a/src/uu/base32/base32.md +++ b/src/uu/base32/base32.md @@ -1,8 +1,9 @@ # base32 ## Usage - -{} [OPTION]... [FILE] +``` +base32 [OPTION]... [FILE] +``` ## About diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 281a9084d78..25faf101d5b 100644 --- a/src/uu/base32/src/base32.rs +++ b/src/uu/base32/src/base32.rs @@ -8,12 +8,12 @@ use std::io::{stdin, Read}; use clap::Command; -use uucore::{encoding::Format, error::UResult, help_section}; +use uucore::{encoding::Format, error::UResult, help_section, help_usage}; pub mod base_common; const ABOUT: &str = help_section!("about"); -const USAGE: &str = help_section!("usage"); +const USAGE: &str = help_usage!(); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { diff --git a/src/uu/base64/base64.md b/src/uu/base64/base64.md index 864948985c8..89b9cb618c3 100644 --- a/src/uu/base64/base64.md +++ b/src/uu/base64/base64.md @@ -1,7 +1,9 @@ # base64 ## Usage -{} [OPTION]... [FILE] +``` +base64 [OPTION]... [FILE] +``` ## About diff --git a/src/uu/base64/src/base64.rs b/src/uu/base64/src/base64.rs index d0120e4a8ef..0b5dcb5b564 100644 --- a/src/uu/base64/src/base64.rs +++ b/src/uu/base64/src/base64.rs @@ -9,12 +9,12 @@ use uu_base32::base_common; pub use uu_base32::uu_app; -use uucore::{encoding::Format, error::UResult, help_section}; +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_section!("usage"); +const USAGE: &str = help_usage!(); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { diff --git a/src/uu/numfmt/numfmt.md b/src/uu/numfmt/numfmt.md index 51ce1de1521..52bd743d87c 100644 --- a/src/uu/numfmt/numfmt.md +++ b/src/uu/numfmt/numfmt.md @@ -1,6 +1,11 @@ # numfmt +## Usage +``` +numfmt [OPTION]... [NUMBER]... +``` + ## About Convert numbers from/to human-readable strings diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index dbe769ed5c7..b026bec2d3b 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -15,7 +15,7 @@ use units::{IEC_BASES, SI_BASES}; use uucore::display::Quotable; use uucore::error::UResult; use uucore::ranges::Range; -use uucore::{format_usage, help_section, InvalidEncodingHandling}; +use uucore::{format_usage, help_section, help_usage, InvalidEncodingHandling}; pub mod errors; pub mod format; @@ -24,7 +24,7 @@ mod units; const ABOUT: &str = help_section!("about"); const LONG_HELP: &str = help_section!("long help"); -const USAGE: &str = "{} [OPTION]... [NUMBER]..."; +const USAGE: &str = help_usage!(); 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 3b40d1460b4..26667d9d7bb 100644 --- a/src/uucore_procs/src/lib.rs +++ b/src/uucore_procs/src/lib.rs @@ -37,32 +37,9 @@ pub fn main(_args: TokenStream, stream: TokenStream) -> TokenStream { TokenStream::from(new) } -/// 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 input_str = input_str.to_lowercase().trim_matches('"').to_string(); - +fn parse_help(section: &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()); @@ -83,19 +60,70 @@ pub fn help_section(input: TokenStream) -> TokenStream { .read_to_string(&mut content) .unwrap(); - let text = content + content .lines() .skip_while(|&l| { l.strip_prefix("##") - .map_or(true, |l| l.trim().to_lowercase() != input_str) + .map_or(true, |l| l.trim().to_lowercase() != section) }) .skip(1) .take_while(|l| !l.starts_with("##")) .collect::>() .join("\n") .trim() - .to_string(); + .to_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 text: String = parse_help("usage") + .strip_suffix("```") + .unwrap() + .lines() + .skip(1) // Skip the "```" of markdown syntax + .map(|l| { + // Replace the util name (assumed to be the first word) with "{}" + // to be replaced with the runtime value later. + if let Some((_util, args)) = l.split_once(' ') { + format!("{{}} {}", args) + } else { + "{}".to_string() + } + }) + .collect(); + TokenTree::Literal(Literal::string(&text)).into() +} - let str = TokenTree::Literal(Literal::string(&text)); - str.into() +/// 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() } From a3a69cf91948c88294aec8dff224db09c95cc734 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 23 Apr 2022 13:26:10 +0200 Subject: [PATCH 6/7] uucore: add filename as argument in help_usage and help_section uucore: make help_section and help_usage take an argument to select a file --- src/uu/base32/src/base32.rs | 4 +- src/uu/base64/src/base64.rs | 4 +- src/uu/numfmt/src/numfmt.rs | 6 +- src/uucore_procs/src/lib.rs | 221 +++++++++++++++++++++++++++--------- 4 files changed, 176 insertions(+), 59 deletions(-) diff --git a/src/uu/base32/src/base32.rs b/src/uu/base32/src/base32.rs index 25faf101d5b..2165e0dcc85 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 0b5dcb5b564..932a1f0c7d5 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 b026bec2d3b..54ea17a63d8 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 26667d9d7bb..ce63b013064 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]" + ) + } } From 2130b3ef696e98fce4cdc6b374de17e19618c0b9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 20 Aug 2022 12:37:52 +0200 Subject: [PATCH 7/7] Fix bad merge --- src/uu/numfmt/src/numfmt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index d250aafef11..189acbd5162 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -16,7 +16,7 @@ use uucore::display::Quotable; use uucore::error::UResult; use uucore::format_usage; use uucore::ranges::Range; -use uucore::{format_usage, help_section, help_usage, InvalidEncodingHandling}; +use uucore::{help_section, help_usage}; pub mod errors; pub mod format;