From fc89ea328419235f4cb6a1126be79f75a90ca6f7 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 18 Dec 2022 16:28:44 +0100 Subject: [PATCH] start reading help file file --- derive/src/argument.rs | 131 +------------------- derive/src/attributes.rs | 224 ++++++++++++++++++----------------- derive/src/flags.rs | 27 ----- derive/src/help.rs | 153 ++++++++++++++++++++++++ derive/src/lib.rs | 23 ++-- derive/src/markdown.rs | 95 ++++++++++----- examples/hello_world.rs | 3 +- examples/hello_world_help.md | 5 + 8 files changed, 354 insertions(+), 307 deletions(-) create mode 100644 derive/src/help.rs create mode 100644 examples/hello_world_help.md diff --git a/derive/src/argument.rs b/derive/src/argument.rs index d7c898f..d78c49f 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -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::::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::::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 { 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 - ) -} diff --git a/derive/src/attributes.rs b/derive/src/attributes.rs index f525074..3ff1aaa 100644 --- a/derive/src/attributes.rs +++ b/derive/src/attributes.rs @@ -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), + File(String), +} + +impl AttributeArguments { + fn parse_all(attr: &Attribute) -> Vec { + attr.parse_args_with(Punctuated::::parse_terminated) + .map(|iter| iter.into_iter().collect::>()) + .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, pub(crate) default: Option, } -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, } -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), -} +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::::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 { - if input.peek(LitStr) { - return parse_flag(input).map(Self::Arg); - } - - if input.peek(Ident) { - let name = input.parse::()?.to_string(); - input.parse::()?; - match name.as_str() { - "parser" => return Ok(Self::Parser(input.parse::()?)), - "default" => return Ok(Self::Default(input.parse::()?)), - _ => 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::::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, } -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 { if input.peek(LitStr) { - return Ok(Self::Key(input.parse::()?.value())); + return Ok(Self::String(input.parse::().unwrap().value())); } - if input.peek(Ident) { - let name = input.parse::()?.to_string(); - input.parse::()?; - match name.as_str() { - "value" => return Ok(Self::Value(input.parse::()?)), - _ => panic!("Unrecognized argument {} for option attribute", name), - }; - } - panic!("Arguments to option attribute must be string literals"); - } -} - -fn parse_flag(input: ParseStream) -> syn::Result { - Ok(input.parse::().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::::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 { if (input.peek(LitInt) && input.peek2(Token![..])) || input.peek(Token![..]) { // We're dealing with a range let range = input.parse::()?; @@ -204,14 +205,17 @@ impl Parse for PositionalAttrArg { return Ok(Self::NumArgs(n..=n)); } - // if input.peek(Ident) { - // let name = input.parse::()?.to_string(); - // input.parse::()?; - // match name.as_str() { - // "value" => return Ok(Self::Value(input.parse::()?)), - // _ => panic!("Unrecognized argument {} for option attribute", name), - // }; - // } - panic!("unexpected argument to positional"); + if input.peek(Ident) { + let name = input.parse::()?.to_string(); + input.parse::()?; + match name.as_str() { + "parser" => return Ok(Self::Parser(input.parse::()?)), + "default" => return Ok(Self::Default(input.parse::()?)), + "value" => return Ok(Self::Value(input.parse::()?)), + "file" => return Ok(Self::File(input.parse::()?.value())), + _ => panic!("Unrecognized argument {} for option attribute", name), + }; + } + panic!("Arguments to option attribute must be string literals"); } } diff --git a/derive/src/flags.rs b/derive/src/flags.rs index 6abc578..9b9d190 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -21,13 +21,6 @@ pub(crate) struct Flag { } 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 diff --git a/derive/src/help.rs b/derive/src/help.rs new file mode 100644 index 0000000..4054bdf --- /dev/null +++ b/derive/src/help.rs @@ -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) +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 74194ac..59769b6 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -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()] diff --git a/derive/src/markdown.rs b/derive/src/markdown.rs index 330bf06..edcda7e 100644 --- a/derive/src/markdown.rs +++ b/derive/src/markdown.rs @@ -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() ))) } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index d72dc70..3a90721 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -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** /// diff --git a/examples/hello_world_help.md b/examples/hello_world_help.md new file mode 100644 index 0000000..81e70d1 --- /dev/null +++ b/examples/hello_world_help.md @@ -0,0 +1,5 @@ +# Helloworld + +## Summary + +Hello this is the summary.