Skip to content

Commit

Permalink
remove markdown rendering and make help write to stdout
Browse files Browse the repository at this point in the history
The markdown rendering is too complicated at the moment
and slows the rest of development down too much. We can
add it back in later.

The generated code for the help string now writes
directly to stdout, instead of building up a String,
this leads to nicer code and is probably faster.
  • Loading branch information
tertsdiepraam committed Jun 4, 2023
1 parent d98f3ca commit a6d9e5b
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 654 deletions.
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ readme = "README.md"
[dependencies]
derive = { version = "0.1.0", path = "derive" }
lexopt = "0.3.0"
term_md = { version = "0.1.0", path = "term_md" }

[workspace]
members = [
"term_md",
"derive",
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# uutils-args

An experimental derive-based argument parser specifically for uutils
72 changes: 29 additions & 43 deletions derive/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use crate::{
argument::{ArgType, Argument},
flags::Flags,
markdown::{get_after_event, get_h2, str_to_renderer},
help_parser::{parse_about, parse_usage, parse_section},
};
use proc_macro2::TokenStream;
use quote::quote;
Expand Down Expand Up @@ -44,73 +44,59 @@ pub(crate) fn help_string(
..
} => {
let flags = flags.format();
let renderer = str_to_renderer(help);
options.push(quote!((#flags, #renderer)));
options.push(quote!((#flags, #help)));
}
// Hidden arguments should not show up in --help
ArgType::Option { hidden: true, .. } => {}
ArgType::Positional { .. } => {}
}
}

let (summary, after_options) = if let Some(file) = &file {
let (summary, after_options) = read_help_file(file);
(
quote!(s.push_str(&#summary.render());),
quote!(
s.push('\n');
s.push_str(&#after_options.render());
),
)

// FIXME: We need to get an option per item and provide proper defaults
let (summary, usage, after_options) = if let Some(file) = file {
read_help_file(file)
} else {
(quote!(), quote!())
("".into(), "".into(), "".into())
};

if !help_flags.is_empty() {
let flags = help_flags.format();
let renderer = str_to_renderer("Display this help message");
options.push(quote!((#flags, #renderer)));
options.push(quote!((#flags, "Display this help message")));
}

if !version_flags.is_empty() {
let flags = version_flags.format();
let renderer = str_to_renderer("Display version information");
options.push(quote!((#flags, #renderer)));
options.push(quote!((#flags, "Display version information")));
}

let options = if !options.is_empty() {
let options = quote!([#(#options),*]);
quote!(
s.push_str("\nOptions:\n");
for (flags, renderer) in #options {
writeln!(w, "\nOptions:")?;
for (flags, help_string) in #options {
let indent = " ".repeat(#indent);

let help_string = renderer.render();
let mut help_lines = help_string.lines();
s.push_str(&indent);
s.push_str(&flags);
write!(w, "{}", &indent)?;
write!(w, "{}", &flags)?;

if flags.len() <= #width {
let line = match help_lines.next() {
Some(line) => line,
None => {
s.push('\n');
writeln!(w)?;
continue;
},
};
let help_indent = " ".repeat(#width-flags.len()+2);
s.push_str(&help_indent);
s.push_str(line);
s.push('\n');
writeln!(w, "{}{}", help_indent, line)?;
} else {
s.push('\n');
writeln!(w, "\n")?;
}

let help_indent = " ".repeat(#width+#indent+2);
for line in help_lines {
s.push_str(&help_indent);
s.push_str(line);
s.push('\n');
writeln!(w, "{}{}", help_indent, line)?;
}
}
)
Expand All @@ -119,26 +105,25 @@ pub(crate) fn help_string(
};

quote!(
let mut s = String::new();

s.push_str(&format!("{} {}\n",
let mut w = ::std::io::stdout();
use ::std::io::Write;
writeln!(w, "{} {}",
option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")),
env!("CARGO_PKG_VERSION"),
));
)?;

#summary
writeln!(w, "{}", #summary)?;

s.push_str(&format!("\nUsage:\n {} [OPTIONS] [ARGS]\n", bin_name));
writeln!(w, "\nUsage:\n {}", format!(#usage, bin_name))?;

#options

#after_options

s
writeln!(w, "{}", #after_options)?;
Ok(())
)
}

fn read_help_file(file: &str) -> (TokenStream, TokenStream) {
fn read_help_file(file: &str) -> (String, String, String) {
let path = Path::new(file);
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mut location = PathBuf::from(manifest_dir);
Expand All @@ -148,8 +133,9 @@ fn read_help_file(file: &str) -> (TokenStream, TokenStream) {
f.read_to_string(&mut contents).unwrap();

(
get_h2("summary", &contents),
get_after_event(pulldown_cmark::Event::Rule, &contents),
parse_about(&contents),
parse_usage(&contents),
parse_section("after help", &contents).unwrap_or_default(),
)
}

Expand Down
236 changes: 236 additions & 0 deletions derive/src/help_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

//! A collection of functions to parse the markdown code of help files.
//!
//! The structure of the markdown code is assumed to be:
//!
//! # util name
//!
//! ```text
//! usage info
//! ```
//!
//! About text
//!
//! ## Section 1
//!
//! Some content
//!
//! ## Section 2
//!
//! Some content
const MARKDOWN_CODE_FENCES: &str = "```";

/// Parses the text between the first markdown code block and the next header, if any,
/// into an about string.
pub fn parse_about(content: &str) -> String {
content
.lines()
.skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.skip(1)
.skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.skip(1)
.take_while(|l| !l.starts_with('#'))
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}

/// Parses the first 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.
pub fn parse_usage(content: &str) -> String {
content
.lines()
.skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.skip(1)
.take_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES))
.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}\n")
} else {
"{}\n".to_string()
}
})
.collect::<Vec<_>>()
.join("")
.trim()
.to_string()
}

/// Get a single section from content
///
/// The section must be a second level section (i.e. start with `##`).
pub fn parse_section(section: &str, content: &str) -> Option<String> {
fn is_section_header(line: &str, section: &str) -> bool {
line.strip_prefix("##")
.map_or(false, |l| l.trim().to_lowercase() == section)
}

let section = &section.to_lowercase();

// We cannot distinguish between an empty or non-existing section below,
// so we do a quick test to check whether the section exists
if content.lines().all(|l| !is_section_header(l, section)) {
return None;
}

// Prefix includes space to allow processing of section with level 3-6 headers
let section_header_prefix = "## ";

Some(
content
.lines()
.skip_while(|&l| !is_section_header(l, section))
.skip(1)
.take_while(|l| !l.starts_with(section_header_prefix))
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string(),
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_section() {
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_section("some section", input).unwrap(),
"This is some section"
);
assert_eq!(
parse_section("SOME SECTION", input).unwrap(),
"This is some section"
);
assert_eq!(
parse_section("another section", input).unwrap(),
"This is the other section\nwith multiple lines"
);
}

#[test]
fn test_parse_section_with_sub_headers() {
let input = "\
# ls\n\
## after section\n\
This is some section\n\
\n\
### level 3 header\n\
\n\
Additional text under the section.\n\
\n\
#### level 4 header\n\
\n\
Yet another paragraph\n";

assert_eq!(
parse_section("after section", input).unwrap(),
"This is some section\n\n\
### level 3 header\n\n\
Additional text under the section.\n\n\
#### level 4 header\n\n\
Yet another paragraph"
);
}

#[test]
fn test_parse_non_existing_section() {
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!(parse_section("non-existing section", input).is_none());
}

#[test]
fn test_parse_usage() {
let input = "\
# ls\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(input), "{} -l");
}

#[test]
fn test_parse_multi_line_usage() {
let input = "\
# ls\n\
```\n\
ls -a\n\
ls -b\n\
ls -c\n\
```\n\
## some section\n\
This is some section\n";

assert_eq!(parse_usage(input), "{} -a\n{} -b\n{} -c");
}

#[test]
fn test_parse_about() {
let input = "\
# ls\n\
```\n\
ls -l\n\
```\n\
\n\
This is the about section\n\
\n\
## some section\n\
This is some section\n";

assert_eq!(parse_about(input), "This is the about section");
}

#[test]
fn test_parse_multi_line_about() {
let input = "\
# ls\n\
```\n\
ls -l\n\
```\n\
\n\
about a\n\
\n\
about b\n\
\n\
## some section\n\
This is some section\n";

assert_eq!(parse_about(input), "about a\n\nabout b");
}
}
Loading

0 comments on commit a6d9e5b

Please sign in to comment.