1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Merge pull request #3180 from tertsdiepraam/long_help_file

Create `help_section` macro and use it for `numfmt`
This commit is contained in:
Terts Diepraam 2022-08-20 13:45:54 +02:00 committed by GitHub
commit 26b7099f83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 302 additions and 64 deletions

17
src/uu/base32/base32.md Normal file
View file

@ -0,0 +1,17 @@
# base32
## Usage
```
base32 [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.

View file

@ -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, help_usage};
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", "base32.md");
const USAGE: &str = help_usage!("base32.md");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {

17
src/uu/base64/base64.md Normal file
View file

@ -0,0 +1,17 @@
# base64
## Usage
```
base64 [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.

View file

@ -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, help_usage};
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", "base64.md");
const USAGE: &str = help_usage!("base64.md");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {

47
src/uu/numfmt/numfmt.md Normal file
View file

@ -0,0 +1,47 @@
<!-- spell-checker:ignore N'th M'th -->
# numfmt
## Usage
```
numfmt [OPTION]... [NUMBER]...
```
## 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.

View file

@ -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::*;
@ -18,47 +16,16 @@ use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::format_usage;
use uucore::ranges::Range;
use uucore::{help_section, help_usage};
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 USAGE: &str = "{} [OPTION]... [NUMBER]...";
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<Item = &'a str>, options: &NumfmtOptions) -> UResult<()> {
for l in args {

View file

@ -1,7 +1,9 @@
// Copyright (C) ~ Roy Ivy III <rivy.dev@gmail.com>; 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,211 @@ pub fn main(_args: TokenStream, stream: TokenStream) -> TokenStream {
TokenStream::from(new)
}
/// 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<TokenTree> = 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<TokenTree> = input.into_iter().collect();
let section = get_argument(&input, 0, "section");
let filename = get_argument(&input, 1, "filename");
let text = parse_help(&section, &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::<String>()
.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());
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| !is_section_header(l, section))
.skip(1)
.take_while(|l| !l.starts_with("##"))
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}
/// Parses a markdown code block into a usage string
///
/// 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()
.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()
}
#[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]"
)
}
}