start reading help file file

This commit is contained in:
Terts Diepraam
2022-12-18 16:28:44 +01:00
parent 5d4953ebca
commit fc89ea3284
8 changed files with 354 additions and 307 deletions
+5 -126
View File
@@ -2,22 +2,18 @@ use std::ops::RangeInclusive;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
punctuated::Punctuated, Attribute, Fields, FieldsUnnamed, Ident, Lit, LitStr, Meta, Token,
Variant,
};
use syn::{Attribute, Fields, FieldsUnnamed, Ident, Lit, Meta, Variant};
use crate::{
attributes::{parse_argument_attribute, ArgAttr},
flags::{Flags, Value},
markdown::str_to_renderer,
};
pub(crate) struct Argument {
ident: Ident,
name: String,
arg_type: ArgType,
help: String,
pub(crate) ident: Ident,
pub(crate) name: String,
pub(crate) arg_type: ArgType,
pub(crate) help: String,
}
pub(crate) enum ArgType {
@@ -31,40 +27,6 @@ pub(crate) enum ArgType {
},
}
pub(crate) fn parse_help_flags(attrs: &[Attribute]) -> Flags {
for attr in attrs {
if attr.path.is_ident("help") {
let mut flags = Flags::new();
for s in attr
.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)
.unwrap()
{
let s = s.value().to_string();
flags.add(&s);
}
return flags;
}
}
Flags::default_help()
}
pub(crate) fn parse_version_flags(attrs: &[Attribute]) -> Flags {
for attr in attrs {
if attr.path.is_ident("version") {
let mut flags = Flags::new();
for s in attr
.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)
.unwrap()
{
let s = s.value().to_string();
flags.add(&s);
}
return flags;
}
}
Flags::default_version()
}
pub(crate) fn parse_argument(v: Variant) -> Option<Argument> {
let ident = v.ident;
let name = ident.to_string();
@@ -353,20 +315,6 @@ fn argument_expression(arg: &Argument) -> TokenStream {
}
}
pub(crate) fn help_handling(help_flags: &Flags) -> TokenStream {
if help_flags.is_empty() {
return quote!();
}
let pat = help_flags.pat();
quote!(
if let #pat = arg {
return Ok(Some(Argument::Help));
}
)
}
pub(crate) fn version_handling(version_flags: &Flags) -> TokenStream {
if version_flags.is_empty() {
return quote!();
@@ -380,72 +328,3 @@ pub(crate) fn version_handling(version_flags: &Flags) -> TokenStream {
}
)
}
pub(crate) fn help_string(
args: &[Argument],
help_flags: &Flags,
version_flags: &Flags,
) -> TokenStream {
let mut options = Vec::new();
let width: usize = 16;
let indent: usize = 2;
for Argument { arg_type, help, .. } in args {
match arg_type {
ArgType::Option { flags, .. } => {
let flags = flags.format();
let renderer = str_to_renderer(help);
options.push(quote!((#flags, #renderer)));
}
ArgType::Positional { .. } => {}
}
}
if !help_flags.is_empty() {
let flags = help_flags.format();
let renderer = str_to_renderer("Display this help message");
options.push(quote!((#flags, #renderer)));
}
if !version_flags.is_empty() {
let flags = version_flags.format();
let renderer = str_to_renderer("Display version information");
options.push(quote!((#flags, #renderer)));
}
let options = quote!([#(#options),*]);
quote!(
let mut s = format!("{} [OPTIONS] [ARGS]\n\nOptions:\n", bin_name);
for (flags, renderer) 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);
if flags.len() <= #width {
let line = match help_lines.next() {
Some(line) => line,
None => return s,
};
let help_indent = " ".repeat(#width-flags.len());
s.push_str(&help_indent);
s.push_str(line);
s.push('\n');
} else {
s.push('\n');
}
let help_indent = " ".repeat(#width+#indent);
for line in help_lines {
s.push_str(&help_indent);
s.push_str(line);
s.push('\n');
}
}
s
)
}
+114 -110
View File
@@ -15,27 +15,58 @@ pub(crate) enum ArgAttr {
pub(crate) fn parse_argument_attribute(attr: &Attribute) -> ArgAttr {
if attr.path.is_ident("option") {
ArgAttr::Option(parse_option_attr(attr))
ArgAttr::Option(OptionAttr::parse(attr))
} else if attr.path.is_ident("positional") {
ArgAttr::Positional(parse_positional_attr(attr))
ArgAttr::Positional(PositionalAttr::parse(attr))
} else {
panic!("Internal error: invalid argument attribute");
}
}
enum AttributeArguments {
String(String),
Parser(Expr),
Default(Expr),
Value(Expr),
NumArgs(RangeInclusive<usize>),
File(String),
}
impl AttributeArguments {
fn parse_all(attr: &Attribute) -> Vec<Self> {
attr.parse_args_with(Punctuated::<AttributeArguments, Token![,]>::parse_terminated)
.map(|iter| iter.into_iter().collect::<Vec<_>>())
.unwrap_or_default()
}
}
#[derive(Default)]
pub(crate) struct OptionAttr {
pub(crate) flags: Flags,
// This should probably not accept any expr to give better errors.
// Closures should be allowed though.
pub(crate) parser: Option<Expr>,
pub(crate) default: Option<Expr>,
}
enum OptionAttrArg {
Arg(String),
Parser(Expr),
Default(Expr),
impl OptionAttr {
pub(crate) fn parse(attr: &Attribute) -> Self {
let mut option_attr = OptionAttr::default();
for arg in AttributeArguments::parse_all(attr) {
match arg {
AttributeArguments::String(a) => option_attr.flags.add(&a),
AttributeArguments::Parser(e) => option_attr.parser = Some(e),
AttributeArguments::Default(e) => option_attr.default = Some(e),
_ => panic!("Invalid argument"),
};
}
assert!(
!option_attr.flags.is_empty(),
"must give a flag in an option attribute"
);
option_attr
}
}
#[derive(Default)]
@@ -44,9 +75,20 @@ pub(crate) struct ValueAttr {
pub(crate) value: Option<Expr>,
}
enum ValueAttrArg {
Key(String),
Value(Expr),
impl ValueAttr {
pub(crate) fn parse(attr: &Attribute) -> Self {
let mut value_attr = Self::default();
for arg in AttributeArguments::parse_all(attr) {
match arg {
AttributeArguments::String(k) => value_attr.keys.push(k),
AttributeArguments::Value(e) => value_attr.value = Some(e),
_ => panic!(),
};
}
value_attr
}
}
pub(crate) struct PositionalAttr {
@@ -59,108 +101,67 @@ impl Default for PositionalAttr {
}
}
enum PositionalAttrArg {
NumArgs(RangeInclusive<usize>),
}
impl PositionalAttr {
pub(crate) fn parse(attr: &Attribute) -> Self {
let mut positional_attr = Self::default();
pub(crate) fn parse_option_attr(attr: &Attribute) -> OptionAttr {
let mut option_attr = OptionAttr::default();
let parsed_args = attr
.parse_args_with(Punctuated::<OptionAttrArg, Token![,]>::parse_terminated)
.unwrap_or_default();
for arg in parsed_args {
match arg {
OptionAttrArg::Arg(a) => option_attr.flags.add(&a),
OptionAttrArg::Parser(e) => option_attr.parser = Some(e),
OptionAttrArg::Default(e) => option_attr.default = Some(e),
};
}
assert!(
!option_attr.flags.is_empty(),
"must give a flag in an option attribute"
);
option_attr
}
impl Parse for OptionAttrArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(LitStr) {
return parse_flag(input).map(Self::Arg);
}
if input.peek(Ident) {
let name = input.parse::<Ident>()?.to_string();
input.parse::<Token![=]>()?;
match name.as_str() {
"parser" => return Ok(Self::Parser(input.parse::<Expr>()?)),
"default" => return Ok(Self::Default(input.parse::<Expr>()?)),
_ => panic!("Unrecognized argument {} for option attribute", name),
for arg in AttributeArguments::parse_all(attr) {
match arg {
AttributeArguments::NumArgs(k) => positional_attr.num_args = k,
_ => panic!(),
};
}
panic!("Arguments to option attribute must be string literals");
positional_attr
}
}
pub(crate) fn parse_value_attr(attr: Attribute) -> ValueAttr {
let mut value_attr = ValueAttr::default();
let Ok(parsed_args) = attr
.parse_args_with(Punctuated::<ValueAttrArg, Token![,]>::parse_terminated)
else {
return value_attr;
};
for arg in parsed_args {
match arg {
ValueAttrArg::Key(k) => value_attr.keys.push(k),
ValueAttrArg::Value(e) => value_attr.value = Some(e),
};
}
value_attr
#[derive(Default)]
pub(crate) struct HelpAttr {
pub(crate) flags: Flags,
pub(crate) file: Option<String>,
}
impl Parse for ValueAttrArg {
impl HelpAttr {
pub(crate) fn parse(attr: &Attribute) -> Self {
let mut help = Self::default();
for arg in AttributeArguments::parse_all(attr) {
match arg {
AttributeArguments::String(s) => help.flags.add(&s),
AttributeArguments::File(filename) => help.file = Some(filename),
_ => panic!(),
}
}
help
}
}
#[derive(Default)]
pub(crate) struct VersionAttr {
pub(crate) flags: Flags,
}
impl VersionAttr {
pub(crate) fn parse(attr: &Attribute) -> Self {
let mut help = Self::default();
for arg in AttributeArguments::parse_all(attr) {
match arg {
AttributeArguments::String(s) => help.flags.add(&s),
_ => panic!(),
}
}
help
}
}
impl Parse for AttributeArguments {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(LitStr) {
return Ok(Self::Key(input.parse::<LitStr>()?.value()));
return Ok(Self::String(input.parse::<LitStr>().unwrap().value()));
}
if input.peek(Ident) {
let name = input.parse::<Ident>()?.to_string();
input.parse::<Token![=]>()?;
match name.as_str() {
"value" => return Ok(Self::Value(input.parse::<Expr>()?)),
_ => panic!("Unrecognized argument {} for option attribute", name),
};
}
panic!("Arguments to option attribute must be string literals");
}
}
fn parse_flag(input: ParseStream) -> syn::Result<String> {
Ok(input.parse::<LitStr>().unwrap().value())
}
pub(crate) fn parse_positional_attr(attr: &Attribute) -> PositionalAttr {
let mut positional_attr = PositionalAttr::default();
let Ok(parsed_args) = attr
.parse_args_with(Punctuated::<PositionalAttrArg, Token![,]>::parse_terminated)
else {
return positional_attr;
};
for arg in parsed_args {
match arg {
PositionalAttrArg::NumArgs(k) => positional_attr.num_args = k,
};
}
positional_attr
}
impl Parse for PositionalAttrArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
if (input.peek(LitInt) && input.peek2(Token![..])) || input.peek(Token![..]) {
// We're dealing with a range
let range = input.parse::<ExprRange>()?;
@@ -204,14 +205,17 @@ impl Parse for PositionalAttrArg {
return Ok(Self::NumArgs(n..=n));
}
// if input.peek(Ident) {
// let name = input.parse::<Ident>()?.to_string();
// input.parse::<Token![=]>()?;
// match name.as_str() {
// "value" => return Ok(Self::Value(input.parse::<Expr>()?)),
// _ => panic!("Unrecognized argument {} for option attribute", name),
// };
// }
panic!("unexpected argument to positional");
if input.peek(Ident) {
let name = input.parse::<Ident>()?.to_string();
input.parse::<Token![=]>()?;
match name.as_str() {
"parser" => return Ok(Self::Parser(input.parse::<Expr>()?)),
"default" => return Ok(Self::Default(input.parse::<Expr>()?)),
"value" => return Ok(Self::Value(input.parse::<Expr>()?)),
"file" => return Ok(Self::File(input.parse::<LitStr>()?.value())),
_ => panic!("Unrecognized argument {} for option attribute", name),
};
}
panic!("Arguments to option attribute must be string literals");
}
}
-27
View File
@@ -21,13 +21,6 @@ pub(crate) struct Flag<T> {
}
impl Flags {
pub(crate) fn new() -> Self {
Self {
short: Vec::new(),
long: Vec::new(),
}
}
pub(crate) fn add(&mut self, flag: &str) {
assert!(flag.starts_with('-'), "Flags must start with a '-'");
if let Some(s) = flag.strip_prefix("--") {
@@ -117,26 +110,6 @@ impl Flags {
}
}
pub(crate) fn default_help() -> Self {
Self {
short: Vec::new(),
long: vec![Flag {
flag: "help".into(),
value: Value::No,
}],
}
}
pub(crate) fn default_version() -> Self {
Self {
short: Vec::new(),
long: vec![Flag {
flag: "version".into(),
value: Value::No,
}],
}
}
pub(crate) fn format(&self) -> String {
let short = self
.short
+153
View File
@@ -0,0 +1,153 @@
use std::{
io::Read,
path::{Path, PathBuf},
};
use crate::{
argument::{ArgType, Argument},
attributes::{HelpAttr, VersionAttr},
flags::Flags,
markdown::{get_h2, str_to_renderer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::Attribute;
pub(crate) fn parse_help_attr(attrs: &[Attribute]) -> HelpAttr {
for attr in attrs {
if attr.path.is_ident("help") {
return HelpAttr::parse(attr);
}
}
HelpAttr::default()
}
pub(crate) fn parse_version_attr(attrs: &[Attribute]) -> VersionAttr {
for attr in attrs {
if attr.path.is_ident("version") {
return VersionAttr::parse(attr);
}
}
VersionAttr::default()
}
pub(crate) fn help_handling(help_flags: &Flags) -> TokenStream {
if help_flags.is_empty() {
return quote!();
}
let pat = help_flags.pat();
quote!(
if let #pat = arg {
return Ok(Some(Argument::Help));
}
)
}
pub(crate) fn help_string(
args: &[Argument],
help_attr: &HelpAttr,
version_flags: &Flags,
) -> TokenStream {
let mut options = Vec::new();
let width: usize = 16;
let indent: usize = 2;
for Argument { arg_type, help, .. } in args {
match arg_type {
ArgType::Option { flags, .. } => {
let flags = flags.format();
let renderer = str_to_renderer(help);
options.push(quote!((#flags, #renderer)));
}
ArgType::Positional { .. } => {}
}
}
let summary = if let Some(file) = &help_attr.file {
let renderer = read_help_file(file);
quote!(s.push_str(&#renderer.render());)
} else {
quote!()
};
if !help_attr.flags.is_empty() {
let flags = help_attr.flags.format();
let renderer = str_to_renderer("Display this help message");
options.push(quote!((#flags, #renderer)));
}
if !version_flags.is_empty() {
let flags = version_flags.format();
let renderer = str_to_renderer("Display version information");
options.push(quote!((#flags, #renderer)));
}
let options = if !options.is_empty() {
let options = quote!([#(#options),*]);
quote!(
s.push_str("\nOptions:\n");
for (flags, renderer) 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);
if flags.len() <= #width {
let line = match help_lines.next() {
Some(line) => line,
None => return s,
};
let help_indent = " ".repeat(#width-flags.len()+2);
s.push_str(&help_indent);
s.push_str(line);
s.push('\n');
} else {
s.push('\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');
}
}
)
} else {
quote!()
};
quote!(
let mut s = String::new();
s.push_str(&format!("{} {}\n",
option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")),
env!("CARGO_PKG_VERSION"),
));
#summary
s.push_str(&format!("\nUsage:\n {} [OPTIONS] [ARGS]\n", bin_name));
#options
s
)
}
fn read_help_file(file: &str) -> TokenStream {
let path = Path::new(file);
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let mut location = PathBuf::from(manifest_dir);
location.push(path);
let mut contents = String::new();
let mut f = std::fs::File::open(location).unwrap();
f.read_to_string(&mut contents).unwrap();
get_h2("summary", &contents)
}
+12 -11
View File
@@ -2,14 +2,15 @@ mod action;
mod argument;
mod attributes;
mod flags;
mod help;
mod markdown;
use action::{parse_action_attr, ActionAttr, ActionType};
use argument::{
help_handling, help_string, long_handling, parse_argument, parse_help_flags,
parse_version_flags, positional_handling, short_handling, version_handling,
long_handling, parse_argument, positional_handling, short_handling, version_handling,
};
use attributes::{parse_value_attr, ValueAttr};
use attributes::ValueAttr;
use help::{help_handling, help_string, parse_help_attr, parse_version_attr};
use proc_macro::TokenStream;
use quote::quote;
@@ -119,7 +120,7 @@ pub fn options(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
#[proc_macro_derive(Arguments, attributes(flag, option, positional, help))]
#[proc_macro_derive(Arguments, attributes(flag, option, positional, help, version))]
pub fn arguments(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
@@ -130,16 +131,16 @@ pub fn arguments(input: TokenStream) -> TokenStream {
panic!("Input should be an enum!");
};
let help_flags = parse_help_flags(&input.attrs);
let version_flags = parse_version_flags(&input.attrs);
let help_attr = parse_help_attr(&input.attrs);
let version_attr = parse_version_attr(&input.attrs);
let arguments: Vec<_> = data.variants.into_iter().flat_map(parse_argument).collect();
let short = short_handling(&arguments);
let long = long_handling(&arguments, &help_flags);
let long = long_handling(&arguments, &help_attr.flags);
let (positional, missing_argument_checks) = positional_handling(&arguments);
let help_string = help_string(&arguments, &help_flags, &version_flags);
let help = help_handling(&help_flags);
let version = version_handling(&version_flags);
let help_string = help_string(&arguments, &help_attr, &version_attr.flags);
let help = help_handling(&help_attr.flags);
let version = version_handling(&version_attr.flags);
let version_string = quote!(format!(
"{} {}",
option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")),
@@ -205,7 +206,7 @@ pub fn from_value(input: TokenStream) -> TokenStream {
continue;
}
let ValueAttr { keys, value } = parse_value_attr(attr);
let ValueAttr { keys, value } = ValueAttr::parse(&attr);
let keys = if keys.is_empty() {
vec![variant_name.to_lowercase()]
+63 -32
View File
@@ -6,42 +6,73 @@ fn prefix(t: TokenStream) -> TokenStream {
quote!(uutils_args::term_md::#t)
}
fn md_to_quote(event: Event) -> TokenStream {
let tokens = match event {
Event::Start(tag) => {
let tag = quote_tag(tag);
quote!(Event::Start(#tag))
}
Event::End(tag) => {
let tag = quote_tag(tag);
quote!(Event::End(#tag))
}
Event::Text(t) => {
let t = t.to_string();
let text = quote!(String::from(#t));
quote!(Event::Text(#text))
}
Event::Code(t) => {
let t = t.to_string();
let text = quote!(String::from(#t));
quote!(Event::Code(#text))
}
Event::SoftBreak => quote!(Event::SoftBreak),
Event::HardBreak => quote!(Event::HardBreak),
Event::Rule => quote!(Event::Rule),
// Below are unsupported in term_md
Event::Html(_) => todo!(),
Event::FootnoteReference(_) => todo!(),
Event::TaskListMarker(_) => todo!(),
};
prefix(tokens)
}
pub(crate) fn str_to_renderer(s: &str) -> TokenStream {
let events = Parser::new(s);
let parsed_events = events.map(|e| {
let event = match e {
Event::Start(tag) => {
let tag = quote_tag(tag);
quote!(Event::Start(#tag))
}
Event::End(tag) => {
let tag = quote_tag(tag);
quote!(Event::End(#tag))
}
Event::Text(t) => {
let t = t.to_string();
let text = quote!(String::from(#t));
quote!(Event::Text(#text))
}
Event::Code(t) => {
let t = t.to_string();
let text = quote!(String::from(#t));
quote!(Event::Code(#text))
}
Event::SoftBreak => quote!(Event::SoftBreak),
Event::HardBreak => quote!(Event::HardBreak),
Event::Rule => quote!(Event::Rule),
// Below are unsupported in term_md
Event::Html(_) => todo!(),
Event::FootnoteReference(_) => todo!(),
Event::TaskListMarker(_) => todo!(),
};
prefix(event)
});
let parsed_events = events.map(md_to_quote);
prefix(quote!(Renderer::new(
40,
60,
vec![#(#parsed_events),*].into_iter()
)))
}
pub(crate) fn get_h2(heading_name: &str, s: &str) -> TokenStream {
let mut events = Parser::new(s);
let mut selected_events = Vec::new();
while let Some(event) = events.next() {
if let Event::Start(Tag::Heading(HeadingLevel::H2, _, _)) = event {
if let Some(Event::Text(s)) = events.next() {
if s.to_lowercase() == heading_name.to_lowercase() {
selected_events.extend(
(&mut events)
.skip_while(|e| {
!matches!(e, Event::End(Tag::Heading(HeadingLevel::H2, _, _)))
})
.skip(1)
.take_while(|e| {
!matches!(e, Event::Start(Tag::Heading(HeadingLevel::H2, _, _)))
}),
)
}
}
}
}
let parsed_events = selected_events.into_iter().map(md_to_quote);
prefix(quote!(Renderer::new(
80,
vec![#(#parsed_events),*].into_iter()
)))
}
+2 -1
View File
@@ -1,7 +1,8 @@
use uutils_args::{Arguments, Options};
#[derive(Clone, Arguments)]
#[help("--help")]
#[help("--help", file = "examples/hello_world_help.md")]
#[version("--version")]
enum Arg {
/// The *name* to **greet**
///
+5
View File
@@ -0,0 +1,5 @@
# Helloworld
## Summary
Hello this is the summary.