From 765448a0a863ade10fddec6ab913ef33fa977e5b Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 11 Jun 2023 22:48:18 +0900 Subject: [PATCH 01/58] Initial commit --- .gitignore | 3 + Cargo.toml | 29 ++ license.txt | 15 + readme.md | 53 ++++ src/lib.rs | 426 +++++++++++++++++++++++++++ src/proc_macros/Cargo.toml | 21 ++ src/proc_macros/mod.rs | 575 +++++++++++++++++++++++++++++++++++++ tests/test.rs | 110 +++++++ 8 files changed, 1232 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 license.txt create mode 100644 readme.md create mode 100644 src/lib.rs create mode 100644 src/proc_macros/Cargo.toml create mode 100644 src/proc_macros/mod.rs create mode 100644 tests/test.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cae3cbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/.vscode diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c3cb359 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "aargvark" +version = "0.0.1" +edition = "2021" +license = "ISC" +description = "Self-similar argument parsing" +homepage = "https://github.com/andrewbaxter/aargvark" +repository = "https://github.com/andrewbaxter/aargvark" +readme = "readme.md" + +[workspace] +members = ["src/proc_macros"] + +[features] +default = [] +serde_json = ["dep:serde_json"] +serde_yaml = ["dep:serde_yaml"] +chrono = ["dep:chrono"] + +[dependencies] +anyhow = "1.0.66" +once_cell = "1.16.0" +regex = { version = "1.7.0", default-features = false, features = ["std"] } +aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.1" } +chrono = { version = "0.4.26", optional = true } +serde_json = { version = "1.0.96", optional = true } +serde_yaml = { version = "0.9.21", optional = true } +convert_case = "0.6.0" +comfy-table = "7.0.0" diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..af6b977 --- /dev/null +++ b/license.txt @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2023 Andrew Baxter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0b3dcdc --- /dev/null +++ b/readme.md @@ -0,0 +1,53 @@ +Self-similar derive-based argument parsing, similar to Clap-derive. + +This attempts to implement a simple, self-similar structure for parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. + +# Why or why not + +Why this and not Clap? + +- This parse more complex data types, like vectors of sub-structures, or enums +- It's more consistent +- It has a super-simple interface (just `#[derive(Aargvark)]`) + +Why not this? + +- Some command line parsing conventions were discarded in order to simplify and maintain self-similarity. A lot of command line conventions are inconsistent or break down as you nest things. +- There's less customizability. Some things (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. Other things just haven't been implemented yet due to lack of time. +- Alpha + +# Conventions and usage + +To add it to your project, run + +```sh +cargo add aargvark +``` + +To parse command line arguments + +1. Define the data type you want to parse them into, like + + ```rust + #[derive(Aargvark)] + struct MyArgs { + velociraptor: String, + deadly: bool, + color_pattern: Option, + } + ``` + +2. Vark it + ``` + let args = MyArgs::vark(); + ``` + +You can derive structs, enums, and tuples, as well as `Vec`, `HashSet`, most `Ip` and `SocketAddr` types, and `PathBuf`. + +There are also custom structs for reading files: + +- `VarkFile` +- `VarkJson` +- `VarkYaml` + +For JSON and Yaml you must enable the respective features. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..77d44e9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,426 @@ +use std::{ + env::args, + process::exit, + net::{ + SocketAddr, + SocketAddrV4, + SocketAddrV6, + IpAddr, + Ipv4Addr, + Ipv6Addr, + }, + path::PathBuf, + io::{ + stdin, + Read, + }, + fs, + collections::HashSet, + hash::Hash, + ffi::{ + OsString, + }, +}; +pub use aargvark_proc_macros; +pub use anyhow::{ + Context, + Error, + Result, +}; +use comfy_table::Cell; +pub use once_cell::sync::Lazy; + +struct VarkErr { + i: usize, + breadcrumbs: Vec, + err: String, +} + +#[doc(hidden)] +pub enum R { + EOF, + Err, + Ok(T), + Help, +} + +#[doc(hidden)] +pub enum PeekR<'a> { + Ok(&'a str), + None, + Help, +} + +#[doc(hidden)] +pub struct VarkState { + args: Vec, + i: usize, + pub breadcrumbs: Vec, + errors: Vec, + pub simple_enum_root: bool, +} + +#[doc(hidden)] +pub fn join_strs(sep: &str, v: &[&str]) -> String { + v.join(sep) +} + +#[doc(hidden)] +pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet) { + let mut text = "Usage:".to_string(); + for s in &state.breadcrumbs { + text.push_str(" "); + text.push_str(&s); + } + return (text, HashSet::new()); +} + +#[doc(hidden)] +pub fn generate_help_section_suffix( + docstr: &str, + placeholders: Vec<&str>, + placeholders_detail: Vec<(String, &str)>, + joiner: &str, +) -> String { + let mut out = String::new(); + for p in placeholders { + out.push_str(joiner); + out.push_str(p); + } + out.push_str("\n\n"); + if !docstr.is_empty() { + out.push_str(docstr); + out.push_str("\n\n"); + } + let mut table = comfy_table::Table::new(); + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + for (placeholder, docstr) in placeholders_detail { + table.add_row(vec![comfy_table::Cell::new(placeholder), Cell::new(docstr)]); + } + table.set_constraints(vec![comfy_table::ColumnConstraint::UpperBoundary(comfy_table::Width::Percentage(60))]); + out.push_str(&table.to_string()); + out.push_str("\n"); + out +} + +impl VarkState { + pub fn peek<'a>(&'a self) -> PeekR<'a> { + if self.i >= self.args.len() { + return PeekR::None; + } + let v = &self.args[self.i]; + if v == "-h" || v == "--help" { + return PeekR::Help; + } + return PeekR::Ok(v); + } + + pub fn position(&self) -> usize { + return self.i; + } + + pub fn rewind(&mut self, i: usize) { + self.i = i; + } + + pub fn consume(&mut self) { + self.i += 1; + } + + pub fn r_ok(&self, v: T) -> R { + return R::Ok(v); + } + + pub fn r_err(&mut self, text: String) -> R { + self.errors.push(VarkErr { + i: self.i, + breadcrumbs: self.breadcrumbs.clone(), + err: text, + }); + return R::Err; + } +} + +/// Parse the explicitly passed in arguments. The `command` is only used in help +/// text. +pub fn vark_explicit(command: String, args: Vec) -> T { + let mut state = VarkState { + args: args, + i: 0, + breadcrumbs: vec![command], + errors: vec![], + simple_enum_root: true, + }; + match T::vark(&mut state) { + R::EOF => { + eprintln!("You must specify command line arguments, use --help for more info."); + exit(1); + }, + R::Err => { + let display_args: Vec = state.args.iter().map(|a| format!("{:?}", a)).collect(); + let mut display_arg_offsets = vec![]; + { + let mut offset = 0; + for d in &display_args { + display_arg_offsets.push(offset); + offset += d.chars().count() + 1; + } + } + let display_args = display_args.join(" "); + let mut text = "Error parsing command line arguments.\n".to_string(); + state.errors.reverse(); + for e in state.errors { + text.push_str("\n"); + text.push_str(&format!(" * {}\n", e.err)); + text.push_str(&format!("while parsing {:?} at\n", e.breadcrumbs)); + text.push_str(&display_args); + text.push_str("\n"); + text.push_str(&" ".repeat(*display_arg_offsets.get(e.i).unwrap())); + text.push_str("^\n"); + } + eprintln!("{}", text); + exit(1); + }, + R::Ok(v) => { + if state.i != state.args.len() { + eprintln!( + "Error parsing command line arguments: final arguments are unrecognized\n{:?}", + &state.args[state.i..] + ); + exit(1); + } + return v; + }, + R::Help => { + let (mut text, mut seen_sections) = generate_help_section_usage_prefix(&state); + T::generate_help_section_suffix(&mut text, &mut seen_sections); + eprintln!("{}", text); + exit(0); + }, + } +} + +/// Parse the command line arguments into the specified type. +pub fn vark() -> T { + let mut args = args(); + let command = args.next().unwrap_or("unknown!".to_string()); + return vark_explicit(command, args.collect::>()); +} + +/// Anything that implements this trait can be parsed and used as a field in other +/// parsable enums/structs. +pub trait AargvarkTrait: Sized { + fn vark(state: &mut VarkState) -> R; + fn generate_help_placeholder() -> String; + fn generate_help_section(text: &mut String, seen_sections: &mut HashSet); + fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet); +} + +/// A helper enum, providing a simpler interface for types that can be parsed from +/// a single primitive string. +pub trait AargvarkFromStr: Sized { + fn from_str(s: &str) -> Result; + fn generate_help_placeholder() -> String; +} + +impl AargvarkTrait for T { + fn vark(state: &mut VarkState) -> R { + let s = match state.peek() { + PeekR::None => return R::EOF, + PeekR::Help => return R::Help, + PeekR::Ok(s) => s, + }; + match T::from_str(s) { + Ok(v) => { + state.consume(); + return state.r_ok(v); + }, + Err(e) => return state.r_err(e), + } + } + + fn generate_help_placeholder() -> String { + T::generate_help_placeholder() + } + + fn generate_help_section(_text: &mut String, _seen_sections: &mut HashSet) { } + + fn generate_help_section_suffix(_text: &mut String, _seen_sections: &mut HashSet) { } +} + +macro_rules! auto_from_str{ + ($t: ty) => { + impl AargvarkFromStr for $t { + fn from_str(s: &str) -> Result { + ::from_str(s).map_err(|e| e.to_string()) + } + + fn generate_help_placeholder() -> String { + format!( + "<{}>", + convert_case::Casing::to_case(&std::any::type_name::(), convert_case::Case::UpperKebab) + ) + } + } + }; +} + +auto_from_str!(String); + +auto_from_str!(OsString); + +auto_from_str!(SocketAddr); + +auto_from_str!(SocketAddrV4); + +auto_from_str!(SocketAddrV6); + +auto_from_str!(IpAddr); + +auto_from_str!(Ipv4Addr); + +auto_from_str!(Ipv6Addr); + +auto_from_str!(PathBuf); + +impl AargvarkTrait for bool { + fn vark(state: &mut VarkState) -> R { + return state.r_ok(true); + } + + fn generate_help_placeholder() -> String { + return "".to_string(); + } + + fn generate_help_section(_text: &mut String, _seen_sections: &mut HashSet) { } + + fn generate_help_section_suffix(_text: &mut String, _seen_sections: &mut HashSet) { } +} + +/// This parses a path (or - for stdin) passed on the command line into bytes. +pub struct AargvarkFile(Vec); + +impl AargvarkFromStr for AargvarkFile { + fn from_str(s: &str) -> Result { + if s == "-" { + let mut out = vec![]; + match stdin().read_to_end(&mut out) { + Ok(_) => return Ok(Self(out)), + Err(e) => return Err(format!("Error reading stdin: {}", e)), + }; + } else { + match fs::read(s) { + Ok(v) => return Ok(Self(v)), + Err(e) => return Err(format!("Error reading {}: {}", s, e)), + }; + } + } + + fn generate_help_placeholder() -> String { + format!("") + } +} + +/// This parses a path (or - for stdin) passed on the command line as json into the +/// specified type. +#[cfg(serde_json)] +pub struct AargvarkJson(T); + +#[cfg(serde_json)] +impl AargvarkFromStr for AargvarkJson { + fn from_str(s: &str) -> Result { + let b = AargvarkFile::from_str(s)?; + match serde_json::from_str(b) { + Ok(v) => return Ok(Self(v)), + Err(e) => return Err(e.to_string()), + }; + } + + fn generate_help_placeholder() -> String { + format!("") + } +} + +/// This parses a path (or - for stdin) passed on the command line as yaml into the +/// specified type. +#[cfg(serde_yaml)] +pub struct AargvarkYaml(T); + +#[cfg(serde_yaml)] +impl AargvarkFromStr for AargvarkYaml { + fn from_str(s: &str) -> Result { + let b = AargvarkFile::from_str(s)?; + match serde_yaml::from_str(b) { + Ok(v) => return Ok(Self(v)), + Err(e) => return Err(e.to_string()), + }; + } + + fn generate_help_placeholder() -> String { + format!("") + } +} + +#[doc(hidden)] +pub fn vark_from_iter>(state: &mut VarkState) -> R { + state.simple_enum_root = false; + let mut out = vec![]; + let mut rewind_to = state.position(); + let mut i = 0usize; + loop { + i += 1; + state.breadcrumbs.push(format!("[{}]", i)); + let r = T::vark(state); + state.breadcrumbs.pop(); + match r { + R::Help => { + return R::Help; + }, + R::Ok(v) => { + out.push(v); + rewind_to = state.position(); + }, + R::Err | R::EOF => { + state.rewind(rewind_to); + return state.r_ok(C::from_iter(out.into_iter())); + }, + }; + } +} + +impl AargvarkTrait for Vec { + fn vark(state: &mut VarkState) -> R { + return vark_from_iter(state); + } + + fn generate_help_placeholder() -> String { + format!("{}[ ...]", T::generate_help_placeholder()) + } + + fn generate_help_section(text: &mut String, seen_sections: &mut HashSet) { + Self::generate_help_section_suffix(text, seen_sections); + } + + fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet) { + T::generate_help_section(text, seen_sections); + } +} + +impl AargvarkTrait for HashSet { + fn vark(state: &mut VarkState) -> R { + return vark_from_iter(state); + } + + fn generate_help_placeholder() -> String { + format!("{}[ ...]", T::generate_help_placeholder()) + } + + fn generate_help_section(text: &mut String, seen_sections: &mut HashSet) { + Self::generate_help_section_suffix(text, seen_sections); + } + + fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet) { + T::generate_help_section(text, seen_sections); + } +} diff --git a/src/proc_macros/Cargo.toml b/src/proc_macros/Cargo.toml new file mode 100644 index 0000000..cd944db --- /dev/null +++ b/src/proc_macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "aargvark_proc_macros" +version = "0.0.1" +edition = "2021" +license = "ISC" +description = "Helper crate for aargvark" +homepage = "https://github.com/andrewbaxter/aargvark" +repository = "https://github.com/andrewbaxter/aargvark" +readme = "../../readme.md" + +[lib] +proc-macro = true +path = "mod.rs" + +[dependencies] +convert_case = "0.6.0" +litrs = "0.2.3" +proc-macro2 = "1.0.47" +quote = "1.0.21" +regex-syntax = "0.6.28" +syn = "1.0.103" diff --git a/src/proc_macros/mod.rs b/src/proc_macros/mod.rs new file mode 100644 index 0000000..317949d --- /dev/null +++ b/src/proc_macros/mod.rs @@ -0,0 +1,575 @@ +use std::collections::HashSet; +use convert_case::{ + Case, + Casing, +}; +use proc_macro2::TokenStream; +use quote::{ + format_ident, + quote, + ToTokens, +}; +use syn::{ + self, + parse_macro_input, + Type, + Fields, + Field, + DeriveInput, + Attribute, +}; + +fn get_docstr(attrs: &Vec) -> String { + for attr in attrs { + if attr.path.to_token_stream().to_string() != "doc" { + continue; + } + let mut tokens = attr.tokens.clone().into_iter(); + if !tokens.next().map(|t| t.to_string().as_str() == "=").unwrap_or(false) { + continue; + } + let s = match tokens.next() { + Some(s) => s.to_string(), + None => continue, + }; + return s; + } + return "".to_string(); +} + +struct GenRec { + vark: TokenStream, + help_child_placeholders: Vec, + help_child_placeholders_detail: Vec<(TokenStream, String)>, + help_recurse: Vec, +} + +fn gen_impl_type(ty: &Type, path: &str) -> GenRec { + match ty { + Type::Path(t) => { + return GenRec { + vark: quote!{ + #t:: vark(state) + }, + help_child_placeholders: vec![quote!{ + #t:: generate_help_placeholder() + }], + help_child_placeholders_detail: vec![], + help_recurse: vec![t.clone()], + }; + }, + Type::Tuple(t) => { + return gen_impl_unnamed( + path, + quote!(), + t.elems.iter().map(|e| (String::new(), e)).collect::>().as_slice(), + ); + }, + _ => panic!("Unsupported type {} in {}", ty.to_token_stream(), path), + } +} + +fn gen_impl_unnamed(path: &str, ident: TokenStream, d: &[(String, &Type)]) -> GenRec { + let mut parse_positional = vec![]; + let mut copy_fields = vec![]; + let mut help_child_placeholders = vec![]; + let mut help_child_placeholder_detail = vec![]; + let mut help_recurse = vec![]; + for (i, ty) in d.iter().enumerate() { + let eof_code = if i == 0 { + quote!{ + break R::EOF; + } + } else { + quote!{ + break state.r_err(format!("Missing argument {}", #i)); + } + }; + let f_ident = format_ident!("v{}", i); + let gen = gen_impl_type(ty.1, path); + help_child_placeholders.extend(gen.help_child_placeholders); + help_child_placeholder_detail.extend(gen.help_child_placeholders_detail); + help_recurse.extend(gen.help_recurse); + let vark = gen.vark; + parse_positional.push(quote!{ + state.breadcrumbs.push(format!("{{{}}}", #i)); + let r = #vark; + state.breadcrumbs.pop(); + let #f_ident = match r { + R:: Help => { + break R::Help; + }, + R:: Ok(v) => { + v + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + #eof_code + } + }; + }); + copy_fields.push(f_ident.to_token_stream()); + } + return GenRec { + vark: quote!{ + loop { + #(#parse_positional) * break state.r_ok(#ident(#(#copy_fields), *)); + } + }, + help_child_placeholders: help_child_placeholders, + help_child_placeholders_detail: help_child_placeholder_detail, + help_recurse: help_recurse, + }; +} + +fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { + match d { + Fields::Named(d) => { + let mut help_placeholders = vec![]; + let mut help_placeholders_detail = vec![]; + let mut help_recurse = vec![]; + let mut vark_optional_fields = vec![]; + let mut vark_parse_optional_cases = vec![]; + let mut vark_parse_positional = vec![]; + let mut vark_copy_fields = vec![]; + let mut required_i = 0usize; + for (i, f) in d.named.iter().enumerate() { + let field_ident = f.ident.as_ref().expect("Named field missing name"); + let f_local_ident = format_ident!("v{}", i); + let help_docstr = get_docstr(&f.attrs); + + // If an optional field, generate opt parsers and skip positional parsing + if |f: &Field| -> bool { + // Confirm optional + let ty = match &f.ty { + Type::Path(t) => { + if t.qself.is_some() { + return false; + } + if t.path.leading_colon.is_some() { + return false; + } + if t.path.segments.len() != 1 { + return false; + } + let s = t.path.segments.first().unwrap(); + if &s.ident.to_string() != "Option" { + return false; + } + match &s.arguments { + syn::PathArguments::None => return false, + syn::PathArguments::AngleBracketed(a) => { + if a.args.len() != 1 { + return false; + } + match a.args.first().unwrap() { + syn::GenericArgument::Type(t) => t, + _ => return false, + } + }, + syn::PathArguments::Parenthesized(_) => return false, + } + }, + _ => return false, + }; + + // Do generation + let flag = format!("--{}", field_ident.to_string().to_case(Case::Kebab)); + let help_docstr = get_docstr(&f.attrs); + vark_optional_fields.push(quote!{ + #field_ident: Option < #ty >, + }); + vark_copy_fields.push(quote!{ + #field_ident: optional.#field_ident + }); + let gen = gen_impl_type(ty, &field_ident.to_string()); + help_recurse.extend(gen.help_recurse); + let help_child_placeholders = gen.help_child_placeholders; + help_placeholders_detail.push((quote!{ + format!( + "{}{}", + #flag, + aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) + ) + }, help_docstr)); + let vark = gen.vark; + vark_parse_optional_cases.push(quote!{ + #flag => { + if optional.#field_ident.is_some() { + return state.r_err(format!("The argument {} was already specified", #flag)); + } + state.consume(); + let #f_local_ident = match #vark { + R:: Help => { + return R::Help; + }, + R:: Ok(v) => { + v + }, + R:: Err => { + return R::Err; + }, + R:: EOF => { + return state.r_err(format!("Missing argument for {}", #flag)); + } + }; + optional.#field_ident = Some(#f_local_ident); + return R::Ok(true); + } + }); + return true; + }(&f) { + continue; + } + + // Positional/required parsing + let help_placeholder = field_ident.to_string().to_case(Case::UpperKebab); + let eof_code = if required_i == 0 { + quote!{ + break R::EOF; + } + } else { + quote!{ + break state.r_err(format!("Missing argument {}", #help_placeholder)); + } + }; + let gen = gen_impl_type(&f.ty, &ident.to_string()); + help_recurse.extend(gen.help_recurse); + help_placeholders.push(quote!(#help_placeholder)); + let help_child_placeholders = gen.help_child_placeholders; + help_placeholders_detail.push((quote!{ + format!( + "{}:{}", + #help_placeholder, + aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) + ) + }, help_docstr)); + let vark = gen.vark; + vark_parse_positional.push(quote!{ + let #f_local_ident = loop { + if match state.peek() { + PeekR:: None => false, + PeekR:: Help => break R:: Help, + PeekR:: Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { + R:: Help => { + break R::Help + }, + R:: Ok(v) => { + v + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + unreachable!(); + } + }, + } + { + continue; + } + break #vark; + }; + let #f_local_ident = match #f_local_ident { + R:: Help => { + break R::Help; + }, + R:: Ok(v) => { + v + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + #eof_code + } + }; + }); + vark_copy_fields.push(quote!{ + #field_ident: #f_local_ident + }); + required_i += 1; + } + if vark_optional_fields.len() > 0 { + help_placeholders.push(quote!("[OPT...]")); + } + + // Assemble code + let vark = quote!{ + { + state.simple_enum_root = false; + loop { + #[derive(Default)] struct Optional { + #(#vark_optional_fields) * + } + let mut optional = Optional::default(); + fn parse_optional( + optional: &mut Optional, + state: &mut aargvark::VarkState, + s: String + ) -> R < bool > { + match s.as_str() { + #(#vark_parse_optional_cases) * _ => return R:: Ok(false), + }; + } + #(#vark_parse_positional) * + // Parse any remaining optional args + let opt_search_res = loop { + match state.peek() { + PeekR::None => { + break state.r_ok(()); + }, + PeekR::Help => break R::Help, + PeekR::Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { + R::Help => { + break R::Help; + }, + R::Ok(v) => { + if !v { + break state.r_ok(()); + } + }, + R::Err => { + break R::Err; + }, + R::EOF => { + unreachable!(); + }, + }, + }; + }; + match opt_search_res { + R::Help => { + break R::Help; + }, + R::Ok(()) => { }, + R::Err => { + break R::Err; + }, + R::EOF => { + unreachable!(); + }, + }; + // Build obj + return + break state.r_ok(#ident { + #(#vark_copy_fields), + * + }); + } + } + }; + return GenRec { + vark: vark, + help_child_placeholders: help_placeholders, + help_child_placeholders_detail: help_placeholders_detail, + help_recurse: help_recurse, + }; + }, + Fields::Unnamed(d) => { + return gen_impl_unnamed( + &ident.to_string(), + ident.to_token_stream(), + d + .unnamed + .iter() + .map(|f| (get_docstr(&f.attrs), &f.ty)) + .collect::>() + .as_slice(), + ); + }, + Fields::Unit => { + return GenRec { + vark: quote!{ + state.r_ok(#ident) + }, + help_child_placeholders: vec![], + help_child_placeholders_detail: vec![], + help_recurse: vec![], + }; + }, + }; +} + +fn gen_impl(ast: syn::DeriveInput) -> TokenStream { + let ident = &ast.ident; + let help_placeholder = ident.to_string().to_case(Case::UpperKebab); + let help_docstr = get_docstr(&ast.attrs); + let help_child_placeholder_joiner; + let gen = match &ast.data { + syn::Data::Struct(d) => { + help_child_placeholder_joiner = quote!(" "); + gen_impl_struct(ast.ident.to_token_stream(), &d.fields) + }, + syn::Data::Enum(d) => { + let mut all_tags = vec![]; + let mut vark_cases = vec![]; + let mut help_recurse = vec![]; + let mut help_placeholders = vec![]; + let mut help_placeholders_detail = vec![]; + let mut help_short_placeholders = vec![]; + let mut help_short_placeholders_detail = vec![]; + help_child_placeholder_joiner = quote!("|"); + for v in &d.variants { + let variant_ident = &v.ident; + let name_str = variant_ident.to_string().to_case(Case::Kebab); + let gen = gen_impl_struct(quote!(#ident:: #variant_ident), &v.fields); + all_tags.push(name_str.clone()); + let help_docstr = get_docstr(&v.attrs); + help_short_placeholders.push(quote!(#name_str)); + help_short_placeholders_detail.push((quote!(#name_str.to_string()), help_docstr.clone())); + help_placeholders.push(quote!(#name_str)); + let help_child_placeholders = gen.help_child_placeholders; + help_placeholders_detail.push( + ( + quote!( + format!( + "{}{}", + #name_str, + aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) + ) + ), + help_docstr, + ), + ); + help_recurse.extend(gen.help_recurse); + let vark = gen.vark; + vark_cases.push(quote!{ + #name_str => { + state.consume(); + state.breadcrumbs.push(#name_str.to_string()); + let v = #vark; + if simple_enum_root && matches !(v, R::Help) { + let (mut text, mut seen_sections) = + aargvark::generate_help_section_usage_prefix(state); + #ident:: generate_help_section_suffix(&mut text, &mut seen_sections); + eprintln!("{}", text); + std::process::exit(0); + } + state.breadcrumbs.pop(); + v + } + }); + } + let help_child_short_placeholders_detail: Vec = + help_short_placeholders_detail + .iter() + .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) + .collect(); + GenRec { + vark: quote!{ + { + let simple_enum_root = state.simple_enum_root; + let tag = match state.peek() { + PeekR:: None => return R:: EOF, + PeekR:: Help => { + if simple_enum_root { + let (mut text, mut seen_sections) = + aargvark::generate_help_section_usage_prefix(state); + text.push_str( + & aargvark:: generate_help_section_suffix( + #help_docstr, + vec![#(#help_short_placeholders), *], + vec![#(#help_child_short_placeholders_detail), *], + #help_child_placeholder_joiner + ) + ); + eprintln!("{}", text); + std::process::exit(0); + } + else { + return R::Help; + } + }, + PeekR:: Ok(s) => s, + }; + match tag { + #(#vark_cases) * _ => { + state.r_err( + format!("Unrecognized variant {}. Choices are {:?}", tag, vec![#(#all_tags), *]), + ) + } + } + } + }, + help_child_placeholders: help_placeholders, + help_child_placeholders_detail: help_placeholders_detail, + help_recurse: help_recurse, + } + }, + syn::Data::Union(_) => panic!("union not supported"), + }; + let vark = gen.vark; + let help_child_placeholders = gen.help_child_placeholders; + let help_child_placeholders_detail: Vec = + gen + .help_child_placeholders_detail + .iter() + .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) + .collect(); + let mut seen_help_recurse = HashSet::new(); + let mut help_recurse = vec![]; + for r in gen.help_recurse { + if !seen_help_recurse.insert(r.to_token_stream().to_string()) { + continue; + } + help_recurse.push(r); + } + return quote!{ + impl aargvark:: AargvarkTrait for #ident { + fn vark(state: &mut aargvark::VarkState) -> aargvark:: R < #ident > { + use aargvark::R; + use aargvark::PeekR; + #vark + } + fn generate_help_placeholder() -> String { + format!("{}", #help_placeholder) + } + fn generate_help_section_suffix(text: &mut String, seen_sections: &mut std::collections::HashSet) { + text.push_str( + & aargvark:: generate_help_section_suffix( + #help_docstr, + vec![#(#help_child_placeholders), *], + vec![#(#help_child_placeholders_detail), *], + #help_child_placeholder_joiner + ) + ); + #(#help_recurse:: generate_help_section(text, seen_sections);) * + } + fn generate_help_section(text: &mut String, seen_sections: &mut std::collections::HashSet) { + if ! seen_sections.insert(#help_placeholder.to_string()) { + return; + } + text.push_str(#help_placeholder); + text.push_str(": "); + #ident:: generate_help_section_suffix(text, seen_sections); + } + } + }; +} + +#[proc_macro_derive(Aargvark)] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + return gen_impl(parse_macro_input!(input as DeriveInput)).into(); +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use proc_macro2::TokenStream; + use quote::quote; + use crate::gen_impl; + + #[test] + fn newtype_string() { + assert_eq!( + gen_impl( + syn::parse2(TokenStream::from_str("enum Yol { + ToqQuol, + }").unwrap()).unwrap(), + ).to_string(), + quote!().to_string() + ); + } +} diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..bba9380 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,110 @@ +use aargvark::{ + self, + vark_explicit, +}; +use aargvark_proc_macros::Aargvark; + +macro_rules! svec{ + ($($l: literal), *) => { + vec![$($l.to_string()), *] + }; +} + +#[test] +fn t_str() { + let v: String = vark_explicit("".to_string(), svec!["a"]); + assert_eq!(v, "a"); +} + +#[test] +fn t_vec() { + let v: Vec = vark_explicit("".to_string(), svec!["a", "b"]); + assert_eq!(v, svec!["a", "b"]); +} + +#[test] +fn t_enum_unit() { + #[derive(Aargvark, PartialEq, Debug)] + enum Yol { + ToqQuol, + } + + let v: Yol = vark_explicit("".to_string(), svec!["toq-quol"]); + assert_eq!(v, Yol::ToqQuol); +} + +#[test] +fn t_enum_tuple() { + #[derive(Aargvark, PartialEq, Debug)] + enum Yol { + ToqQuol(String, String), + } + + let v: Yol = vark_explicit("".to_string(), svec!["toq-quol", "yon", "nor"]); + assert_eq!(v, Yol::ToqQuol("yon".into(), "nor".into())); +} + +#[test] +fn t_enum_struct() { + #[derive(Aargvark, PartialEq, Debug)] + enum Yol { + ToqQuol { + a: String, + }, + } + + let v: Yol = vark_explicit("".to_string(), svec!["toq-quol", "pahla"]); + assert_eq!(v, Yol::ToqQuol { a: "pahla".into() }); +} + +#[test] +fn t_struct() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + a: String, + } + + let v: Naya = vark_explicit("".to_string(), svec!["wowo"]); + assert_eq!(v, Naya { a: "wowo".into() }); +} + +#[test] +fn t_struct_opt_only() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + a: Option, + } + + let v: Naya = vark_explicit("".to_string(), svec!["--a", "wowo"]); + assert_eq!(v, Naya { a: Some("wowo".into()) }); +} + +#[test] +fn t_struct_opt_first() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + b: String, + a: Option, + } + + let v: Naya = vark_explicit("".to_string(), svec!["--a", "wowo", "noh"]); + assert_eq!(v, Naya { + b: "noh".into(), + a: Some("wowo".into()), + }); +} + +#[test] +fn t_struct_opt_last() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + b: String, + a: Option, + } + + let v: Naya = vark_explicit("".to_string(), svec!["noh", "--a", "wowo"]); + assert_eq!(v, Naya { + b: "noh".into(), + a: Some("wowo".into()), + }); +} From 4ac2559a4fe88c7e754f05cafd44c90645e85ee2 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 12 Jun 2023 00:15:45 +0900 Subject: [PATCH 02/58] Tweaks, fixes --- Cargo.toml | 17 +++---- readme.md | 8 +-- src/lib.rs | 112 +++++++++++++++++++++++++++-------------- src/proc_macros/mod.rs | 60 +++++++++++++++------- 4 files changed, 129 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c3cb359..5406ba3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,17 +13,16 @@ members = ["src/proc_macros"] [features] default = [] -serde_json = ["dep:serde_json"] -serde_yaml = ["dep:serde_yaml"] -chrono = ["dep:chrono"] +serde_json = ["dep:serde_json", "dep:serde"] +serde_yaml = ["dep:serde_yaml", "dep:serde"] +http_types = ["dep:http"] [dependencies] -anyhow = "1.0.66" -once_cell = "1.16.0" -regex = { version = "1.7.0", default-features = false, features = ["std"] } aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.1" } -chrono = { version = "0.4.26", optional = true } -serde_json = { version = "1.0.96", optional = true } -serde_yaml = { version = "0.9.21", optional = true } +serde_json = { version = "1", optional = true } +serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" comfy-table = "7.0.0" +url = { version = "2", optional = true } +http = { version = "0", optional = true } +serde = { version = "1", optional = true } diff --git a/readme.md b/readme.md index 0b3dcdc..d31f8b0 100644 --- a/readme.md +++ b/readme.md @@ -39,15 +39,15 @@ To parse command line arguments 2. Vark it ``` - let args = MyArgs::vark(); + let args = aargvark::vark::(); ``` You can derive structs, enums, and tuples, as well as `Vec`, `HashSet`, most `Ip` and `SocketAddr` types, and `PathBuf`. There are also custom structs for reading files: -- `VarkFile` -- `VarkJson` -- `VarkYaml` +- `AargvarkFile` +- `AargvarkJson` +- `AargvarkYaml` For JSON and Yaml you must enable the respective features. diff --git a/src/lib.rs b/src/lib.rs index 77d44e9..a3920db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,14 +21,8 @@ use std::{ OsString, }, }; -pub use aargvark_proc_macros; -pub use anyhow::{ - Context, - Error, - Result, -}; +pub use aargvark_proc_macros::Aargvark; use comfy_table::Cell; -pub use once_cell::sync::Lazy; struct VarkErr { i: usize, @@ -67,9 +61,11 @@ pub fn join_strs(sep: &str, v: &[&str]) -> String { #[doc(hidden)] pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet) { - let mut text = "Usage:".to_string(); - for s in &state.breadcrumbs { - text.push_str(" "); + let mut text = "Usage: ".to_string(); + for (i, s) in state.breadcrumbs.iter().enumerate() { + if i > 0 { + text.push_str(" "); + } text.push_str(&s); } return (text, HashSet::new()); @@ -83,14 +79,17 @@ pub fn generate_help_section_suffix( joiner: &str, ) -> String { let mut out = String::new(); - for p in placeholders { - out.push_str(joiner); + out.push_str(" "); + for (i, p) in placeholders.iter().enumerate() { + if i > 0 { + out.push_str(joiner); + } out.push_str(p); } out.push_str("\n\n"); if !docstr.is_empty() { out.push_str(docstr); - out.push_str("\n\n"); + out.push_str("\n"); } let mut table = comfy_table::Table::new(); table.load_preset(comfy_table::presets::NOTHING); @@ -100,7 +99,7 @@ pub fn generate_help_section_suffix( } table.set_constraints(vec![comfy_table::ColumnConstraint::UpperBoundary(comfy_table::Width::Percentage(60))]); out.push_str(&table.to_string()); - out.push_str("\n"); + out.push_str("\n\n"); out } @@ -212,6 +211,7 @@ pub fn vark() -> T { /// parsable enums/structs. pub trait AargvarkTrait: Sized { fn vark(state: &mut VarkState) -> R; + fn always_opt() -> bool; fn generate_help_placeholder() -> String; fn generate_help_section(text: &mut String, seen_sections: &mut HashSet); fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet); @@ -240,6 +240,10 @@ impl AargvarkTrait for T { } } + fn always_opt() -> bool { + false + } + fn generate_help_placeholder() -> String { T::generate_help_placeholder() } @@ -250,45 +254,69 @@ impl AargvarkTrait for T { } macro_rules! auto_from_str{ - ($t: ty) => { + ($placeholder: literal, $t: ty) => { impl AargvarkFromStr for $t { fn from_str(s: &str) -> Result { ::from_str(s).map_err(|e| e.to_string()) } fn generate_help_placeholder() -> String { - format!( - "<{}>", - convert_case::Casing::to_case(&std::any::type_name::(), convert_case::Case::UpperKebab) - ) + format!("<{}>", $placeholder) } } }; } -auto_from_str!(String); +auto_from_str!("STRING", String); + +auto_from_str!("INT", u8); + +auto_from_str!("INT", u16); + +auto_from_str!("INT", u32); + +auto_from_str!("INT", u64); + +auto_from_str!("INT", i8); + +auto_from_str!("INT", i16); -auto_from_str!(OsString); +auto_from_str!("INT", i32); -auto_from_str!(SocketAddr); +auto_from_str!("INT", i64); -auto_from_str!(SocketAddrV4); +auto_from_str!("INT", f32); -auto_from_str!(SocketAddrV6); +auto_from_str!("NUM", f64); -auto_from_str!(IpAddr); +auto_from_str!("STRING", OsString); -auto_from_str!(Ipv4Addr); +auto_from_str!("SOCKET", SocketAddr); -auto_from_str!(Ipv6Addr); +auto_from_str!("SOCKETV4", SocketAddrV4); -auto_from_str!(PathBuf); +auto_from_str!("SOCKETV6", SocketAddrV6); + +auto_from_str!("IP", IpAddr); + +auto_from_str!("IPV4", Ipv4Addr); + +auto_from_str!("IPV6", Ipv6Addr); + +auto_from_str!("PATH", PathBuf); + +#[cfg(feature = "http_types")] +auto_from_str!("URI", http::Uri); impl AargvarkTrait for bool { fn vark(state: &mut VarkState) -> R { return state.r_ok(true); } + fn always_opt() -> bool { + true + } + fn generate_help_placeholder() -> String { return "".to_string(); } @@ -324,14 +352,14 @@ impl AargvarkFromStr for AargvarkFile { /// This parses a path (or - for stdin) passed on the command line as json into the /// specified type. -#[cfg(serde_json)] -pub struct AargvarkJson(T); +#[cfg(feature = "serde_json")] +pub struct AargvarkJson(pub T); -#[cfg(serde_json)] -impl AargvarkFromStr for AargvarkJson { +#[cfg(feature = "serde_json")] +impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { fn from_str(s: &str) -> Result { let b = AargvarkFile::from_str(s)?; - match serde_json::from_str(b) { + match serde_json::from_slice(&b.0) { Ok(v) => return Ok(Self(v)), Err(e) => return Err(e.to_string()), }; @@ -344,14 +372,14 @@ impl AargvarkFromStr for AargvarkJson { /// This parses a path (or - for stdin) passed on the command line as yaml into the /// specified type. -#[cfg(serde_yaml)] -pub struct AargvarkYaml(T); +#[cfg(feature = "serde_yaml")] +pub struct AargvarkYaml(pub T); -#[cfg(serde_yaml)] -impl AargvarkFromStr for AargvarkYaml { +#[cfg(feature = "serde_yaml")] +impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { fn from_str(s: &str) -> Result { let b = AargvarkFile::from_str(s)?; - match serde_yaml::from_str(b) { + match serde_yaml::from_slice(&b.0) { Ok(v) => return Ok(Self(v)), Err(e) => return Err(e.to_string()), }; @@ -394,6 +422,10 @@ impl AargvarkTrait for Vec { return vark_from_iter(state); } + fn always_opt() -> bool { + false + } + fn generate_help_placeholder() -> String { format!("{}[ ...]", T::generate_help_placeholder()) } @@ -412,6 +444,10 @@ impl AargvarkTrait for HashSet { return vark_from_iter(state); } + fn always_opt() -> bool { + false + } + fn generate_help_placeholder() -> String { format!("{}[ ...]", T::generate_help_placeholder()) } diff --git a/src/proc_macros/mod.rs b/src/proc_macros/mod.rs index 317949d..62924cc 100644 --- a/src/proc_macros/mod.rs +++ b/src/proc_macros/mod.rs @@ -28,11 +28,19 @@ fn get_docstr(attrs: &Vec) -> String { if !tokens.next().map(|t| t.to_string().as_str() == "=").unwrap_or(false) { continue; } - let s = match tokens.next() { - Some(s) => s.to_string(), - None => continue, - }; - return s; + let mut out = String::new(); + for t in tokens { + match t { + proc_macro2::TokenTree::Group(_) => continue, + proc_macro2::TokenTree::Ident(_) => continue, + proc_macro2::TokenTree::Punct(_) => continue, + proc_macro2::TokenTree::Literal(l) => { + let s = l.to_string(); + out.push_str(&s[1 .. s.len() - 1]); + }, + } + } + return out.trim().to_string(); } return "".to_string(); } @@ -49,10 +57,10 @@ fn gen_impl_type(ty: &Type, path: &str) -> GenRec { Type::Path(t) => { return GenRec { vark: quote!{ - #t:: vark(state) + < #t >:: vark(state) }, help_child_placeholders: vec![quote!{ - #t:: generate_help_placeholder() + &< #t >:: generate_help_placeholder() }], help_child_placeholders_detail: vec![], help_recurse: vec![t.clone()], @@ -410,7 +418,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let mut help_placeholders_detail = vec![]; let mut help_short_placeholders = vec![]; let mut help_short_placeholders_detail = vec![]; - help_child_placeholder_joiner = quote!("|"); + help_child_placeholder_joiner = quote!(" | "); for v in &d.variants { let variant_ident = &v.ident; let name_str = variant_ident.to_string().to_case(Case::Kebab); @@ -430,11 +438,17 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) ), - help_docstr, + help_docstr.clone(), ), ); help_recurse.extend(gen.help_recurse); let vark = gen.vark; + let help_child_placeholders_detail: Vec = + gen + .help_child_placeholders_detail + .iter() + .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) + .collect(); vark_cases.push(quote!{ #name_str => { state.consume(); @@ -443,8 +457,17 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { if simple_enum_root && matches !(v, R::Help) { let (mut text, mut seen_sections) = aargvark::generate_help_section_usage_prefix(state); - #ident:: generate_help_section_suffix(&mut text, &mut seen_sections); - eprintln!("{}", text); + text.push_str( + & aargvark:: generate_help_section_suffix( + #help_docstr, + vec![#(#help_child_placeholders), *], + vec![#(#help_child_placeholders_detail), *], + " " + ) + ); + #( + < #help_recurse >:: generate_help_section(&mut text, &mut seen_sections); + ) * eprintln !("{}", text.trim()); std::process::exit(0); } state.breadcrumbs.pop(); @@ -452,7 +475,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { } }); } - let help_child_short_placeholders_detail: Vec = + let help_short_placeholders_detail: Vec = help_short_placeholders_detail .iter() .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) @@ -471,11 +494,11 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { & aargvark:: generate_help_section_suffix( #help_docstr, vec![#(#help_short_placeholders), *], - vec![#(#help_child_short_placeholders_detail), *], + vec![#(#help_short_placeholders_detail), *], #help_child_placeholder_joiner ) ); - eprintln!("{}", text); + eprintln!("{}", text.trim()); std::process::exit(0); } else { @@ -523,6 +546,9 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { use aargvark::PeekR; #vark } + fn always_opt() -> bool { + false + } fn generate_help_placeholder() -> String { format!("{}", #help_placeholder) } @@ -535,15 +561,15 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { #help_child_placeholder_joiner ) ); - #(#help_recurse:: generate_help_section(text, seen_sections);) * + #(< #help_recurse >:: generate_help_section(text, seen_sections);) * } fn generate_help_section(text: &mut String, seen_sections: &mut std::collections::HashSet) { if ! seen_sections.insert(#help_placeholder.to_string()) { return; } text.push_str(#help_placeholder); - text.push_str(": "); - #ident:: generate_help_section_suffix(text, seen_sections); + text.push_str(":"); + < #ident >:: generate_help_section_suffix(text, seen_sections); } } }; From 4669eb7c625011c66583589f1bab0c1b44a66572 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 12 Jun 2023 00:56:54 +0900 Subject: [PATCH 03/58] Looking better --- Cargo.toml | 3 ++- src/lib.rs | 23 +++++++++++----- src/proc_macros/mod.rs | 61 +++++++++++++++++++----------------------- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5406ba3..55d21a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.1" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" -comfy-table = "7.0.0" +comfy-table = { version = "7.0.0", features = ["custom_styling"] } url = { version = "2", optional = true } http = { version = "0", optional = true } serde = { version = "1", optional = true } +console = "0.15.7" diff --git a/src/lib.rs b/src/lib.rs index a3920db..66e8f57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,14 @@ pub fn join_strs(sep: &str, v: &[&str]) -> String { v.join(sep) } +pub fn style_lit(l: &str) -> String { + console::Style::new().bold().apply_to(l).to_string() +} + +pub fn style_name(l: &str) -> String { + l.to_string() +} + #[doc(hidden)] pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet) { let mut text = "Usage: ".to_string(); @@ -66,7 +74,7 @@ pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet if i > 0 { text.push_str(" "); } - text.push_str(&s); + text.push_str(&style_lit(s)); } return (text, HashSet::new()); } @@ -75,7 +83,7 @@ pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet pub fn generate_help_section_suffix( docstr: &str, placeholders: Vec<&str>, - placeholders_detail: Vec<(String, &str)>, + placeholders_detail: Vec<(&str, &str)>, joiner: &str, ) -> String { let mut out = String::new(); @@ -97,7 +105,10 @@ pub fn generate_help_section_suffix( for (placeholder, docstr) in placeholders_detail { table.add_row(vec![comfy_table::Cell::new(placeholder), Cell::new(docstr)]); } - table.set_constraints(vec![comfy_table::ColumnConstraint::UpperBoundary(comfy_table::Width::Percentage(60))]); + table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { + lower: comfy_table::Width::Percentage(20), + upper: comfy_table::Width::Percentage(60), + }]); out.push_str(&table.to_string()); out.push_str("\n\n"); out @@ -178,7 +189,7 @@ pub fn vark_explicit(command: String, args: Vec) -> T text.push_str(&" ".repeat(*display_arg_offsets.get(e.i).unwrap())); text.push_str("^\n"); } - eprintln!("{}", text); + eprintln!("{}\n", text); exit(1); }, R::Ok(v) => { @@ -194,7 +205,7 @@ pub fn vark_explicit(command: String, args: Vec) -> T R::Help => { let (mut text, mut seen_sections) = generate_help_section_usage_prefix(&state); T::generate_help_section_suffix(&mut text, &mut seen_sections); - eprintln!("{}", text); + eprintln!("{}\n", text.trim()); exit(0); }, } @@ -261,7 +272,7 @@ macro_rules! auto_from_str{ } fn generate_help_placeholder() -> String { - format!("<{}>", $placeholder) + format!("<{}>", style_lit($placeholder)) } } }; diff --git a/src/proc_macros/mod.rs b/src/proc_macros/mod.rs index 62924cc..68d7091 100644 --- a/src/proc_macros/mod.rs +++ b/src/proc_macros/mod.rs @@ -20,29 +20,19 @@ use syn::{ }; fn get_docstr(attrs: &Vec) -> String { + let mut out = String::new(); for attr in attrs { - if attr.path.to_token_stream().to_string() != "doc" { + if !attr.path.is_ident("doc") { continue; } - let mut tokens = attr.tokens.clone().into_iter(); - if !tokens.next().map(|t| t.to_string().as_str() == "=").unwrap_or(false) { - continue; - } - let mut out = String::new(); - for t in tokens { - match t { - proc_macro2::TokenTree::Group(_) => continue, - proc_macro2::TokenTree::Ident(_) => continue, - proc_macro2::TokenTree::Punct(_) => continue, - proc_macro2::TokenTree::Literal(l) => { - let s = l.to_string(); - out.push_str(&s[1 .. s.len() - 1]); - }, - } + match attr.parse_meta().unwrap() { + syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(v), .. }) => { + out.push_str(&v.value()); + }, + _ => continue, } - return out.trim().to_string(); } - return "".to_string(); + return out.trim().to_string(); } struct GenRec { @@ -196,9 +186,9 @@ fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { help_recurse.extend(gen.help_recurse); let help_child_placeholders = gen.help_child_placeholders; help_placeholders_detail.push((quote!{ - format!( + &format!( "{}{}", - #flag, + aargvark:: style_lit(#flag), aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) }, help_docstr)); @@ -245,12 +235,12 @@ fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { }; let gen = gen_impl_type(&f.ty, &ident.to_string()); help_recurse.extend(gen.help_recurse); - help_placeholders.push(quote!(#help_placeholder)); + help_placeholders.push(quote!(& aargvark:: style_name(#help_placeholder))); let help_child_placeholders = gen.help_child_placeholders; help_placeholders_detail.push((quote!{ - format!( + &format!( "{}:{}", - #help_placeholder, + aargvark:: style_name(#help_placeholder), aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) }, help_docstr)); @@ -425,23 +415,26 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let gen = gen_impl_struct(quote!(#ident:: #variant_ident), &v.fields); all_tags.push(name_str.clone()); let help_docstr = get_docstr(&v.attrs); - help_short_placeholders.push(quote!(#name_str)); - help_short_placeholders_detail.push((quote!(#name_str.to_string()), help_docstr.clone())); - help_placeholders.push(quote!(#name_str)); + help_short_placeholders.push(quote!(& aargvark:: style_lit(#name_str))); + help_short_placeholders_detail.push( + (quote!(& aargvark:: style_lit(#name_str)), help_docstr.clone()), + ); + help_placeholders.push(quote!(& aargvark:: style_lit(#name_str))); let help_child_placeholders = gen.help_child_placeholders; help_placeholders_detail.push( ( quote!( - format!( + &format!( "{}{}", - #name_str, + aargvark:: style_lit(#name_str), aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) ), help_docstr.clone(), ), ); - help_recurse.extend(gen.help_recurse); + help_recurse.extend(gen.help_recurse.clone()); + let child_help_recurse = gen.help_recurse; let vark = gen.vark; let help_child_placeholders_detail: Vec = gen @@ -466,8 +459,8 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { ) ); #( - < #help_recurse >:: generate_help_section(&mut text, &mut seen_sections); - ) * eprintln !("{}", text.trim()); + < #child_help_recurse >:: generate_help_section(&mut text, &mut seen_sections); + ) * eprintln !("{}\n", text.trim()); std::process::exit(0); } state.breadcrumbs.pop(); @@ -498,7 +491,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { #help_child_placeholder_joiner ) ); - eprintln!("{}", text.trim()); + eprintln!("{}\n", text.trim()); std::process::exit(0); } else { @@ -550,7 +543,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { false } fn generate_help_placeholder() -> String { - format!("{}", #help_placeholder) + aargvark:: style_name(#help_placeholder) } fn generate_help_section_suffix(text: &mut String, seen_sections: &mut std::collections::HashSet) { text.push_str( @@ -567,7 +560,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { if ! seen_sections.insert(#help_placeholder.to_string()) { return; } - text.push_str(#help_placeholder); + text.push_str(& aargvark:: style_name(#help_placeholder)); text.push_str(":"); < #ident >:: generate_help_section_suffix(text, seen_sections); } From d76075831ed8e0c2307279efdfc3d2ddf9de8089 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 12 Jun 2023 03:04:45 +0900 Subject: [PATCH 04/58] Solid --- readme.md | 38 +++++++++++++++++++++++++++++--------- src/lib.rs | 37 +++++++++++++++++-------------------- src/proc_macros/mod.rs | 3 --- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/readme.md b/readme.md index d31f8b0..a68371d 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,32 @@ -Self-similar derive-based argument parsing, similar to Clap-derive. +Self-similar derive-based command line argument parsing, in the same genre as Clap-derive. -This attempts to implement a simple, self-similar structure for parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. +This attempts to support parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. Just because you can doesn't mean you should. + +``` +$ echo This is an example help output, sans light ansi styling +$ ./target/debug/spagh-cli publish -h +Usage: ./target/debug/spagh-cli publish PUBLISH + +Create or replace existing publish data for an identity on a publisher server + + +PUBLISH: SERVER IDENTITY DATA + + SERVER: URL of a server with publishing set up + IDENTITY: IDENTITY Identity to publish as + DATA: |- Data to publish. Must be json in the structure `{KEY: {"ttl": SECONDS, "value": "DATA"}, ...}` + +IDENTITY: local | card + + local |- + card PCSC-ID PIN +``` # Why or why not Why this and not Clap? -- This parse more complex data types, like vectors of sub-structures, or enums +- This parses more complex data types, like vectors of sub-structures, or enums - It's more consistent - It has a super-simple interface (just `#[derive(Aargvark)]`) @@ -42,12 +62,12 @@ To parse command line arguments let args = aargvark::vark::(); ``` -You can derive structs, enums, and tuples, as well as `Vec`, `HashSet`, most `Ip` and `SocketAddr` types, and `PathBuf`. +Optional fields in structs become `--long` arguments. If you want a `bool` long option that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. -There are also custom structs for reading files: +You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, most `Ip` and `SocketAddr` types, and `PathBuf` provided. -- `AargvarkFile` -- `AargvarkJson` -- `AargvarkYaml` +Some additional wrappers are provided from automatically loading (and parsing) files: -For JSON and Yaml you must enable the respective features. +- `AargvarkFile` +- `AargvarkJson` requires feature `serde_json` +- `AargvarkYaml` requires feature `serde_yaml` diff --git a/src/lib.rs b/src/lib.rs index 66e8f57..0c9b5bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,7 +222,6 @@ pub fn vark() -> T { /// parsable enums/structs. pub trait AargvarkTrait: Sized { fn vark(state: &mut VarkState) -> R; - fn always_opt() -> bool; fn generate_help_placeholder() -> String; fn generate_help_section(text: &mut String, seen_sections: &mut HashSet); fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet); @@ -251,10 +250,6 @@ impl AargvarkTrait for T { } } - fn always_opt() -> bool { - false - } - fn generate_help_placeholder() -> String { T::generate_help_placeholder() } @@ -324,10 +319,6 @@ impl AargvarkTrait for bool { return state.r_ok(true); } - fn always_opt() -> bool { - true - } - fn generate_help_placeholder() -> String { return "".to_string(); } @@ -357,7 +348,7 @@ impl AargvarkFromStr for AargvarkFile { } fn generate_help_placeholder() -> String { - format!("") + format!("<{}>|{}", style_lit("PATH"), style_lit("-")) } } @@ -377,7 +368,14 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { } fn generate_help_placeholder() -> String { - format!("") + format!("<{}>|{}", style_lit("PATH"), style_lit("-")) + } +} + +#[cfg(feature = "serde_json")] +impl Clone for AargvarkJson { + fn clone(&self) -> Self { + AargvarkJson(self.0.clone()) } } @@ -397,7 +395,14 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { } fn generate_help_placeholder() -> String { - format!("") + format!("<{}>|{}", style_lit("PATH"), style_lit("-")) + } +} + +#[cfg(feature = "serde_yaml")] +impl Clone for AargvarkYaml { + fn clone(&self) -> Self { + AargvarkYaml(self.0.clone()) } } @@ -433,10 +438,6 @@ impl AargvarkTrait for Vec { return vark_from_iter(state); } - fn always_opt() -> bool { - false - } - fn generate_help_placeholder() -> String { format!("{}[ ...]", T::generate_help_placeholder()) } @@ -455,10 +456,6 @@ impl AargvarkTrait for HashSet { return vark_from_iter(state); } - fn always_opt() -> bool { - false - } - fn generate_help_placeholder() -> String { format!("{}[ ...]", T::generate_help_placeholder()) } diff --git a/src/proc_macros/mod.rs b/src/proc_macros/mod.rs index 68d7091..c74d71a 100644 --- a/src/proc_macros/mod.rs +++ b/src/proc_macros/mod.rs @@ -539,9 +539,6 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { use aargvark::PeekR; #vark } - fn always_opt() -> bool { - false - } fn generate_help_placeholder() -> String { aargvark:: style_name(#help_placeholder) } From eebc2f6b50d982bbbd17bc415a4a20b5f2797faf Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 12 Jun 2023 03:06:10 +0900 Subject: [PATCH 05/58] Removing unused deps --- Cargo.toml | 2 +- src/proc_macros/Cargo.toml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 55d21a1..01457d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.1" } +aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.2" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" diff --git a/src/proc_macros/Cargo.toml b/src/proc_macros/Cargo.toml index cd944db..01976b4 100644 --- a/src/proc_macros/Cargo.toml +++ b/src/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.1" +version = "0.0.2" edition = "2021" license = "ISC" description = "Helper crate for aargvark" @@ -14,8 +14,6 @@ path = "mod.rs" [dependencies] convert_case = "0.6.0" -litrs = "0.2.3" proc-macro2 = "1.0.47" quote = "1.0.21" -regex-syntax = "0.6.28" syn = "1.0.103" From 158ec1223be9f9dd79de00389d0c4a0e3d4fd17c Mon Sep 17 00:00:00 2001 From: andrew <> Date: Tue, 13 Jun 2023 14:17:10 +0900 Subject: [PATCH 06/58] Readme tweaks --- readme.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index a68371d..da7702d 100644 --- a/readme.md +++ b/readme.md @@ -32,8 +32,8 @@ Why this and not Clap? Why not this? -- Some command line parsing conventions were discarded in order to simplify and maintain self-similarity. A lot of command line conventions are inconsistent or break down as you nest things. -- There's less customizability. Some things (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. Other things just haven't been implemented yet due to lack of time. +- Some command line parsing conventions were discarded in order to simplify and maintain self-similarity. A lot of command line conventions are inconsistent or break down as you nest things, after all. +- There's less customizability. Some tricks (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. Other things just haven't been implemented yet due to lack of time. - Alpha # Conventions and usage @@ -62,12 +62,14 @@ To parse command line arguments let args = aargvark::vark::(); ``` -Optional fields in structs become `--long` arguments. If you want a `bool` long option that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. +Optional fields in structs become optional (`--long`) arguments. If you want a `bool` long option that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, most `Ip` and `SocketAddr` types, and `PathBuf` provided. -Some additional wrappers are provided from automatically loading (and parsing) files: +Some additional wrappers are provided for automatically loading (and parsing) files: - `AargvarkFile` - `AargvarkJson` requires feature `serde_json` - `AargvarkYaml` requires feature `serde_yaml` + +To parse your own types, implement `AargvarkTrait`, or if your type takes a single string argument you can implement `AargvarkFromStr`. From 32d0257f4ddeb5eb35f23d170b80d31e4af6a056 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Tue, 13 Jun 2023 14:20:16 +0900 Subject: [PATCH 07/58] More readme junk --- readme.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index da7702d..0c4aea2 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,7 @@ -Self-similar derive-based command line argument parsing, in the same genre as Clap-derive. +Self-similar derive-based command line argument parsing, in the same genre as Clap-derive. It supports + +- Command line parsing +- Help This attempts to support parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. Just because you can doesn't mean you should. From 8f7c6329cfd6053c7ed20204390c1717825cb965 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 18 Jun 2023 10:47:44 +0900 Subject: [PATCH 08/58] Array out of bounds fix, formatting --- Cargo.toml | 2 +- src/lib.rs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 01457d3..2b0a868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.1" +version = "0.0.2" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/src/lib.rs b/src/lib.rs index 0c9b5bc..d5d7dee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -183,10 +183,20 @@ pub fn vark_explicit(command: String, args: Vec) -> T for e in state.errors { text.push_str("\n"); text.push_str(&format!(" * {}\n", e.err)); - text.push_str(&format!("while parsing {:?} at\n", e.breadcrumbs)); + text.push_str(&format!(" while parsing {:?} at\n", e.breadcrumbs)); + text.push_str(" "); text.push_str(&display_args); text.push_str("\n"); - text.push_str(&" ".repeat(*display_arg_offsets.get(e.i).unwrap())); + text.push_str(" "); + text.push_str( + &" ".repeat( + display_arg_offsets + .get(e.i) + .cloned() + .or_else(|| display_arg_offsets.last().cloned()) + .unwrap_or(0usize), + ), + ); text.push_str("^\n"); } eprintln!("{}\n", text); From 5a5607eac4cac0de5e3a8127d3b9d1d37b7f69db Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 18 Jun 2023 10:53:51 +0900 Subject: [PATCH 09/58] A different end-of-args error display strategy --- Cargo.toml | 2 +- src/lib.rs | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b0a868..5699344 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.2" +version = "0.0.3" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/src/lib.rs b/src/lib.rs index d5d7dee..4b6c582 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,8 +176,10 @@ pub fn vark_explicit(command: String, args: Vec) -> T display_arg_offsets.push(offset); offset += d.chars().count() + 1; } + display_arg_offsets.push(offset); } - let display_args = display_args.join(" "); + let mut display_args = display_args.join(" "); + display_args.push_str(" "); let mut text = "Error parsing command line arguments.\n".to_string(); state.errors.reverse(); for e in state.errors { @@ -188,15 +190,7 @@ pub fn vark_explicit(command: String, args: Vec) -> T text.push_str(&display_args); text.push_str("\n"); text.push_str(" "); - text.push_str( - &" ".repeat( - display_arg_offsets - .get(e.i) - .cloned() - .or_else(|| display_arg_offsets.last().cloned()) - .unwrap_or(0usize), - ), - ); + text.push_str(&" ".repeat(display_arg_offsets.get(e.i).cloned().unwrap_or(0usize))); text.push_str("^\n"); } eprintln!("{}\n", text); From 3ebe9355a017e46d4dc70978e8f4595c4063173e Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 5 Jul 2023 21:50:41 +0900 Subject: [PATCH 10/58] Keep source info with deserialized files, release --- Cargo.toml | 2 +- src/lib.rs | 55 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5699344..84669af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.3" +version = "0.0.4" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/src/lib.rs b/src/lib.rs index 4b6c582..424d1cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -332,20 +332,35 @@ impl AargvarkTrait for bool { fn generate_help_section_suffix(_text: &mut String, _seen_sections: &mut HashSet) { } } +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Source { + Stdin, + File(PathBuf), +} + /// This parses a path (or - for stdin) passed on the command line into bytes. -pub struct AargvarkFile(Vec); +pub struct AargvarkFile { + pub value: Vec, + pub source: Source, +} impl AargvarkFromStr for AargvarkFile { fn from_str(s: &str) -> Result { if s == "-" { let mut out = vec![]; match stdin().read_to_end(&mut out) { - Ok(_) => return Ok(Self(out)), + Ok(_) => return Ok(Self { + value: out, + source: Source::Stdin, + }), Err(e) => return Err(format!("Error reading stdin: {}", e)), }; } else { match fs::read(s) { - Ok(v) => return Ok(Self(v)), + Ok(v) => return Ok(Self { + value: v, + source: Source::File(PathBuf::from(s)), + }), Err(e) => return Err(format!("Error reading {}: {}", s, e)), }; } @@ -359,14 +374,20 @@ impl AargvarkFromStr for AargvarkFile { /// This parses a path (or - for stdin) passed on the command line as json into the /// specified type. #[cfg(feature = "serde_json")] -pub struct AargvarkJson(pub T); +pub struct AargvarkJson { + pub value: T, + pub source: Source, +} #[cfg(feature = "serde_json")] impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { fn from_str(s: &str) -> Result { let b = AargvarkFile::from_str(s)?; - match serde_json::from_slice(&b.0) { - Ok(v) => return Ok(Self(v)), + match serde_json::from_slice(&b.value) { + Ok(v) => return Ok(Self { + value: v, + source: b.source, + }), Err(e) => return Err(e.to_string()), }; } @@ -379,21 +400,30 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { #[cfg(feature = "serde_json")] impl Clone for AargvarkJson { fn clone(&self) -> Self { - AargvarkJson(self.0.clone()) + return AargvarkJson { + value: self.value.clone(), + source: self.source.clone(), + }; } } /// This parses a path (or - for stdin) passed on the command line as yaml into the /// specified type. #[cfg(feature = "serde_yaml")] -pub struct AargvarkYaml(pub T); +pub struct AargvarkYaml { + pub value: T, + pub source: Source, +} #[cfg(feature = "serde_yaml")] impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { fn from_str(s: &str) -> Result { let b = AargvarkFile::from_str(s)?; - match serde_yaml::from_slice(&b.0) { - Ok(v) => return Ok(Self(v)), + match serde_yaml::from_slice(&b.value) { + Ok(v) => return Ok(Self { + value: v, + source: b.source, + }), Err(e) => return Err(e.to_string()), }; } @@ -406,7 +436,10 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { #[cfg(feature = "serde_yaml")] impl Clone for AargvarkYaml { fn clone(&self) -> Self { - AargvarkYaml(self.0.clone()) + return AargvarkYaml { + value: self.value.clone(), + source: self.source.clone(), + }; } } From 6ba7632141f56ec8635e370f502929c550862986 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Tue, 5 Sep 2023 21:40:27 +0900 Subject: [PATCH 11/58] Style improvements; usize --- src/lib.rs | 39 +++++++++++++++++++++++---------------- src/proc_macros/mod.rs | 24 +++++++++++------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 424d1cb..e0caa87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,22 +59,26 @@ pub fn join_strs(sep: &str, v: &[&str]) -> String { v.join(sep) } -pub fn style_lit(l: &str) -> String { - console::Style::new().bold().apply_to(l).to_string() +pub fn style_type(l: &str) -> String { + console::Style::new().magenta().apply_to(l).to_string() } -pub fn style_name(l: &str) -> String { - l.to_string() +pub fn style_link(l: &str) -> String { + console::Style::new().blue().apply_to(l).to_string() +} + +pub fn style_section(l: &str) -> String { + console::Style::new().bold().apply_to(l).to_string() } #[doc(hidden)] pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet) { - let mut text = "Usage: ".to_string(); + let mut text = style_section("Usage: "); for (i, s) in state.breadcrumbs.iter().enumerate() { if i > 0 { text.push_str(" "); } - text.push_str(&style_lit(s)); + text.push_str(s); } return (text, HashSet::new()); } @@ -97,13 +101,14 @@ pub fn generate_help_section_suffix( out.push_str("\n\n"); if !docstr.is_empty() { out.push_str(docstr); - out.push_str("\n"); + out.push_str("\n\n"); } let mut table = comfy_table::Table::new(); table.load_preset(comfy_table::presets::NOTHING); table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); for (placeholder, docstr) in placeholders_detail { - table.add_row(vec![comfy_table::Cell::new(placeholder), Cell::new(docstr)]); + // One space due to invisible border + table.add_row(vec![comfy_table::Cell::new(format!(" {}", placeholder)), Cell::new(docstr)]); } table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { lower: comfy_table::Width::Percentage(20), @@ -111,7 +116,7 @@ pub fn generate_help_section_suffix( }]); out.push_str(&table.to_string()); out.push_str("\n\n"); - out + return out; } impl VarkState { @@ -271,7 +276,7 @@ macro_rules! auto_from_str{ } fn generate_help_placeholder() -> String { - format!("<{}>", style_lit($placeholder)) + style_type(&format!("<{}>", $placeholder)) } } }; @@ -287,6 +292,8 @@ auto_from_str!("INT", u32); auto_from_str!("INT", u64); +auto_from_str!("INT", usize); + auto_from_str!("INT", i8); auto_from_str!("INT", i16); @@ -324,7 +331,7 @@ impl AargvarkTrait for bool { } fn generate_help_placeholder() -> String { - return "".to_string(); + return style_type(""); } fn generate_help_section(_text: &mut String, _seen_sections: &mut HashSet) { } @@ -367,7 +374,7 @@ impl AargvarkFromStr for AargvarkFile { } fn generate_help_placeholder() -> String { - format!("<{}>|{}", style_lit("PATH"), style_lit("-")) + return style_type("|-"); } } @@ -393,7 +400,7 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { } fn generate_help_placeholder() -> String { - format!("<{}>|{}", style_lit("PATH"), style_lit("-")) + format!("<{}>|{}", style_type("PATH"), style_type("-")) } } @@ -429,7 +436,7 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { } fn generate_help_placeholder() -> String { - format!("<{}>|{}", style_lit("PATH"), style_lit("-")) + format!("<{}>|{}", style_type("PATH"), style_type("-")) } } @@ -476,7 +483,7 @@ impl AargvarkTrait for Vec { } fn generate_help_placeholder() -> String { - format!("{}[ ...]", T::generate_help_placeholder()) + return format!("{}{}", T::generate_help_placeholder(), style_link("[ ...]")); } fn generate_help_section(text: &mut String, seen_sections: &mut HashSet) { @@ -494,7 +501,7 @@ impl AargvarkTrait for HashSet { } fn generate_help_placeholder() -> String { - format!("{}[ ...]", T::generate_help_placeholder()) + return format!("{}{}", T::generate_help_placeholder(), style_link("[ ...]")); } fn generate_help_section(text: &mut String, seen_sections: &mut HashSet) { diff --git a/src/proc_macros/mod.rs b/src/proc_macros/mod.rs index c74d71a..a2ff614 100644 --- a/src/proc_macros/mod.rs +++ b/src/proc_macros/mod.rs @@ -188,7 +188,7 @@ fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { help_placeholders_detail.push((quote!{ &format!( "{}{}", - aargvark:: style_lit(#flag), + #flag, aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) }, help_docstr)); @@ -235,12 +235,12 @@ fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { }; let gen = gen_impl_type(&f.ty, &ident.to_string()); help_recurse.extend(gen.help_recurse); - help_placeholders.push(quote!(& aargvark:: style_name(#help_placeholder))); + help_placeholders.push(quote!(& aargvark:: style_link(#help_placeholder))); let help_child_placeholders = gen.help_child_placeholders; help_placeholders_detail.push((quote!{ &format!( - "{}:{}", - aargvark:: style_name(#help_placeholder), + "{}{}", + aargvark::style_link(&format!("{}:", #help_placeholder)), aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) }, help_docstr)); @@ -291,7 +291,7 @@ fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { required_i += 1; } if vark_optional_fields.len() > 0 { - help_placeholders.push(quote!("[OPT...]")); + help_placeholders.push(quote!(&aargvark::style_link("[OPT...]"))); } // Assemble code @@ -415,18 +415,16 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let gen = gen_impl_struct(quote!(#ident:: #variant_ident), &v.fields); all_tags.push(name_str.clone()); let help_docstr = get_docstr(&v.attrs); - help_short_placeholders.push(quote!(& aargvark:: style_lit(#name_str))); - help_short_placeholders_detail.push( - (quote!(& aargvark:: style_lit(#name_str)), help_docstr.clone()), - ); - help_placeholders.push(quote!(& aargvark:: style_lit(#name_str))); + help_short_placeholders.push(quote!(#name_str)); + help_short_placeholders_detail.push((quote!(#name_str), help_docstr.clone())); + help_placeholders.push(quote!(#name_str)); let help_child_placeholders = gen.help_child_placeholders; help_placeholders_detail.push( ( quote!( &format!( "{}{}", - aargvark:: style_lit(#name_str), + #name_str, aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) ) ), @@ -540,7 +538,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { #vark } fn generate_help_placeholder() -> String { - aargvark:: style_name(#help_placeholder) + aargvark:: style_link(#help_placeholder) } fn generate_help_section_suffix(text: &mut String, seen_sections: &mut std::collections::HashSet) { text.push_str( @@ -557,7 +555,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { if ! seen_sections.insert(#help_placeholder.to_string()) { return; } - text.push_str(& aargvark:: style_name(#help_placeholder)); + text.push_str(& aargvark:: style_section(#help_placeholder)); text.push_str(":"); < #ident >:: generate_help_section_suffix(text, seen_sections); } From 76c8398ee1ee899580d6a34d347fa4f34ffa653c Mon Sep 17 00:00:00 2001 From: andrew <> Date: Tue, 5 Sep 2023 21:40:57 +0900 Subject: [PATCH 12/58] Release --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 84669af..d530e5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.4" +version = "0.0.5" edition = "2021" license = "ISC" description = "Self-similar argument parsing" From 5717ed27e0753b7f19847a06c448d4598dec8ad4 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Tue, 5 Sep 2023 21:43:20 +0900 Subject: [PATCH 13/58] Bump macros too, rerelease to fix --- Cargo.toml | 4 ++-- src/proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d530e5b..88d05da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.5" +version = "0.0.6" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.2" } +aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.3" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" diff --git a/src/proc_macros/Cargo.toml b/src/proc_macros/Cargo.toml index 01976b4..9d565e0 100644 --- a/src/proc_macros/Cargo.toml +++ b/src/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.2" +version = "0.0.3" edition = "2021" license = "ISC" description = "Helper crate for aargvark" From 9f05418093803a086931d34e8ed627ce1ac393d1 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 27 Dec 2023 01:57:07 +0900 Subject: [PATCH 14/58] Output tweak, dep bump --- Cargo.toml | 4 ++-- src/lib.rs | 2 +- src/proc_macros/Cargo.toml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88d05da..d5a5151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.6" +version = "0.0.7" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -22,7 +22,7 @@ aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.3" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" -comfy-table = { version = "7.0.0", features = ["custom_styling"] } +comfy-table = { version = "7.1.0", features = ["custom_styling"] } url = { version = "2", optional = true } http = { version = "0", optional = true } serde = { version = "1", optional = true } diff --git a/src/lib.rs b/src/lib.rs index e0caa87..159a3b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,7 +190,7 @@ pub fn vark_explicit(command: String, args: Vec) -> T for e in state.errors { text.push_str("\n"); text.push_str(&format!(" * {}\n", e.err)); - text.push_str(&format!(" while parsing {:?} at\n", e.breadcrumbs)); + text.push_str(&format!(" while processing command {:?} at\n", e.breadcrumbs)); text.push_str(" "); text.push_str(&display_args); text.push_str("\n"); diff --git a/src/proc_macros/Cargo.toml b/src/proc_macros/Cargo.toml index 9d565e0..6651eb2 100644 --- a/src/proc_macros/Cargo.toml +++ b/src/proc_macros/Cargo.toml @@ -14,6 +14,6 @@ path = "mod.rs" [dependencies] convert_case = "0.6.0" -proc-macro2 = "1.0.47" -quote = "1.0.21" -syn = "1.0.103" +proc-macro2 = "1.0.71" +quote = "1.0.33" +syn = "1.0.109" From a5a927f0bd78a143827af8015441fc3821ec1091 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Fri, 12 Jan 2024 01:03:36 +0900 Subject: [PATCH 15/58] Bump http dep to 1, release --- Cargo.toml | 6 +++--- src/proc_macros/Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d5a5151..5c3367c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.7" +version = "0.0.8" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -24,6 +24,6 @@ serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" comfy-table = { version = "7.1.0", features = ["custom_styling"] } url = { version = "2", optional = true } -http = { version = "0", optional = true } +http = { version = "1", optional = true } serde = { version = "1", optional = true } -console = "0.15.7" +console = "0.15.8" diff --git a/src/proc_macros/Cargo.toml b/src/proc_macros/Cargo.toml index 6651eb2..07553c1 100644 --- a/src/proc_macros/Cargo.toml +++ b/src/proc_macros/Cargo.toml @@ -14,6 +14,6 @@ path = "mod.rs" [dependencies] convert_case = "0.6.0" -proc-macro2 = "1.0.71" -quote = "1.0.33" +proc-macro2 = "1.0.76" +quote = "1.0.35" syn = "1.0.109" From 11fb17bbed2a86c01edba8d00fc805f2f67cc169 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 29 Jan 2024 06:24:58 +0900 Subject: [PATCH 16/58] Rework help, customization --- Cargo.toml | 7 +- {src/proc_macros => proc_macros}/Cargo.toml | 3 +- proc_macros/mod.rs | 744 ++++++++++++++++++++ readme.md | 16 +- src/lib.rs | 573 +++++++++++---- src/proc_macros/mod.rs | 589 ---------------- tests/test.rs | 11 + 7 files changed, 1216 insertions(+), 727 deletions(-) rename {src/proc_macros => proc_macros}/Cargo.toml (90%) create mode 100644 proc_macros/mod.rs delete mode 100644 src/proc_macros/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 5c3367c..3963cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.0.8" +version = "0.1.0" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -9,7 +9,7 @@ repository = "https://github.com/andrewbaxter/aargvark" readme = "readme.md" [workspace] -members = ["src/proc_macros"] +members = ["proc_macros"] [features] default = [] @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "src/proc_macros", version = "0.0.3" } +aargvark_proc_macros = { path = "proc_macros", version = "0.0.4" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" @@ -27,3 +27,4 @@ url = { version = "2", optional = true } http = { version = "1", optional = true } serde = { version = "1", optional = true } console = "0.15.8" +textwrap = { version = "0.16.0", features = ["terminal_size"] } diff --git a/src/proc_macros/Cargo.toml b/proc_macros/Cargo.toml similarity index 90% rename from src/proc_macros/Cargo.toml rename to proc_macros/Cargo.toml index 07553c1..4f50aff 100644 --- a/src/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.3" +version = "0.0.4" edition = "2021" license = "ISC" description = "Helper crate for aargvark" @@ -14,6 +14,7 @@ path = "mod.rs" [dependencies] convert_case = "0.6.0" +genemichaels = "0.2.3" proc-macro2 = "1.0.76" quote = "1.0.35" syn = "1.0.109" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs new file mode 100644 index 0000000..f2c3edf --- /dev/null +++ b/proc_macros/mod.rs @@ -0,0 +1,744 @@ +use convert_case::{ + Case, + Casing, +}; +use proc_macro2::TokenStream; +use quote::{ + format_ident, + quote, + ToTokens, +}; +use syn::{ + self, + parse_macro_input, + Attribute, + DeriveInput, + Fields, + Lit, + Meta, + Type, +}; + +/// Break boundary - remove the footgunishness of using loop for this directly +macro_rules! bb{ + ($l: lifetime _; $($t: tt) *) => { + $l: loop { + #[allow(unreachable_code)] break { + $($t) * + }; + } + }; + ($($t: tt) *) => { + loop { + #[allow(unreachable_code)] break { + $($t) * + }; + } + }; +} + +#[derive(Default, Clone)] +struct VarkAttr { + help_break: bool, + literal: Option, + id: Option, +} + +fn get_vark(attrs: &Vec) -> VarkAttr { + let mut help_break = false; + let mut literal = None; + let mut id = None; + for a in attrs { + let Ok(m) = a.parse_meta() else { + continue; + }; + let Meta:: List(m) = m else { + continue; + }; + if &m.path.to_token_stream().to_string() != "vark" { + continue; + } + for m in m.nested { + let syn:: NestedMeta:: Meta(m) = m else { + continue; + }; + match &m { + Meta::Path(k) => { + match k.to_token_stream().to_string().as_str() { + "break" => { + help_break = true; + }, + i => { + panic!("Unexpected argument in `vark` attr: {:?}", i); + }, + } + }, + Meta::List(_) => { + panic!("Unexpected tokens in `vark` attr arguments: {:?}", m.to_token_stream()); + }, + Meta::NameValue(kv) => { + match kv.path.to_token_stream().to_string().as_str() { + "literal" => { + literal = Some(match &kv.lit { + Lit::Str(s) => s.value(), + l => panic!("`vark` `literal` argument must be a string, got {}", l.to_token_stream()), + }); + }, + "id" => { + id = Some(match &kv.lit { + Lit::Str(s) => s.value(), + l => panic!("`vark` `id` argument must be a string, got {}", l.to_token_stream()), + }); + }, + i => { + panic!("Unexpected argument in `vark` attr: {:?}", i); + }, + } + }, + } + } + } + return VarkAttr { + help_break: help_break, + literal: literal, + id: id, + }; +} + +fn get_docstr(attrs: &Vec) -> String { + let mut out = String::new(); + for attr in attrs { + if !attr.path.is_ident("doc") { + continue; + } + match attr.parse_meta().unwrap() { + syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(v), .. }) => { + out.push_str(&v.value()); + }, + _ => continue, + } + } + return out.trim().to_string(); +} + +struct GenRec { + vark: TokenStream, + help_pattern: TokenStream, +} + +fn gen_impl_type(ty: &Type, path: &str) -> GenRec { + match ty { + Type::Path(t) => { + return GenRec { + vark: quote!{ + < #t >:: vark(state) + }, + help_pattern: quote!{ + < #t as aargvark:: AargvarkTrait >:: build_help_pattern(state) + }, + }; + }, + Type::Tuple(t) => { + return gen_impl_unnamed( + path, + ty.to_token_stream(), + quote!(), + "TUPLE", + "", + 0, + t.elems.iter().map(|e| (VarkAttr::default(), String::new(), e)).collect::>().as_slice(), + ); + }, + _ => panic!("Unsupported type {} in {}", ty.to_token_stream(), path), + } +} + +fn gen_impl_unnamed( + path: &str, + parent_ident: TokenStream, + ident: TokenStream, + help_placeholder: &str, + help_docstr: &str, + subtype_index: usize, + d: &[(VarkAttr, String, &Type)], +) -> GenRec { + let mut parse_positional = vec![]; + let mut copy_fields = vec![]; + let mut help_fields = vec![]; + let mut help_field_patterns = vec![]; + let help_unit_transparent = d.len() == 1 && d[0].1.is_empty(); + for (i, (field_vark_attr, field_help_docstr, field_ty)) in d.iter().enumerate() { + let eof_code = if i == 0 { + quote!{ + break R::EOF; + } + } else { + quote!{ + break state.r_err(format!("Missing argument {}", #i)); + } + }; + let f_ident = format_ident!("v{}", i); + let gen = gen_impl_type(field_ty, path); + let vark = gen.vark; + let placeholder = field_vark_attr.id.clone().unwrap_or_else(|| { + let mut placeholder = vec![]; + let mut placeholder_i = i; + loop { + placeholder.push((('A' as u8) + (placeholder_i % 27) as u8) as char); + if placeholder_i < 27 { + break; + } + placeholder_i = placeholder_i / 27; + } + placeholder.reverse(); + String::from_iter(placeholder) + }); + let push_breadcrumb; + let pop_breadcrumb; + if !help_unit_transparent { + push_breadcrumb = quote!{ + state.breadcrumbs.push(#placeholder.to_string()); + }; + pop_breadcrumb = quote!{ + state.breadcrumbs.pop(); + }; + } else { + push_breadcrumb = quote!(); + pop_breadcrumb = quote!(); + } + parse_positional.push(quote!{ + #push_breadcrumb + //. . + let r = #vark; + #pop_breadcrumb + //. . + let #f_ident = match r { + R:: Ok(v) => { + v + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + #eof_code + } + }; + }); + copy_fields.push(f_ident.to_token_stream()); + let field_help_pattern = gen.help_pattern; + help_fields.push(quote!{ + struct_.fields.push(aargvark:: HelpField { + id: #placeholder.to_string(), + pattern: #field_help_pattern, + description: #field_help_docstr.to_string(), + }); + }); + help_field_patterns.push(field_help_pattern); + } + return GenRec { + vark: quote!{ + loop { + #(#parse_positional) * break state.r_ok(#ident(#(#copy_fields), *)); + } + }, + help_pattern: if d.is_empty() { + quote!{ + aargvark::HelpPattern(vec![]) + } + } else if help_unit_transparent { + help_field_patterns.pop().unwrap() + } else { + quote!{ + { + let( + key, + struct_ + ) = state.add_struct( + std:: any:: TypeId:: of::< #parent_ident >(), + #subtype_index, + #help_placeholder, + #help_docstr + ); + let mut struct_ = struct_.borrow_mut(); + #(#help_fields) * + //. . + aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]) + } + } + }, + }; +} + +fn gen_impl_struct( + parent_ident: TokenStream, + ident: TokenStream, + help_placeholder: &str, + help_docstr: &str, + subtype_index: usize, + d: &Fields, +) -> GenRec { + match d { + Fields::Named(d) => { + let mut help_fields = vec![]; + let mut partial_help_fields = vec![]; + let mut vark_optional_fields = vec![]; + let mut vark_parse_optional_cases = vec![]; + let mut vark_parse_positional = vec![]; + let mut vark_copy_fields = vec![]; + let mut required_i = 0usize; + 'next_field: for (i, f) in d.named.iter().enumerate() { + let field_vark_attr = get_vark(&f.attrs); + let field_help_docstr = get_docstr(&f.attrs); + let field_ident = f.ident.as_ref().expect("Named field missing name"); + let f_local_ident = format_ident!("v{}", i); + + // If an optional field, generate opt parsers and skip positional parsing + bb!{ + 'not_optional _; + let ty; + { + let Type:: Path(t) =& f.ty else { + break 'not_optional; + }; + if t.qself.is_some() { + break 'not_optional; + } + if t.path.leading_colon.is_some() { + break 'not_optional; + } + if t.path.segments.len() != 1 { + break 'not_optional; + } + let s = t.path.segments.first().unwrap(); + if &s.ident.to_string() != "Option" { + break 'not_optional; + } + let syn:: PathArguments:: AngleBracketed(a) =& s.arguments else { + break 'not_optional; + }; + if a.args.len() != 1 { + break 'not_optional; + } + let syn:: GenericArgument:: Type(t) =& a.args[0] else { + break 'not_optional; + }; + ty = t; + } + let flag = + format!( + "--{}", + field_vark_attr + .literal + .clone() + .unwrap_or_else(|| field_ident.to_string().to_case(Case::Kebab)) + ); + vark_optional_fields.push(quote!{ + #field_ident: Option < #ty >, + }); + vark_copy_fields.push(quote!{ + #field_ident: optional.#field_ident + }); + let gen = gen_impl_type(ty, &field_ident.to_string()); + let vark = gen.vark; + vark_parse_optional_cases.push(quote!{ + #flag => { + if optional.#field_ident.is_some() { + return state.r_err(format!("The argument {} was already specified", #flag)); + } + state.consume(); + let #f_local_ident = match #vark { + R:: Ok(v) => { + v + }, + R:: Err => { + return R::Err; + }, + R:: EOF => { + return state.r_err(format!("Missing argument for {}", #flag)); + } + }; + optional.#field_ident = Some(#f_local_ident); + return R::Ok(true); + } + }); + let field_help_pattern = gen.help_pattern; + let help_field = quote!{ + aargvark:: HelpOptionalField { + literal: #flag.to_string(), + pattern: #field_help_pattern, + description: #field_help_docstr.to_string(), + } + }; + help_fields.push(quote!{ + struct_.optional_fields.push(#help_field); + }); + partial_help_fields.push(quote!{ + if optional.#field_ident.is_none() { + optional_fields.push(#help_field); + } + }); + continue 'next_field; + }; + + // Positional/required parsing + let field_help_placeholder = field_vark_attr.id.unwrap_or_else(|| field_ident.to_string().to_case(Case::UpperKebab)); + let eof_code = if required_i == 0 { + quote!{ + break R::EOF; + } + } else { + quote!{ + break state.r_err(format!("Missing argument {}", #field_help_placeholder)); + } + }; + let gen = gen_impl_type(&f.ty, &ident.to_string()); + let vark = gen.vark; + let field_help_pattern = gen.help_pattern; + vark_parse_positional.push(quote!{ + let #f_local_ident = loop { + if match state.peek() { + PeekR:: None => false, + PeekR:: Help => { + aargvark:: show_help_and_exit(state, | state | { + return aargvark:: HelpPartialProduction { + description: #field_help_docstr.to_string(), + content: build_partial_help(state, #required_i, &optional), + }; + }); + }, + PeekR:: Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { + R:: Ok(v) => { + v + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + unreachable!(); + } + }, + } + { + continue; + } + break #vark; + }; + let #f_local_ident = match #f_local_ident { + R:: Ok(v) => { + v + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + #eof_code + } + }; + }); + vark_copy_fields.push(quote!{ + #field_ident: #f_local_ident + }); + let help_field = quote!{ + aargvark:: HelpField { + id: #field_help_placeholder.to_string(), + pattern: #field_help_pattern, + description: #field_help_docstr.to_string(), + } + }; + help_fields.push(quote!{ + struct_.fields.push(#help_field); + }); + partial_help_fields.push(quote!{ + if required_i <= #required_i { + fields.push(#help_field); + } + }); + required_i += 1; + } + + // Assemble code + let vark = quote!{ + { + loop { + #[derive(Default)] struct Optional { + #(#vark_optional_fields) * + } + let mut optional = Optional::default(); + fn parse_optional( + optional: &mut Optional, + state: &mut aargvark::VarkState, + s: String + ) -> R < bool > { + match s.as_str() { + #(#vark_parse_optional_cases) * + //. . + _ => return R:: Ok(false), + }; + } + fn build_partial_help( + state: &mut aargvark::HelpState, + required_i: usize, + optional: &Optional + ) -> aargvark:: HelpPartialContent { + let mut fields = vec![]; + let mut optional_fields = vec![]; + #(#partial_help_fields) * + //. . + return aargvark:: HelpPartialContent:: struct_(fields, optional_fields); + } + #(#vark_parse_positional) * + // Parse any remaining optional args + let opt_search_res = loop { + match state.peek() { + PeekR:: None => { + break state.r_ok(()); + }, + PeekR:: Help => { + aargvark:: show_help_and_exit(state, | state | { + return aargvark:: HelpPartialProduction { + description: #help_docstr.to_string(), + content: build_partial_help(state, #required_i, &optional), + }; + }); + }, + PeekR:: Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { + R:: Ok(v) => { + if !v { + break state.r_ok(()); + } + }, + R:: Err => { + break R::Err; + }, + R:: EOF => { + unreachable!(); + }, + }, + }; + }; + match opt_search_res { + R::Ok(()) => { }, + R::Err => { + break R::Err; + }, + R::EOF => { + unreachable!(); + }, + }; + // Build obj + return + break state.r_ok(#ident { + #(#vark_copy_fields), + * + }); + } + } + }; + return GenRec { + vark: vark, + help_pattern: quote!{ + { + let( + key, + struct_ + ) = state.add_struct( + std::any::TypeId::of::(), + #subtype_index, + #help_placeholder, + #help_docstr + ); + let mut struct_ = struct_.borrow_mut(); + #(#help_fields) * + //. . + aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]) + } + }, + }; + }, + Fields::Unnamed(d) => { + return gen_impl_unnamed( + &ident.to_string(), + parent_ident, + ident.to_token_stream(), + help_placeholder, + help_docstr, + subtype_index, + d + .unnamed + .iter() + .map(|f| (get_vark(&f.attrs), get_docstr(&f.attrs), &f.ty)) + .collect::>() + .as_slice(), + ); + }, + Fields::Unit => { + return GenRec { + vark: quote!{ + state.r_ok(#ident) + }, + help_pattern: quote!{ + aargvark::HelpPattern(vec![]) + }, + }; + }, + }; +} + +fn gen_impl(ast: syn::DeriveInput) -> TokenStream { + let ident = &ast.ident; + let vark_attr = get_vark(&ast.attrs); + let help_docstr = get_docstr(&ast.attrs); + let help_placeholder = vark_attr.id.unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); + let vark; + let help_build; + match &ast.data { + syn::Data::Struct(d) => { + let gen = + gen_impl_struct( + ast.ident.to_token_stream(), + ast.ident.to_token_stream(), + &help_placeholder, + &help_docstr, + 0, + &d.fields, + ); + vark = gen.vark; + help_build = gen.help_pattern; + }, + syn::Data::Enum(d) => { + let mut all_tags = vec![]; + let mut vark_cases = vec![]; + let mut help_variants = vec![]; + for (subtype_index, v) in d.variants.iter().enumerate() { + let variant_vark_attr = get_vark(&v.attrs); + let variant_help_docstr = get_docstr(&v.attrs); + let variant_ident = &v.ident; + let name_str = + variant_vark_attr.literal.unwrap_or_else(|| variant_ident.to_string().to_case(Case::Kebab)); + let gen = + gen_impl_struct( + ident.to_token_stream(), + quote!(#ident:: #variant_ident), + &name_str, + "", + subtype_index + 1, + &v.fields, + ); + all_tags.push(name_str.clone()); + let vark = gen.vark; + let partial_help_variant_pattern = gen.help_pattern; + vark_cases.push(quote!{ + #name_str => { + state.consume(); + state.breadcrumbs.push(#name_str.to_string()); + let v = #vark; + state.breadcrumbs.pop(); + v + } + }); + let help_variant_pattern; + if vark_attr.help_break { + help_variant_pattern = quote!(aargvark::HelpPattern(vec![])); + } else { + help_variant_pattern = partial_help_variant_pattern; + } + help_variants.push(quote!{ + variants.push(aargvark:: HelpVariant { + literal: #name_str.to_string(), + pattern: #help_variant_pattern, + description: #variant_help_docstr.to_string(), + }); + }); + } + vark = quote!{ + { + let tag = match state.peek() { + PeekR:: None => return R:: EOF, + PeekR:: Help => { + aargvark:: show_help_and_exit(state, | state | { + let mut variants = vec![]; + #(#help_variants) * + //. . + return aargvark:: HelpPartialProduction { + description: #help_docstr.to_string(), + content: aargvark::HelpPartialContent::enum_(variants), + }; + }); + }, + PeekR:: Ok(s) => s, + }; + match tag { + #(#vark_cases) * _ => { + state.r_err( + format!("Unrecognized variant {}. Choices are {:?}", tag, vec![#(#all_tags), *]), + ) + } + } + } + }; + help_build = quote!{ + let( + key, + variants + ) = state.add_enum(std::any::TypeId::of::(), 0, #help_placeholder, #help_docstr); + let mut variants = variants.borrow_mut(); + #(#help_variants) * + //. . + return aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]); + }; + }, + syn::Data::Union(_) => panic!("Union not supported"), + }; + return quote!{ + impl aargvark:: AargvarkTrait for #ident { + fn vark(state: &mut aargvark::VarkState) -> aargvark:: R < #ident > { + use aargvark::R; + use aargvark::PeekR; + #vark + } + fn build_help_pattern(state: &mut aargvark::HelpState) -> aargvark:: HelpPattern { + #help_build + } + } + }; +} + +#[proc_macro_derive(Aargvark, attributes(vark))] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + return gen_impl(parse_macro_input!(input as DeriveInput)).into(); +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use genemichaels::FormatConfig; + use proc_macro2::TokenStream; + use quote::quote; + use crate::gen_impl; + + #[test] + fn dump() { + let got = gen_impl(syn::parse2(quote!{ + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + b: Option<()>, + } + }).unwrap()); + let cfg = FormatConfig::default(); + let mut s = + [&got].into_iter().map(|s| genemichaels::format_str(&s.to_string(), &cfg)).collect::>(); + let got = s.remove(0).expect(&format!("Failed to format got code:\n{}", got.to_string())).rendered; + panic!("{}", got); + } + + #[test] + fn newtype_string() { + assert_eq!( + gen_impl( + syn::parse2(TokenStream::from_str("enum Yol { + ToqQuol, + }").unwrap()).unwrap(), + ).to_string(), + quote!().to_string() + ); + } +} diff --git a/readme.md b/readme.md index 0c4aea2..028fdd8 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,7 @@ Why this and not Clap? Why not this? - Some command line parsing conventions were discarded in order to simplify and maintain self-similarity. A lot of command line conventions are inconsistent or break down as you nest things, after all. -- There's less customizability. Some tricks (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. Other things just haven't been implemented yet due to lack of time. +- Quirky CLI parsing generally isn't supported: Some tricks (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. (Other things just haven't been implemented yet due to lack of time) - Alpha # Conventions and usage @@ -76,3 +76,17 @@ Some additional wrappers are provided for automatically loading (and parsing) fi - `AargvarkYaml` requires feature `serde_yaml` To parse your own types, implement `AargvarkTrait`, or if your type takes a single string argument you can implement `AargvarkFromStr`. + +# Advanced usage + +- Prevent recursion in help + + Add `#[vark(break)]` to a type to prevent recursing into any of the children. This is useful for subcommand enums - attach this to the enum and it will list the arguments but not the arguments' arguments (unless you do `-h` after specifying one on the command line). + +- Rename enum variants and option keys + + Add `#[vark(name="x")]` to a field. + +- Change placeholder text + + Add `#[vark(id="x")]` to a field. diff --git a/src/lib.rs b/src/lib.rs index 159a3b1..3e35eb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,20 @@ use std::{ + any::TypeId, + cell::RefCell, + collections::{ + HashMap, + HashSet, + }, env::args, - process::exit, + ffi::{ + OsString, + }, + fs, + hash::Hash, + io::{ + stdin, + Read, + }, net::{ SocketAddr, SocketAddrV4, @@ -10,16 +24,8 @@ use std::{ Ipv6Addr, }, path::PathBuf, - io::{ - stdin, - Read, - }, - fs, - collections::HashSet, - hash::Hash, - ffi::{ - OsString, - }, + process::exit, + rc::Rc, }; pub use aargvark_proc_macros::Aargvark; use comfy_table::Cell; @@ -35,7 +41,6 @@ pub enum R { EOF, Err, Ok(T), - Help, } #[doc(hidden)] @@ -51,72 +56,6 @@ pub struct VarkState { i: usize, pub breadcrumbs: Vec, errors: Vec, - pub simple_enum_root: bool, -} - -#[doc(hidden)] -pub fn join_strs(sep: &str, v: &[&str]) -> String { - v.join(sep) -} - -pub fn style_type(l: &str) -> String { - console::Style::new().magenta().apply_to(l).to_string() -} - -pub fn style_link(l: &str) -> String { - console::Style::new().blue().apply_to(l).to_string() -} - -pub fn style_section(l: &str) -> String { - console::Style::new().bold().apply_to(l).to_string() -} - -#[doc(hidden)] -pub fn generate_help_section_usage_prefix(state: &VarkState) -> (String, HashSet) { - let mut text = style_section("Usage: "); - for (i, s) in state.breadcrumbs.iter().enumerate() { - if i > 0 { - text.push_str(" "); - } - text.push_str(s); - } - return (text, HashSet::new()); -} - -#[doc(hidden)] -pub fn generate_help_section_suffix( - docstr: &str, - placeholders: Vec<&str>, - placeholders_detail: Vec<(&str, &str)>, - joiner: &str, -) -> String { - let mut out = String::new(); - out.push_str(" "); - for (i, p) in placeholders.iter().enumerate() { - if i > 0 { - out.push_str(joiner); - } - out.push_str(p); - } - out.push_str("\n\n"); - if !docstr.is_empty() { - out.push_str(docstr); - out.push_str("\n\n"); - } - let mut table = comfy_table::Table::new(); - table.load_preset(comfy_table::presets::NOTHING); - table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); - for (placeholder, docstr) in placeholders_detail { - // One space due to invisible border - table.add_row(vec![comfy_table::Cell::new(format!(" {}", placeholder)), Cell::new(docstr)]); - } - table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { - lower: comfy_table::Width::Percentage(20), - upper: comfy_table::Width::Percentage(60), - }]); - out.push_str(&table.to_string()); - out.push_str("\n\n"); - return out; } impl VarkState { @@ -165,7 +104,6 @@ pub fn vark_explicit(command: String, args: Vec) -> T i: 0, breadcrumbs: vec![command], errors: vec![], - simple_enum_root: true, }; match T::vark(&mut state) { R::EOF => { @@ -211,12 +149,6 @@ pub fn vark_explicit(command: String, args: Vec) -> T } return v; }, - R::Help => { - let (mut text, mut seen_sections) = generate_help_section_usage_prefix(&state); - T::generate_help_section_suffix(&mut text, &mut seen_sections); - eprintln!("{}\n", text.trim()); - exit(0); - }, } } @@ -231,23 +163,28 @@ pub fn vark() -> T { /// parsable enums/structs. pub trait AargvarkTrait: Sized { fn vark(state: &mut VarkState) -> R; - fn generate_help_placeholder() -> String; - fn generate_help_section(text: &mut String, seen_sections: &mut HashSet); - fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet); + fn build_help_pattern(state: &mut HelpState) -> HelpPattern; } /// A helper enum, providing a simpler interface for types that can be parsed from /// a single primitive string. pub trait AargvarkFromStr: Sized { fn from_str(s: &str) -> Result; - fn generate_help_placeholder() -> String; + fn build_help_pattern(state: &mut HelpState) -> HelpPattern; } impl AargvarkTrait for T { fn vark(state: &mut VarkState) -> R { let s = match state.peek() { PeekR::None => return R::EOF, - PeekR::Help => return R::Help, + PeekR::Help => { + show_help_and_exit(state, |state| { + return HelpPartialProduction { + description: "".to_string(), + content: HelpPartialContent::Pattern(::build_help_pattern(state)), + }; + }); + }, PeekR::Ok(s) => s, }; match T::from_str(s) { @@ -259,24 +196,20 @@ impl AargvarkTrait for T { } } - fn generate_help_placeholder() -> String { - T::generate_help_placeholder() + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return ::build_help_pattern(state); } - - fn generate_help_section(_text: &mut String, _seen_sections: &mut HashSet) { } - - fn generate_help_section_suffix(_text: &mut String, _seen_sections: &mut HashSet) { } } macro_rules! auto_from_str{ ($placeholder: literal, $t: ty) => { impl AargvarkFromStr for $t { fn from_str(s: &str) -> Result { - ::from_str(s).map_err(|e| e.to_string()) + return ::from_str(s).map_err(|e| e.to_string()); } - fn generate_help_placeholder() -> String { - style_type(&format!("<{}>", $placeholder)) + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement:: Type($placeholder.to_string())]); } } }; @@ -325,18 +258,23 @@ auto_from_str!("PATH", PathBuf); #[cfg(feature = "http_types")] auto_from_str!("URI", http::Uri); -impl AargvarkTrait for bool { - fn vark(state: &mut VarkState) -> R { - return state.r_ok(true); +impl AargvarkFromStr for bool { + fn from_str(s: &str) -> Result { + return ::from_str(s).map_err(|e| e.to_string()); } - fn generate_help_placeholder() -> String { - return style_type(""); + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + HelpPattern(vec![HelpPatternElement::Literal("true".to_string())]), + HelpPattern(vec![HelpPatternElement::Literal("false".to_string())]) + ], + ) + ], + ); } - - fn generate_help_section(_text: &mut String, _seen_sections: &mut HashSet) { } - - fn generate_help_section_suffix(_text: &mut String, _seen_sections: &mut HashSet) { } } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -373,8 +311,17 @@ impl AargvarkFromStr for AargvarkFile { } } - fn generate_help_placeholder() -> String { - return style_type("|-"); + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + ::build_help_pattern(state), + HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) + ], + ) + ], + ); } } @@ -399,8 +346,17 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { }; } - fn generate_help_placeholder() -> String { - format!("<{}>|{}", style_type("PATH"), style_type("-")) + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + ::build_help_pattern(state), + HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) + ], + ) + ], + ); } } @@ -435,8 +391,17 @@ impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { }; } - fn generate_help_placeholder() -> String { - format!("<{}>|{}", style_type("PATH"), style_type("-")) + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + ::build_help_pattern(state), + HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) + ], + ) + ], + ); } } @@ -452,7 +417,6 @@ impl Clone for AargvarkYaml { #[doc(hidden)] pub fn vark_from_iter>(state: &mut VarkState) -> R { - state.simple_enum_root = false; let mut out = vec![]; let mut rewind_to = state.position(); let mut i = 0usize; @@ -462,9 +426,6 @@ pub fn vark_from_iter>(state: &mut VarkStat let r = T::vark(state); state.breadcrumbs.pop(); match r { - R::Help => { - return R::Help; - }, R::Ok(v) => { out.push(v); rewind_to = state.position(); @@ -482,33 +443,379 @@ impl AargvarkTrait for Vec { return vark_from_iter(state); } - fn generate_help_placeholder() -> String { - return format!("{}{}", T::generate_help_placeholder(), style_link("[ ...]")); + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Array(T::build_help_pattern(state))]); } +} - fn generate_help_section(text: &mut String, seen_sections: &mut HashSet) { - Self::generate_help_section_suffix(text, seen_sections); +impl AargvarkTrait for HashSet { + fn vark(state: &mut VarkState) -> R { + return vark_from_iter(state); } - fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet) { - T::generate_help_section(text, seen_sections); + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Array(T::build_help_pattern(state))]); } } -impl AargvarkTrait for HashSet { - fn vark(state: &mut VarkState) -> R { - return vark_from_iter(state); +fn style_usage(s: impl AsRef) -> String { + return s.as_ref().to_string(); +} + +fn style_description(s: impl AsRef) -> String { + return s.as_ref().to_string(); +} + +fn style_id(s: impl AsRef) -> String { + return console::Style::new().blue().dim().apply_to(s.as_ref()).to_string(); +} + +fn style_type(s: impl AsRef) -> String { + return console::Style::new().magenta().apply_to(s.as_ref()).to_string(); +} + +fn style_logical(s: impl AsRef) -> String { + return console::Style::new().dim().apply_to(s.as_ref()).to_string(); +} + +fn style_literal(s: impl AsRef) -> String { + return console::Style::new().bold().apply_to(s.as_ref()).to_string(); +} + +#[derive(Hash, PartialEq, Eq, Clone, Copy)] +pub struct HelpProductionKey { + type_id: TypeId, + variant: usize, +} + +struct HelpProduction { + id: String, + description: String, + content: HelpProductionType, +} + +pub enum HelpPartialContent { + Pattern(HelpPattern), + Production(HelpProductionType), +} + +impl HelpPartialContent { + pub fn struct_(fields: Vec, optional_fields: Vec) -> Self { + return HelpPartialContent::Production( + HelpProductionType::Struct(Rc::new(RefCell::new(HelpProductionTypeStruct { + fields: fields, + optional_fields: optional_fields, + }))), + ); + } + + pub fn enum_(variants: Vec) -> Self { + return HelpPartialContent::Production(HelpProductionType::Enum(Rc::new(RefCell::new(variants)))); + } +} + +pub struct HelpPartialProduction { + pub description: String, + pub content: HelpPartialContent, +} + +pub enum HelpProductionType { + Struct(Rc>), + Enum(Rc>>), +} + +pub struct HelpProductionTypeStruct { + pub fields: Vec, + pub optional_fields: Vec, +} + +pub struct HelpField { + pub id: String, + pub pattern: HelpPattern, + pub description: String, +} + +pub struct HelpOptionalField { + pub literal: String, + pub pattern: HelpPattern, + pub description: String, +} + +pub struct HelpVariant { + pub literal: String, + pub pattern: HelpPattern, + pub description: String, +} + +#[derive(Clone)] +pub struct HelpPattern(pub Vec); + +impl HelpPattern { + fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { + let mut out = String::new(); + for (i, e) in self.0.iter().enumerate() { + if i > 0 { + out.push_str(" "); + } + out.push_str(&e.render(stack, state)); + } + return out; + } +} + +#[derive(Clone)] +pub enum HelpPatternElement { + Literal(String), + Type(String), + Reference(HelpProductionKey), + Option(HelpPattern), + Array(HelpPattern), + Variant(Vec), +} + +impl HelpPatternElement { + fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { + match self { + HelpPatternElement::Literal(l) => return style_literal(l), + HelpPatternElement::Type(i) => return style_type(format!("<{}>", i)), + HelpPatternElement::Reference(i) => { + let production = state.productions.get(i).unwrap(); + stack.push((*i, production.clone())); + return style_id(production.id.as_str()) + }, + HelpPatternElement::Option(i) => return format!( + "{}{}{}", + style_logical("["), + i.render(stack, state), + style_logical("]") + ), + HelpPatternElement::Array(i) => return format!("{}{}", i.render(stack, state), style_logical("[ ...]")), + HelpPatternElement::Variant(i) => return i + .iter() + .map(|x| x.render(stack, state)) + .collect::>() + .join(&style_logical(" | ")), + } } +} + +pub struct HelpState { + // Write during building + name_counter: HashMap, + // Write during building, read during rendering + productions: HashMap>, +} - fn generate_help_placeholder() -> String { - return format!("{}{}", T::generate_help_placeholder(), style_link("[ ...]")); +impl HelpState { + fn add( + &mut self, + type_id: TypeId, + type_id_variant: usize, + id: impl ToString, + description: impl ToString, + content: HelpProductionType, + ) -> HelpProductionKey { + let mut id = id.to_string(); + let count = *self.name_counter.entry(id.clone()).and_modify(|x| *x += 1).or_insert(1); + if count > 1 { + id = format!("{} ({})", id, count); + } + let key = HelpProductionKey { + type_id: type_id, + variant: type_id_variant, + }; + self.productions.insert(key, Rc::new(HelpProduction { + id: id, + description: description.to_string(), + content: content, + })); + return key; + } + + pub fn add_struct( + &mut self, + type_id: TypeId, + type_id_variant: usize, + id: impl ToString, + description: impl ToString, + ) -> (HelpProductionKey, Rc>) { + let out = Rc::new(RefCell::new(HelpProductionTypeStruct { + fields: vec![], + optional_fields: vec![], + })); + let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Struct(out.clone())); + return (key, out); + } + + pub fn add_enum( + &mut self, + type_id: TypeId, + type_id_variant: usize, + id: impl ToString, + description: impl ToString, + ) -> (HelpProductionKey, Rc>>) { + let out = Rc::new(RefCell::new(vec![])); + let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Enum(out.clone())); + return (key, out); } +} - fn generate_help_section(text: &mut String, seen_sections: &mut HashSet) { - Self::generate_help_section_suffix(text, seen_sections); +pub fn show_help_and_exit< + F: FnOnce(&mut HelpState) -> HelpPartialProduction, +>(state: &VarkState, build_root: F) -> ! { + fn format_desc(out: &mut String, desc: &str) { + if !desc.is_empty() { + out.push_str( + &style_description( + textwrap::wrap( + desc, + &textwrap::Options::with_termwidth().initial_indent(" ").subsequent_indent(" "), + ).join("\n"), + ), + ); + out.push_str("\n\n"); + } } - fn generate_help_section_suffix(text: &mut String, seen_sections: &mut HashSet) { - T::generate_help_section(text, seen_sections); + fn format_pattern(out: &mut String, content: &HelpProductionType) { + match content { + HelpProductionType::Struct(struct_) => { + let struct_ = struct_.borrow(); + for f in &struct_.fields { + out.push_str(" "); + out.push_str(&style_id(&f.id)); + } + if !struct_.optional_fields.is_empty() { + out.push_str(" "); + out.push_str(&style_logical("[ ...OPT]")); + } + }, + HelpProductionType::Enum(fields) => { + for (i, f) in fields.borrow().iter().enumerate() { + if i > 0 { + out.push_str(" |"); + } + out.push_str(" "); + out.push_str(&style_literal(&f.literal)); + } + }, + } } + + fn format_content( + out: &mut String, + stack: &mut Vec<(HelpProductionKey, Rc)>, + help_state: &HelpState, + content: &HelpProductionType, + ) { + let mut table = comfy_table::Table::new(); + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + match content { + HelpProductionType::Struct(struct_) => { + let struct_ = struct_.borrow(); + for f in &struct_.fields { + table.add_row( + vec![ + comfy_table::Cell::new( + format!(" {}: {}", style_id(&f.id), f.pattern.render(stack, help_state)), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + for f in &struct_.optional_fields { + let mut elems = vec![HelpPatternElement::Literal(f.literal.clone())]; + elems.extend(f.pattern.0.clone()); + table.add_row( + vec![ + comfy_table::Cell::new( + format!( + " {}", + HelpPatternElement::Option(HelpPattern(elems)).render(stack, help_state) + ), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + }, + HelpProductionType::Enum(fields) => { + for f in &*fields.borrow() { + table.add_row( + vec![ + comfy_table::Cell::new( + format!(" {} {}", style_literal(&f.literal), f.pattern.render(stack, help_state)), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + }, + } + table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { + lower: comfy_table::Width::Percentage(20), + upper: comfy_table::Width::Percentage(60), + }]); + out.push_str(&table.to_string()); + out.push_str("\n\n"); + } + + let mut help_state = HelpState { + name_counter: HashMap::new(), + productions: HashMap::new(), + }; + let mut stack = Vec::<(HelpProductionKey, Rc)>::new(); + let mut seen_productions = HashSet::::new(); + let partial = build_root(&mut help_state); + + // Write initial partial production + let mut out = style_usage("Usage:"); + for s in &state.breadcrumbs { + out.push_str(" "); + out.push_str(&style_literal(s)); + } + out.push_str(" >"); + let mut temp_stack = vec![]; + match &partial.content { + HelpPartialContent::Pattern(p) => { + if !p.0.is_empty() { + out.push_str(" "); + out.push_str(&p.render(&mut temp_stack, &help_state)); + } + }, + HelpPartialContent::Production(content) => { + format_pattern(&mut out, content); + }, + } + out.push_str("\n\n"); + format_desc(&mut out, &partial.description); + match &partial.content { + HelpPartialContent::Pattern(_) => { + out.push_str("\n\n"); + }, + HelpPartialContent::Production(content) => { + format_content(&mut out, &mut temp_stack, &mut help_state, content); + }, + } + temp_stack.reverse(); + stack.extend(temp_stack); + + // Recurse productions + while let Some((key, top)) = stack.pop() { + if !seen_productions.insert(key) { + continue; + } + out.push_str(&style_id(&top.id)); + out.push_str(":"); + format_pattern(&mut out, &top.content); + out.push_str("\n\n"); + format_desc(&mut out, &top.description); + let mut temp_stack = vec![]; + format_content(&mut out, &mut temp_stack, &mut help_state, &top.content); + temp_stack.reverse(); + stack.extend(temp_stack); + } + print!("{}", out); + exit(0); } diff --git a/src/proc_macros/mod.rs b/src/proc_macros/mod.rs deleted file mode 100644 index a2ff614..0000000 --- a/src/proc_macros/mod.rs +++ /dev/null @@ -1,589 +0,0 @@ -use std::collections::HashSet; -use convert_case::{ - Case, - Casing, -}; -use proc_macro2::TokenStream; -use quote::{ - format_ident, - quote, - ToTokens, -}; -use syn::{ - self, - parse_macro_input, - Type, - Fields, - Field, - DeriveInput, - Attribute, -}; - -fn get_docstr(attrs: &Vec) -> String { - let mut out = String::new(); - for attr in attrs { - if !attr.path.is_ident("doc") { - continue; - } - match attr.parse_meta().unwrap() { - syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(v), .. }) => { - out.push_str(&v.value()); - }, - _ => continue, - } - } - return out.trim().to_string(); -} - -struct GenRec { - vark: TokenStream, - help_child_placeholders: Vec, - help_child_placeholders_detail: Vec<(TokenStream, String)>, - help_recurse: Vec, -} - -fn gen_impl_type(ty: &Type, path: &str) -> GenRec { - match ty { - Type::Path(t) => { - return GenRec { - vark: quote!{ - < #t >:: vark(state) - }, - help_child_placeholders: vec![quote!{ - &< #t >:: generate_help_placeholder() - }], - help_child_placeholders_detail: vec![], - help_recurse: vec![t.clone()], - }; - }, - Type::Tuple(t) => { - return gen_impl_unnamed( - path, - quote!(), - t.elems.iter().map(|e| (String::new(), e)).collect::>().as_slice(), - ); - }, - _ => panic!("Unsupported type {} in {}", ty.to_token_stream(), path), - } -} - -fn gen_impl_unnamed(path: &str, ident: TokenStream, d: &[(String, &Type)]) -> GenRec { - let mut parse_positional = vec![]; - let mut copy_fields = vec![]; - let mut help_child_placeholders = vec![]; - let mut help_child_placeholder_detail = vec![]; - let mut help_recurse = vec![]; - for (i, ty) in d.iter().enumerate() { - let eof_code = if i == 0 { - quote!{ - break R::EOF; - } - } else { - quote!{ - break state.r_err(format!("Missing argument {}", #i)); - } - }; - let f_ident = format_ident!("v{}", i); - let gen = gen_impl_type(ty.1, path); - help_child_placeholders.extend(gen.help_child_placeholders); - help_child_placeholder_detail.extend(gen.help_child_placeholders_detail); - help_recurse.extend(gen.help_recurse); - let vark = gen.vark; - parse_positional.push(quote!{ - state.breadcrumbs.push(format!("{{{}}}", #i)); - let r = #vark; - state.breadcrumbs.pop(); - let #f_ident = match r { - R:: Help => { - break R::Help; - }, - R:: Ok(v) => { - v - }, - R:: Err => { - break R::Err; - }, - R:: EOF => { - #eof_code - } - }; - }); - copy_fields.push(f_ident.to_token_stream()); - } - return GenRec { - vark: quote!{ - loop { - #(#parse_positional) * break state.r_ok(#ident(#(#copy_fields), *)); - } - }, - help_child_placeholders: help_child_placeholders, - help_child_placeholders_detail: help_child_placeholder_detail, - help_recurse: help_recurse, - }; -} - -fn gen_impl_struct(ident: TokenStream, d: &Fields) -> GenRec { - match d { - Fields::Named(d) => { - let mut help_placeholders = vec![]; - let mut help_placeholders_detail = vec![]; - let mut help_recurse = vec![]; - let mut vark_optional_fields = vec![]; - let mut vark_parse_optional_cases = vec![]; - let mut vark_parse_positional = vec![]; - let mut vark_copy_fields = vec![]; - let mut required_i = 0usize; - for (i, f) in d.named.iter().enumerate() { - let field_ident = f.ident.as_ref().expect("Named field missing name"); - let f_local_ident = format_ident!("v{}", i); - let help_docstr = get_docstr(&f.attrs); - - // If an optional field, generate opt parsers and skip positional parsing - if |f: &Field| -> bool { - // Confirm optional - let ty = match &f.ty { - Type::Path(t) => { - if t.qself.is_some() { - return false; - } - if t.path.leading_colon.is_some() { - return false; - } - if t.path.segments.len() != 1 { - return false; - } - let s = t.path.segments.first().unwrap(); - if &s.ident.to_string() != "Option" { - return false; - } - match &s.arguments { - syn::PathArguments::None => return false, - syn::PathArguments::AngleBracketed(a) => { - if a.args.len() != 1 { - return false; - } - match a.args.first().unwrap() { - syn::GenericArgument::Type(t) => t, - _ => return false, - } - }, - syn::PathArguments::Parenthesized(_) => return false, - } - }, - _ => return false, - }; - - // Do generation - let flag = format!("--{}", field_ident.to_string().to_case(Case::Kebab)); - let help_docstr = get_docstr(&f.attrs); - vark_optional_fields.push(quote!{ - #field_ident: Option < #ty >, - }); - vark_copy_fields.push(quote!{ - #field_ident: optional.#field_ident - }); - let gen = gen_impl_type(ty, &field_ident.to_string()); - help_recurse.extend(gen.help_recurse); - let help_child_placeholders = gen.help_child_placeholders; - help_placeholders_detail.push((quote!{ - &format!( - "{}{}", - #flag, - aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) - ) - }, help_docstr)); - let vark = gen.vark; - vark_parse_optional_cases.push(quote!{ - #flag => { - if optional.#field_ident.is_some() { - return state.r_err(format!("The argument {} was already specified", #flag)); - } - state.consume(); - let #f_local_ident = match #vark { - R:: Help => { - return R::Help; - }, - R:: Ok(v) => { - v - }, - R:: Err => { - return R::Err; - }, - R:: EOF => { - return state.r_err(format!("Missing argument for {}", #flag)); - } - }; - optional.#field_ident = Some(#f_local_ident); - return R::Ok(true); - } - }); - return true; - }(&f) { - continue; - } - - // Positional/required parsing - let help_placeholder = field_ident.to_string().to_case(Case::UpperKebab); - let eof_code = if required_i == 0 { - quote!{ - break R::EOF; - } - } else { - quote!{ - break state.r_err(format!("Missing argument {}", #help_placeholder)); - } - }; - let gen = gen_impl_type(&f.ty, &ident.to_string()); - help_recurse.extend(gen.help_recurse); - help_placeholders.push(quote!(& aargvark:: style_link(#help_placeholder))); - let help_child_placeholders = gen.help_child_placeholders; - help_placeholders_detail.push((quote!{ - &format!( - "{}{}", - aargvark::style_link(&format!("{}:", #help_placeholder)), - aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) - ) - }, help_docstr)); - let vark = gen.vark; - vark_parse_positional.push(quote!{ - let #f_local_ident = loop { - if match state.peek() { - PeekR:: None => false, - PeekR:: Help => break R:: Help, - PeekR:: Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { - R:: Help => { - break R::Help - }, - R:: Ok(v) => { - v - }, - R:: Err => { - break R::Err; - }, - R:: EOF => { - unreachable!(); - } - }, - } - { - continue; - } - break #vark; - }; - let #f_local_ident = match #f_local_ident { - R:: Help => { - break R::Help; - }, - R:: Ok(v) => { - v - }, - R:: Err => { - break R::Err; - }, - R:: EOF => { - #eof_code - } - }; - }); - vark_copy_fields.push(quote!{ - #field_ident: #f_local_ident - }); - required_i += 1; - } - if vark_optional_fields.len() > 0 { - help_placeholders.push(quote!(&aargvark::style_link("[OPT...]"))); - } - - // Assemble code - let vark = quote!{ - { - state.simple_enum_root = false; - loop { - #[derive(Default)] struct Optional { - #(#vark_optional_fields) * - } - let mut optional = Optional::default(); - fn parse_optional( - optional: &mut Optional, - state: &mut aargvark::VarkState, - s: String - ) -> R < bool > { - match s.as_str() { - #(#vark_parse_optional_cases) * _ => return R:: Ok(false), - }; - } - #(#vark_parse_positional) * - // Parse any remaining optional args - let opt_search_res = loop { - match state.peek() { - PeekR::None => { - break state.r_ok(()); - }, - PeekR::Help => break R::Help, - PeekR::Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { - R::Help => { - break R::Help; - }, - R::Ok(v) => { - if !v { - break state.r_ok(()); - } - }, - R::Err => { - break R::Err; - }, - R::EOF => { - unreachable!(); - }, - }, - }; - }; - match opt_search_res { - R::Help => { - break R::Help; - }, - R::Ok(()) => { }, - R::Err => { - break R::Err; - }, - R::EOF => { - unreachable!(); - }, - }; - // Build obj + return - break state.r_ok(#ident { - #(#vark_copy_fields), - * - }); - } - } - }; - return GenRec { - vark: vark, - help_child_placeholders: help_placeholders, - help_child_placeholders_detail: help_placeholders_detail, - help_recurse: help_recurse, - }; - }, - Fields::Unnamed(d) => { - return gen_impl_unnamed( - &ident.to_string(), - ident.to_token_stream(), - d - .unnamed - .iter() - .map(|f| (get_docstr(&f.attrs), &f.ty)) - .collect::>() - .as_slice(), - ); - }, - Fields::Unit => { - return GenRec { - vark: quote!{ - state.r_ok(#ident) - }, - help_child_placeholders: vec![], - help_child_placeholders_detail: vec![], - help_recurse: vec![], - }; - }, - }; -} - -fn gen_impl(ast: syn::DeriveInput) -> TokenStream { - let ident = &ast.ident; - let help_placeholder = ident.to_string().to_case(Case::UpperKebab); - let help_docstr = get_docstr(&ast.attrs); - let help_child_placeholder_joiner; - let gen = match &ast.data { - syn::Data::Struct(d) => { - help_child_placeholder_joiner = quote!(" "); - gen_impl_struct(ast.ident.to_token_stream(), &d.fields) - }, - syn::Data::Enum(d) => { - let mut all_tags = vec![]; - let mut vark_cases = vec![]; - let mut help_recurse = vec![]; - let mut help_placeholders = vec![]; - let mut help_placeholders_detail = vec![]; - let mut help_short_placeholders = vec![]; - let mut help_short_placeholders_detail = vec![]; - help_child_placeholder_joiner = quote!(" | "); - for v in &d.variants { - let variant_ident = &v.ident; - let name_str = variant_ident.to_string().to_case(Case::Kebab); - let gen = gen_impl_struct(quote!(#ident:: #variant_ident), &v.fields); - all_tags.push(name_str.clone()); - let help_docstr = get_docstr(&v.attrs); - help_short_placeholders.push(quote!(#name_str)); - help_short_placeholders_detail.push((quote!(#name_str), help_docstr.clone())); - help_placeholders.push(quote!(#name_str)); - let help_child_placeholders = gen.help_child_placeholders; - help_placeholders_detail.push( - ( - quote!( - &format!( - "{}{}", - #name_str, - aargvark:: join_strs("", &[#(&format!(" {}", #help_child_placeholders)), *]) - ) - ), - help_docstr.clone(), - ), - ); - help_recurse.extend(gen.help_recurse.clone()); - let child_help_recurse = gen.help_recurse; - let vark = gen.vark; - let help_child_placeholders_detail: Vec = - gen - .help_child_placeholders_detail - .iter() - .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) - .collect(); - vark_cases.push(quote!{ - #name_str => { - state.consume(); - state.breadcrumbs.push(#name_str.to_string()); - let v = #vark; - if simple_enum_root && matches !(v, R::Help) { - let (mut text, mut seen_sections) = - aargvark::generate_help_section_usage_prefix(state); - text.push_str( - & aargvark:: generate_help_section_suffix( - #help_docstr, - vec![#(#help_child_placeholders), *], - vec![#(#help_child_placeholders_detail), *], - " " - ) - ); - #( - < #child_help_recurse >:: generate_help_section(&mut text, &mut seen_sections); - ) * eprintln !("{}\n", text.trim()); - std::process::exit(0); - } - state.breadcrumbs.pop(); - v - } - }); - } - let help_short_placeholders_detail: Vec = - help_short_placeholders_detail - .iter() - .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) - .collect(); - GenRec { - vark: quote!{ - { - let simple_enum_root = state.simple_enum_root; - let tag = match state.peek() { - PeekR:: None => return R:: EOF, - PeekR:: Help => { - if simple_enum_root { - let (mut text, mut seen_sections) = - aargvark::generate_help_section_usage_prefix(state); - text.push_str( - & aargvark:: generate_help_section_suffix( - #help_docstr, - vec![#(#help_short_placeholders), *], - vec![#(#help_short_placeholders_detail), *], - #help_child_placeholder_joiner - ) - ); - eprintln!("{}\n", text.trim()); - std::process::exit(0); - } - else { - return R::Help; - } - }, - PeekR:: Ok(s) => s, - }; - match tag { - #(#vark_cases) * _ => { - state.r_err( - format!("Unrecognized variant {}. Choices are {:?}", tag, vec![#(#all_tags), *]), - ) - } - } - } - }, - help_child_placeholders: help_placeholders, - help_child_placeholders_detail: help_placeholders_detail, - help_recurse: help_recurse, - } - }, - syn::Data::Union(_) => panic!("union not supported"), - }; - let vark = gen.vark; - let help_child_placeholders = gen.help_child_placeholders; - let help_child_placeholders_detail: Vec = - gen - .help_child_placeholders_detail - .iter() - .map(|(placeholder, docstr)| quote!((#placeholder, #docstr))) - .collect(); - let mut seen_help_recurse = HashSet::new(); - let mut help_recurse = vec![]; - for r in gen.help_recurse { - if !seen_help_recurse.insert(r.to_token_stream().to_string()) { - continue; - } - help_recurse.push(r); - } - return quote!{ - impl aargvark:: AargvarkTrait for #ident { - fn vark(state: &mut aargvark::VarkState) -> aargvark:: R < #ident > { - use aargvark::R; - use aargvark::PeekR; - #vark - } - fn generate_help_placeholder() -> String { - aargvark:: style_link(#help_placeholder) - } - fn generate_help_section_suffix(text: &mut String, seen_sections: &mut std::collections::HashSet) { - text.push_str( - & aargvark:: generate_help_section_suffix( - #help_docstr, - vec![#(#help_child_placeholders), *], - vec![#(#help_child_placeholders_detail), *], - #help_child_placeholder_joiner - ) - ); - #(< #help_recurse >:: generate_help_section(text, seen_sections);) * - } - fn generate_help_section(text: &mut String, seen_sections: &mut std::collections::HashSet) { - if ! seen_sections.insert(#help_placeholder.to_string()) { - return; - } - text.push_str(& aargvark:: style_section(#help_placeholder)); - text.push_str(":"); - < #ident >:: generate_help_section_suffix(text, seen_sections); - } - } - }; -} - -#[proc_macro_derive(Aargvark)] -pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - return gen_impl(parse_macro_input!(input as DeriveInput)).into(); -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - use proc_macro2::TokenStream; - use quote::quote; - use crate::gen_impl; - - #[test] - fn newtype_string() { - assert_eq!( - gen_impl( - syn::parse2(TokenStream::from_str("enum Yol { - ToqQuol, - }").unwrap()).unwrap(), - ).to_string(), - quote!().to_string() - ); - } -} diff --git a/tests/test.rs b/tests/test.rs index bba9380..95bb127 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -108,3 +108,14 @@ fn t_struct_opt_last() { a: Some("wowo".into()), }); } + +#[test] +fn t_help_break() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + b: Option<()>, + } + + let v: Naya = vark_explicit("".to_string(), svec!["noh", "--a", "wowo"]); + assert_eq!(v, Naya { b: None }); +} From 4b329872893a8cda00a366c8c1419351a44ab08f Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 29 Jan 2024 06:26:51 +0900 Subject: [PATCH 17/58] Tweak cargo --- proc_macros/Cargo.toml | 2 +- src/lib.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 4f50aff..8a4f4db 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -6,7 +6,7 @@ license = "ISC" description = "Helper crate for aargvark" homepage = "https://github.com/andrewbaxter/aargvark" repository = "https://github.com/andrewbaxter/aargvark" -readme = "../../readme.md" +readme = "../readme.md" [lib] proc-macro = true diff --git a/src/lib.rs b/src/lib.rs index 3e35eb2..7ffd4db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![doc= include_str !("../readme.md")] + use std::{ any::TypeId, cell::RefCell, From d8861bfdc8aa830e3d9537381804bb932cebbfdb Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 29 Jan 2024 06:39:55 +0900 Subject: [PATCH 18/58] Tweaks, readme update again --- Cargo.toml | 2 +- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 2 +- readme.md | 25 ++++++++++++++----------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3963cc8..fecf6c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 8a4f4db..4048012 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.4" +version = "0.0.5" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index f2c3edf..f570ade 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -401,7 +401,7 @@ fn gen_impl_struct( PeekR:: Help => { aargvark:: show_help_and_exit(state, | state | { return aargvark:: HelpPartialProduction { - description: #field_help_docstr.to_string(), + description: #help_docstr.to_string(), content: build_partial_help(state, #required_i, &optional), }; }); diff --git a/readme.md b/readme.md index 028fdd8..51c15b6 100644 --- a/readme.md +++ b/readme.md @@ -6,23 +6,26 @@ Self-similar derive-based command line argument parsing, in the same genre as Cl This attempts to support parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. Just because you can doesn't mean you should. ``` -$ echo This is an example help output, sans light ansi styling -$ ./target/debug/spagh-cli publish -h -Usage: ./target/debug/spagh-cli publish PUBLISH +$ # This is an example help output, sans light ansi styling +$ spagh set -h +Usage: spagh set > IDENTITY DATA -Create or replace existing publish data for an identity on a publisher server + IDENTITY: BACKED-IDENTITY-ARG Identity to publish as + DATA: | - Data to publish. Must be json in the structure `{KEY: {"ttl": MINUTES, "value": DATA}, ...}` +BACKED-IDENTITY-ARG: local | card -PUBLISH: SERVER IDENTITY DATA + An identity with its associated secret. - SERVER: URL of a server with publishing set up - IDENTITY: IDENTITY Identity to publish as - DATA: |- Data to publish. Must be json in the structure `{KEY: {"ttl": SECONDS, "value": "DATA"}, ...}` + local A file containing a generated key + card card PC/SC card with ED25519 key -IDENTITY: local | card +card: PCSC-ID PIN - local |- - card PCSC-ID PIN + PCSC-ID: Card to register, using id per pcscd (not identity id) + PIN: Card pin + +$ ``` # Why or why not From d326e357450593ca4849cb543cfa3fe9d00d3ac5 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 29 Jan 2024 21:01:19 +0900 Subject: [PATCH 19/58] Redo breadcrumbs, more breakpoints --- Cargo.toml | 4 ++-- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 40 +++++++++++++++++----------------------- readme.md | 8 ++++++-- src/lib.rs | 21 +++++++++------------ tests/test.rs | 20 ++++++++++---------- 6 files changed, 45 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fecf6c2..5e1fce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.1.1" +version = "0.1.2" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.0.4" } +aargvark_proc_macros = { path = "proc_macros", version = "0.0.6" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 4048012..4ed8b63 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.5" +version = "0.0.6" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index f570ade..3148bcb 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -193,24 +193,9 @@ fn gen_impl_unnamed( placeholder.reverse(); String::from_iter(placeholder) }); - let push_breadcrumb; - let pop_breadcrumb; - if !help_unit_transparent { - push_breadcrumb = quote!{ - state.breadcrumbs.push(#placeholder.to_string()); - }; - pop_breadcrumb = quote!{ - state.breadcrumbs.pop(); - }; - } else { - push_breadcrumb = quote!(); - pop_breadcrumb = quote!(); - } parse_positional.push(quote!{ - #push_breadcrumb //. . let r = #vark; - #pop_breadcrumb //. . let #f_ident = match r { R:: Ok(v) => { @@ -273,6 +258,7 @@ fn gen_impl_struct( parent_ident: TokenStream, ident: TokenStream, help_placeholder: &str, + vark_attr: &VarkAttr, help_docstr: &str, subtype_index: usize, d: &Fields, @@ -361,7 +347,13 @@ fn gen_impl_struct( return R::Ok(true); } }); - let field_help_pattern = gen.help_pattern; + let field_help_pattern; + if vark_attr.help_break || field_vark_attr.help_break { + field_help_pattern = quote!(aargvark::HelpPattern(vec![])); + } + else { + field_help_pattern = gen.help_pattern; + } let help_field = quote!{ aargvark:: HelpOptionalField { literal: #flag.to_string(), @@ -587,7 +579,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let ident = &ast.ident; let vark_attr = get_vark(&ast.attrs); let help_docstr = get_docstr(&ast.attrs); - let help_placeholder = vark_attr.id.unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); + let help_placeholder = vark_attr.id.clone().unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); let vark; let help_build; match &ast.data { @@ -597,6 +589,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { ast.ident.to_token_stream(), ast.ident.to_token_stream(), &help_placeholder, + &vark_attr, &help_docstr, 0, &d.fields, @@ -613,12 +606,16 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let variant_help_docstr = get_docstr(&v.attrs); let variant_ident = &v.ident; let name_str = - variant_vark_attr.literal.unwrap_or_else(|| variant_ident.to_string().to_case(Case::Kebab)); + variant_vark_attr + .literal + .clone() + .unwrap_or_else(|| variant_ident.to_string().to_case(Case::Kebab)); let gen = gen_impl_struct( ident.to_token_stream(), quote!(#ident:: #variant_ident), &name_str, + &variant_vark_attr, "", subtype_index + 1, &v.fields, @@ -629,14 +626,11 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { vark_cases.push(quote!{ #name_str => { state.consume(); - state.breadcrumbs.push(#name_str.to_string()); - let v = #vark; - state.breadcrumbs.pop(); - v + #vark } }); let help_variant_pattern; - if vark_attr.help_break { + if vark_attr.help_break || variant_vark_attr.help_break { help_variant_pattern = quote!(aargvark::HelpPattern(vec![])); } else { help_variant_pattern = partial_help_variant_pattern; diff --git a/readme.md b/readme.md index 51c15b6..c57a1a9 100644 --- a/readme.md +++ b/readme.md @@ -82,14 +82,18 @@ To parse your own types, implement `AargvarkTrait`, or if your type takes a sing # Advanced usage +- Vecs + + Vec elements are space separated. The way vec parsing works is it attempts to parse as many elements as possible. When parsing one element fails, it rewinds to after it parsed the last successful element and proceeds from the next field after the vec. + - Prevent recursion in help - Add `#[vark(break)]` to a type to prevent recursing into any of the children. This is useful for subcommand enums - attach this to the enum and it will list the arguments but not the arguments' arguments (unless you do `-h` after specifying one on the command line). + Add `#[vark(break)]` to a type, field, or variant to prevent recursing into any of the children. This is useful for subcommand enums - attach this to the enum and it will list the arguments but not the arguments' arguments (unless you do `-h` after specifying one on the command line). - Rename enum variants and option keys Add `#[vark(name="x")]` to a field. -- Change placeholder text +- Change placeholder (id) string Add `#[vark(id="x")]` to a field. diff --git a/src/lib.rs b/src/lib.rs index 7ffd4db..fc3b0fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,6 @@ use comfy_table::Cell; struct VarkErr { i: usize, - breadcrumbs: Vec, err: String, } @@ -54,9 +53,9 @@ pub enum PeekR<'a> { #[doc(hidden)] pub struct VarkState { + command: Option, args: Vec, i: usize, - pub breadcrumbs: Vec, errors: Vec, } @@ -91,7 +90,6 @@ impl VarkState { pub fn r_err(&mut self, text: String) -> R { self.errors.push(VarkErr { i: self.i, - breadcrumbs: self.breadcrumbs.clone(), err: text, }); return R::Err; @@ -100,11 +98,11 @@ impl VarkState { /// Parse the explicitly passed in arguments. The `command` is only used in help /// text. -pub fn vark_explicit(command: String, args: Vec) -> T { +pub fn vark_explicit(command: Option, args: Vec) -> T { let mut state = VarkState { + command: command, args: args, i: 0, - breadcrumbs: vec![command], errors: vec![], }; match T::vark(&mut state) { @@ -130,7 +128,6 @@ pub fn vark_explicit(command: String, args: Vec) -> T for e in state.errors { text.push_str("\n"); text.push_str(&format!(" * {}\n", e.err)); - text.push_str(&format!(" while processing command {:?} at\n", e.breadcrumbs)); text.push_str(" "); text.push_str(&display_args); text.push_str("\n"); @@ -157,7 +154,7 @@ pub fn vark_explicit(command: String, args: Vec) -> T /// Parse the command line arguments into the specified type. pub fn vark() -> T { let mut args = args(); - let command = args.next().unwrap_or("unknown!".to_string()); + let command = args.next(); return vark_explicit(command, args.collect::>()); } @@ -421,12 +418,8 @@ impl Clone for AargvarkYaml { pub fn vark_from_iter>(state: &mut VarkState) -> R { let mut out = vec![]; let mut rewind_to = state.position(); - let mut i = 0usize; loop { - i += 1; - state.breadcrumbs.push(format!("[{}]", i)); let r = T::vark(state); - state.breadcrumbs.pop(); match r { R::Ok(v) => { out.push(v); @@ -773,7 +766,11 @@ pub fn show_help_and_exit< // Write initial partial production let mut out = style_usage("Usage:"); - for s in &state.breadcrumbs { + if let Some(s) = &state.command { + out.push_str(" "); + out.push_str(&style_literal(s)); + } + for s in state.args.iter().take(state.i) { out.push_str(" "); out.push_str(&style_literal(s)); } diff --git a/tests/test.rs b/tests/test.rs index 95bb127..2051b61 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -12,13 +12,13 @@ macro_rules! svec{ #[test] fn t_str() { - let v: String = vark_explicit("".to_string(), svec!["a"]); + let v: String = vark_explicit(None, svec!["a"]); assert_eq!(v, "a"); } #[test] fn t_vec() { - let v: Vec = vark_explicit("".to_string(), svec!["a", "b"]); + let v: Vec = vark_explicit(None, svec!["a", "b"]); assert_eq!(v, svec!["a", "b"]); } @@ -29,7 +29,7 @@ fn t_enum_unit() { ToqQuol, } - let v: Yol = vark_explicit("".to_string(), svec!["toq-quol"]); + let v: Yol = vark_explicit(None, svec!["toq-quol"]); assert_eq!(v, Yol::ToqQuol); } @@ -40,7 +40,7 @@ fn t_enum_tuple() { ToqQuol(String, String), } - let v: Yol = vark_explicit("".to_string(), svec!["toq-quol", "yon", "nor"]); + let v: Yol = vark_explicit(None, svec!["toq-quol", "yon", "nor"]); assert_eq!(v, Yol::ToqQuol("yon".into(), "nor".into())); } @@ -53,7 +53,7 @@ fn t_enum_struct() { }, } - let v: Yol = vark_explicit("".to_string(), svec!["toq-quol", "pahla"]); + let v: Yol = vark_explicit(None, svec!["toq-quol", "pahla"]); assert_eq!(v, Yol::ToqQuol { a: "pahla".into() }); } @@ -64,7 +64,7 @@ fn t_struct() { a: String, } - let v: Naya = vark_explicit("".to_string(), svec!["wowo"]); + let v: Naya = vark_explicit(None, svec!["wowo"]); assert_eq!(v, Naya { a: "wowo".into() }); } @@ -75,7 +75,7 @@ fn t_struct_opt_only() { a: Option, } - let v: Naya = vark_explicit("".to_string(), svec!["--a", "wowo"]); + let v: Naya = vark_explicit(None, svec!["--a", "wowo"]); assert_eq!(v, Naya { a: Some("wowo".into()) }); } @@ -87,7 +87,7 @@ fn t_struct_opt_first() { a: Option, } - let v: Naya = vark_explicit("".to_string(), svec!["--a", "wowo", "noh"]); + let v: Naya = vark_explicit(None, svec!["--a", "wowo", "noh"]); assert_eq!(v, Naya { b: "noh".into(), a: Some("wowo".into()), @@ -102,7 +102,7 @@ fn t_struct_opt_last() { a: Option, } - let v: Naya = vark_explicit("".to_string(), svec!["noh", "--a", "wowo"]); + let v: Naya = vark_explicit(None, svec!["noh", "--a", "wowo"]); assert_eq!(v, Naya { b: "noh".into(), a: Some("wowo".into()), @@ -116,6 +116,6 @@ fn t_help_break() { b: Option<()>, } - let v: Naya = vark_explicit("".to_string(), svec!["noh", "--a", "wowo"]); + let v: Naya = vark_explicit(None, svec!["noh", "--a", "wowo"]); assert_eq!(v, Naya { b: None }); } From 9fdfea16da6e09547160fc1babe040594295da43 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Thu, 22 Feb 2024 18:48:35 +0900 Subject: [PATCH 20/58] Support derive on generics --- Cargo.toml | 4 ++-- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 45 ++++++++++++++++++++++++++++++++++-------- tests/test.rs | 11 ++++++----- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e1fce1..71b61eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.1.2" +version = "0.1.3" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.0.6" } +aargvark_proc_macros = { path = "proc_macros", version = "0.0.7" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6.0" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 4ed8b63..af1e0be 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.6" +version = "0.0.7" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 3148bcb..489f7e1 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -257,6 +257,8 @@ fn gen_impl_unnamed( fn gen_impl_struct( parent_ident: TokenStream, ident: TokenStream, + decl_generics: &TokenStream, + forward_generics: &TokenStream, help_placeholder: &str, vark_attr: &VarkAttr, help_docstr: &str, @@ -268,6 +270,7 @@ fn gen_impl_struct( let mut help_fields = vec![]; let mut partial_help_fields = vec![]; let mut vark_optional_fields = vec![]; + let mut vark_optional_fields_default = vec![]; let mut vark_parse_optional_cases = vec![]; let mut vark_parse_positional = vec![]; let mut vark_copy_fields = vec![]; @@ -321,6 +324,9 @@ fn gen_impl_struct( vark_optional_fields.push(quote!{ #field_ident: Option < #ty >, }); + vark_optional_fields_default.push(quote!{ + #field_ident: None, + }); vark_copy_fields.push(quote!{ #field_ident: optional.#field_ident }); @@ -452,12 +458,14 @@ fn gen_impl_struct( let vark = quote!{ { loop { - #[derive(Default)] struct Optional { + struct Optional #decl_generics { #(#vark_optional_fields) * } - let mut optional = Optional::default(); - fn parse_optional( - optional: &mut Optional, + let mut optional = Optional { + #(#vark_optional_fields_default) * + }; + fn parse_optional #decl_generics( + optional:& mut Optional #forward_generics, state: &mut aargvark::VarkState, s: String ) -> R < bool > { @@ -467,10 +475,10 @@ fn gen_impl_struct( _ => return R:: Ok(false), }; } - fn build_partial_help( + fn build_partial_help #decl_generics( state: &mut aargvark::HelpState, required_i: usize, - optional: &Optional + optional:& Optional #forward_generics, ) -> aargvark:: HelpPartialContent { let mut fields = vec![]; let mut optional_fields = vec![]; @@ -577,6 +585,23 @@ fn gen_impl_struct( fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let ident = &ast.ident; + let decl_generics = ast.generics.to_token_stream(); + let forward_generics; + { + let mut parts = vec![]; + for p in ast.generics.params { + match p { + syn::GenericParam::Type(p) => parts.push(p.ident.to_token_stream()), + syn::GenericParam::Lifetime(p) => parts.push(p.lifetime.to_token_stream()), + syn::GenericParam::Const(p) => parts.push(p.ident.to_token_stream()), + } + } + if parts.is_empty() { + forward_generics = quote!(); + } else { + forward_generics = quote!(< #(#parts), *>); + } + } let vark_attr = get_vark(&ast.attrs); let help_docstr = get_docstr(&ast.attrs); let help_placeholder = vark_attr.id.clone().unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); @@ -588,6 +613,8 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { gen_impl_struct( ast.ident.to_token_stream(), ast.ident.to_token_stream(), + &decl_generics, + &forward_generics, &help_placeholder, &vark_attr, &help_docstr, @@ -614,6 +641,8 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { gen_impl_struct( ident.to_token_stream(), quote!(#ident:: #variant_ident), + &decl_generics, + &forward_generics, &name_str, &variant_vark_attr, "", @@ -683,8 +712,8 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { syn::Data::Union(_) => panic!("Union not supported"), }; return quote!{ - impl aargvark:: AargvarkTrait for #ident { - fn vark(state: &mut aargvark::VarkState) -> aargvark:: R < #ident > { + impl #decl_generics aargvark:: AargvarkTrait for #ident #forward_generics { + fn vark(state: &mut aargvark::VarkState) -> aargvark:: R < #ident #forward_generics > { use aargvark::R; use aargvark::PeekR; #vark diff --git a/tests/test.rs b/tests/test.rs index 2051b61..7cfa9e0 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,6 +1,7 @@ use aargvark::{ self, vark_explicit, + AargvarkTrait, }; use aargvark_proc_macros::Aargvark; @@ -110,12 +111,12 @@ fn t_struct_opt_last() { } #[test] -fn t_help_break() { +fn t_generic() { #[derive(Aargvark, PartialEq, Debug)] - struct Naya { - b: Option<()>, + struct Naya { + b: Option, } - let v: Naya = vark_explicit(None, svec!["noh", "--a", "wowo"]); - assert_eq!(v, Naya { b: None }); + let v: Naya = vark_explicit(None, svec!["--b", "hi"]); + assert_eq!(v, Naya { b: Some("hi".to_string()) }); } From e7e277325d2761a9de2caa37b7665d27b37dfc50 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 18 Mar 2024 16:21:40 +0900 Subject: [PATCH 21/58] Move exit(1) out of vark_explicit --- Cargo.toml | 11 ++-- src/lib.rs | 153 +++++++++++++++++++++++++++++++++++--------------- tests/test.rs | 20 +++---- 3 files changed, 124 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 71b61eb..45019d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.1.3" +version = "0.2.0" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -21,10 +21,11 @@ http_types = ["dep:http"] aargvark_proc_macros = { path = "proc_macros", version = "0.0.7" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } -convert_case = "0.6.0" -comfy-table = { version = "7.1.0", features = ["custom_styling"] } +convert_case = "0.6" +comfy-table = { version = "7", features = ["custom_styling"] } url = { version = "2", optional = true } http = { version = "1", optional = true } serde = { version = "1", optional = true } -console = "0.15.8" -textwrap = { version = "0.16.0", features = ["terminal_size"] } +console = "0.15" +textwrap = { version = "0.16", features = ["terminal_size"] } +unicode-width = "0.1" diff --git a/src/lib.rs b/src/lib.rs index fc3b0fd..cdd95d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,10 +31,11 @@ use std::{ }; pub use aargvark_proc_macros::Aargvark; use comfy_table::Cell; +use unicode_width::UnicodeWidthStr; -struct VarkErr { - i: usize, - err: String, +pub struct VarkFailure { + pub arg_offset: usize, + pub error: String, } #[doc(hidden)] @@ -56,7 +57,7 @@ pub struct VarkState { command: Option, args: Vec, i: usize, - errors: Vec, + errors: Vec, } impl VarkState { @@ -88,17 +89,91 @@ impl VarkState { } pub fn r_err(&mut self, text: String) -> R { - self.errors.push(VarkErr { - i: self.i, - err: text, + self.errors.push(VarkFailure { + arg_offset: self.i, + error: text, }); return R::Err; } } -/// Parse the explicitly passed in arguments. The `command` is only used in help -/// text. -pub fn vark_explicit(command: Option, args: Vec) -> T { +pub enum ErrorDetail { + /// The command was empty + Empty, + /// Fully parsed command but additional unconsumed arguments follow (offset of + /// first unrecognized argument) + TooMuch(usize), + /// Aargvark considers multiple possible parses. This is a list of considered + /// parses, in order of when they were ruled out. + Incorrect(Vec), +} + +/// Returned by `vark_explicit`. `command` is whatever is passed as `command` to +/// `vark_explicit`, the first of argv if using `vark`. `args` is the remaining +/// arguments. +pub struct Error { + pub command: Option, + pub args: Vec, + pub detail: ErrorDetail, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.detail { + ErrorDetail::Empty => { + return "You must specify arguments, use --help for more info".fmt(f); + }, + ErrorDetail::TooMuch(first) => { + return format_args!( + "Error parsing command line arguments: final arguments are unrecognized\n{:?}", + &self.args[*first..] + ).fmt(f); + }, + ErrorDetail::Incorrect(failures) => { + let mut display_args = vec![]; + if let Some(c) = &self.command { + display_args.push(c.clone()); + } + display_args.extend(self.args.iter().map(|a| format!("{:?}", a))); + let mut display_arg_offsets = vec![]; + { + let mut offset = 0; + for d in &display_args { + display_arg_offsets.push(offset); + offset += d.width() + 1; + } + display_arg_offsets.push(offset); + } + let mut display_args = display_args.join(" "); + display_args.push_str(" "); + let mut text = "Error parsing arguments.\n".to_string(); + for e in failures.iter().rev() { + text.push_str("\n"); + text.push_str(&format!(" * {}\n", e.error)); + text.push_str(" "); + text.push_str(&display_args); + text.push_str("\n"); + text.push_str(" "); + text.push_str(&" ".repeat(display_arg_offsets.get(e.arg_offset).cloned().unwrap_or(0usize))); + text.push_str("^\n"); + } + return text.fmt(f); + }, + } + } +} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return std::fmt::Display::fmt(self, f); + } +} + +impl std::error::Error for Error { } + +/// Parse the explicitly passed in arguments - don't read application globals. The +/// `command` is only used in help and error text. +pub fn vark_explicit(command: Option, args: Vec) -> Result { let mut state = VarkState { command: command, args: args, @@ -107,46 +182,28 @@ pub fn vark_explicit(command: Option, args: Vec { - eprintln!("You must specify command line arguments, use --help for more info."); - exit(1); + return Err(Error { + command: state.command, + args: state.args, + detail: ErrorDetail::Empty, + }); }, R::Err => { - let display_args: Vec = state.args.iter().map(|a| format!("{:?}", a)).collect(); - let mut display_arg_offsets = vec![]; - { - let mut offset = 0; - for d in &display_args { - display_arg_offsets.push(offset); - offset += d.chars().count() + 1; - } - display_arg_offsets.push(offset); - } - let mut display_args = display_args.join(" "); - display_args.push_str(" "); - let mut text = "Error parsing command line arguments.\n".to_string(); - state.errors.reverse(); - for e in state.errors { - text.push_str("\n"); - text.push_str(&format!(" * {}\n", e.err)); - text.push_str(" "); - text.push_str(&display_args); - text.push_str("\n"); - text.push_str(" "); - text.push_str(&" ".repeat(display_arg_offsets.get(e.i).cloned().unwrap_or(0usize))); - text.push_str("^\n"); - } - eprintln!("{}\n", text); - exit(1); + return Err(Error { + command: state.command, + args: state.args, + detail: ErrorDetail::Incorrect(state.errors), + }); }, R::Ok(v) => { if state.i != state.args.len() { - eprintln!( - "Error parsing command line arguments: final arguments are unrecognized\n{:?}", - &state.args[state.i..] - ); - exit(1); + return Err(Error { + command: state.command, + args: state.args, + detail: ErrorDetail::TooMuch(state.i), + }); } - return v; + return Ok(v); }, } } @@ -155,7 +212,13 @@ pub fn vark_explicit(command: Option, args: Vec() -> T { let mut args = args(); let command = args.next(); - return vark_explicit(command, args.collect::>()); + match vark_explicit(command, args.collect::>()) { + Ok(v) => return v, + Err(e) => { + eprintln!("{:?}", e); + exit(1); + }, + } } /// Anything that implements this trait can be parsed and used as a field in other diff --git a/tests/test.rs b/tests/test.rs index 7cfa9e0..e8619ca 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -13,13 +13,13 @@ macro_rules! svec{ #[test] fn t_str() { - let v: String = vark_explicit(None, svec!["a"]); + let v: String = vark_explicit(None, svec!["a"]).unwrap(); assert_eq!(v, "a"); } #[test] fn t_vec() { - let v: Vec = vark_explicit(None, svec!["a", "b"]); + let v: Vec = vark_explicit(None, svec!["a", "b"]).unwrap(); assert_eq!(v, svec!["a", "b"]); } @@ -30,7 +30,7 @@ fn t_enum_unit() { ToqQuol, } - let v: Yol = vark_explicit(None, svec!["toq-quol"]); + let v: Yol = vark_explicit(None, svec!["toq-quol"]).unwrap(); assert_eq!(v, Yol::ToqQuol); } @@ -41,7 +41,7 @@ fn t_enum_tuple() { ToqQuol(String, String), } - let v: Yol = vark_explicit(None, svec!["toq-quol", "yon", "nor"]); + let v: Yol = vark_explicit(None, svec!["toq-quol", "yon", "nor"]).unwrap(); assert_eq!(v, Yol::ToqQuol("yon".into(), "nor".into())); } @@ -54,7 +54,7 @@ fn t_enum_struct() { }, } - let v: Yol = vark_explicit(None, svec!["toq-quol", "pahla"]); + let v: Yol = vark_explicit(None, svec!["toq-quol", "pahla"]).unwrap(); assert_eq!(v, Yol::ToqQuol { a: "pahla".into() }); } @@ -65,7 +65,7 @@ fn t_struct() { a: String, } - let v: Naya = vark_explicit(None, svec!["wowo"]); + let v: Naya = vark_explicit(None, svec!["wowo"]).unwrap(); assert_eq!(v, Naya { a: "wowo".into() }); } @@ -76,7 +76,7 @@ fn t_struct_opt_only() { a: Option, } - let v: Naya = vark_explicit(None, svec!["--a", "wowo"]); + let v: Naya = vark_explicit(None, svec!["--a", "wowo"]).unwrap(); assert_eq!(v, Naya { a: Some("wowo".into()) }); } @@ -88,7 +88,7 @@ fn t_struct_opt_first() { a: Option, } - let v: Naya = vark_explicit(None, svec!["--a", "wowo", "noh"]); + let v: Naya = vark_explicit(None, svec!["--a", "wowo", "noh"]).unwrap(); assert_eq!(v, Naya { b: "noh".into(), a: Some("wowo".into()), @@ -103,7 +103,7 @@ fn t_struct_opt_last() { a: Option, } - let v: Naya = vark_explicit(None, svec!["noh", "--a", "wowo"]); + let v: Naya = vark_explicit(None, svec!["noh", "--a", "wowo"]).unwrap(); assert_eq!(v, Naya { b: "noh".into(), a: Some("wowo".into()), @@ -117,6 +117,6 @@ fn t_generic() { b: Option, } - let v: Naya = vark_explicit(None, svec!["--b", "hi"]); + let v: Naya = vark_explicit(None, svec!["--b", "hi"]).unwrap(); assert_eq!(v, Naya { b: Some("hi".to_string()) }); } From b3bc58bd0f13c717791213494991aaa44f501dca Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 24 Apr 2024 13:05:12 +0900 Subject: [PATCH 22/58] Fix bad argument pointer offset --- Cargo.toml | 2 +- src/lib.rs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45019d8..aa72b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/src/lib.rs b/src/lib.rs index cdd95d0..34fdaab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,8 +131,10 @@ impl std::fmt::Display for Error { }, ErrorDetail::Incorrect(failures) => { let mut display_args = vec![]; + let mut offset_offset = 0; if let Some(c) = &self.command { display_args.push(c.clone()); + offset_offset = 1; } display_args.extend(self.args.iter().map(|a| format!("{:?}", a))); let mut display_arg_offsets = vec![]; @@ -154,7 +156,11 @@ impl std::fmt::Display for Error { text.push_str(&display_args); text.push_str("\n"); text.push_str(" "); - text.push_str(&" ".repeat(display_arg_offsets.get(e.arg_offset).cloned().unwrap_or(0usize))); + text.push_str( + &" ".repeat( + display_arg_offsets.get(e.arg_offset + offset_offset).cloned().unwrap_or(0usize), + ), + ); text.push_str("^\n"); } return text.fmt(f); From 4699a060ff01124adf4ceabefe5a1e1d140acf48 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 3 Jun 2024 03:31:41 +0900 Subject: [PATCH 23/58] Admin update --- .github/pull_request_template.md | 4 ++++ .github/workflows/copyright.yml | 18 ++++++++++++++++++ license.txt | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/copyright.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ded2f34 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,4 @@ + +--- + +I agree that when the request is merged I assign the copyright of the request to the repository owner. diff --git a/.github/workflows/copyright.yml b/.github/workflows/copyright.yml new file mode 100644 index 0000000..b5b42d2 --- /dev/null +++ b/.github/workflows/copyright.yml @@ -0,0 +1,18 @@ +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + confirm_agreement: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BODY: ${{ github.event.pull_request.body }} + PR_ID: ${{ github.event.pull_request.number }} + run: | + set -xeu + if ! grep -F "$(tail -n 1 .github/pull_request_template.md)" <(echo "$BODY"); then + gh pr close --comment "All changes must include the provided agreement to the copyright assignment." --delete-branch "$PR_ID" + fi diff --git a/license.txt b/license.txt index af6b977..bd73668 100644 --- a/license.txt +++ b/license.txt @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2023 Andrew Baxter +Copyright (c) 2024 Andrew Baxter Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -12,4 +12,4 @@ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file +PERFORMANCE OF THIS SOFTWARE. From c3a199c359ded09f3fbbfad8348854035e6f6bca Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 5 Jun 2024 14:00:30 +0900 Subject: [PATCH 24/58] Output, messaging tweaks --- Cargo.toml | 2 +- src/lib.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa72b5a..3a439ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.2.1" +version = "0.2.2" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/src/lib.rs b/src/lib.rs index 34fdaab..1280040 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,7 @@ impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.detail { ErrorDetail::Empty => { - return "You must specify arguments, use --help for more info".fmt(f); + return "Missing arguments, use --help for more info".fmt(f); }, ErrorDetail::TooMuch(first) => { return format_args!( @@ -843,7 +843,6 @@ pub fn show_help_and_exit< out.push_str(" "); out.push_str(&style_literal(s)); } - out.push_str(" >"); let mut temp_stack = vec![]; match &partial.content { HelpPartialContent::Pattern(p) => { From 024ece43ba4aeed287e74284ae16c253eecb2872 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 15 Jul 2024 21:56:12 +0900 Subject: [PATCH 25/58] Add simple KV, string HashMap support --- Cargo.toml | 2 +- src/lib.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.rs | 12 +++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3a439ef..9756c0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.2.2" +version = "0.2.3" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/src/lib.rs b/src/lib.rs index 1280040..9009aee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -522,6 +522,80 @@ impl AargvarkTrait for HashSet { } } +/// A key-value argument type (single argument) that takes the format `K=V` where +/// both `K` and `V` need to be string-parsable types. The argument is split at the +/// first unescaped `=` - additional `=` in the key can be escaped with `\`. +/// +/// This is used for the `HashMap` implementation which takes a series of arguments +/// like `a=a b=b c=123`. +struct AargvarkKV { + pub key: K, + pub value: V, +} + +impl AargvarkTrait for AargvarkKV { + fn vark(state: &mut VarkState) -> R { + let res = String::vark(state); + let res = match res { + R::EOF => return R::EOF, + R::Err => return R::Err, + R::Ok(r) => r, + }; + let mut res = res.into_bytes().into_iter(); + let mut k = vec![]; + let mut escape = false; + for c in &mut res { + if escape { + k.push(c); + escape = false; + } else { + if c == b'\\' { + escape = true; + } else if c == b'=' { + break; + } else { + k.push(c); + } + } + } + let key = match K::from_str(&unsafe { + String::from_utf8_unchecked(k) + }) { + Ok(r) => r, + Err(e) => return state.r_err(format!("Error parsing map key: {}", e)), + }; + let value = match V::from_str(&unsafe { + String::from_utf8_unchecked(res.collect()) + }) { + Ok(r) => r, + Err(e) => return state.r_err(format!("Error parsing map value: {}", e)), + }; + return state.r_ok(AargvarkKV { + key: key, + value: value, + }); + } + + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Literal("K=V".to_string())]); + } +} + +impl AargvarkTrait for HashMap { + fn vark(state: &mut VarkState) -> R { + let res = match >>::vark(state) { + R::EOF => return R::EOF, + R::Err => return R::Err, + R::Ok(r) => r, + }; + return state.r_ok(res.into_iter().map(|kv| (kv.key, kv.value)).collect()); + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return >>::build_help_pattern(state); + } +} + fn style_usage(s: impl AsRef) -> String { return s.as_ref().to_string(); } diff --git a/tests/test.rs b/tests/test.rs index e8619ca..afad5ad 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use aargvark::{ self, vark_explicit, @@ -120,3 +121,14 @@ fn t_generic() { let v: Naya = vark_explicit(None, svec!["--b", "hi"]).unwrap(); assert_eq!(v, Naya { b: Some("hi".to_string()) }); } + +#[test] +fn t_map() { + let v = vark_explicit::>(None, svec!["a=2", "b=3"]).unwrap(); + assert_eq!(v, { + let mut m = HashMap::new(); + m.insert("a".to_string(), 2); + m.insert("b".to_string(), 3); + m + }); +} From 62d0ae24032dfbe0a1fed0e0493590b2a9b25d72 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 22 Jul 2024 00:19:22 +0900 Subject: [PATCH 26/58] Bump syn to 2 and other deps --- Cargo.toml | 4 +- proc_macros/Cargo.toml | 12 +-- proc_macros/mod.rs | 202 +++++++++++++++++++++-------------------- readme.md | 2 +- 4 files changed, 111 insertions(+), 109 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9756c0f..265a44e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.2.3" +version = "0.2.4" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.0.7" } +aargvark_proc_macros = { path = "proc_macros", version = "0.1.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index af1e0be..f732255 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.0.7" +version = "0.1.0" edition = "2021" license = "ISC" description = "Helper crate for aargvark" @@ -13,8 +13,8 @@ proc-macro = true path = "mod.rs" [dependencies] -convert_case = "0.6.0" -genemichaels = "0.2.3" -proc-macro2 = "1.0.76" -quote = "1.0.35" -syn = "1.0.109" +convert_case = "0.6" +genemichaels = "0.3" +proc-macro2 = "1" +quote = "1" +syn = "2" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 489f7e1..849251e 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -11,11 +11,15 @@ use quote::{ use syn::{ self, parse_macro_input, + punctuated::Punctuated, + spanned::Spanned, Attribute, DeriveInput, + Expr, Fields, Lit, Meta, + Token, Type, }; @@ -44,76 +48,81 @@ struct VarkAttr { id: Option, } -fn get_vark(attrs: &Vec) -> VarkAttr { +fn get_vark(attrs: &Vec) -> Result { let mut help_break = false; let mut literal = None; let mut id = None; for a in attrs { - let Ok(m) = a.parse_meta() else { - continue; - }; - let Meta:: List(m) = m else { - continue; - }; - if &m.path.to_token_stream().to_string() != "vark" { - continue; - } - for m in m.nested { - let syn:: NestedMeta:: Meta(m) = m else { - continue; - }; - match &m { - Meta::Path(k) => { - match k.to_token_stream().to_string().as_str() { - "break" => { - help_break = true; - }, - i => { - panic!("Unexpected argument in `vark` attr: {:?}", i); - }, - } - }, - Meta::List(_) => { - panic!("Unexpected tokens in `vark` attr arguments: {:?}", m.to_token_stream()); - }, - Meta::NameValue(kv) => { - match kv.path.to_token_stream().to_string().as_str() { - "literal" => { - literal = Some(match &kv.lit { - Lit::Str(s) => s.value(), - l => panic!("`vark` `literal` argument must be a string, got {}", l.to_token_stream()), - }); + match &a.meta { + Meta::List(list) if list.path.is_ident("vark") => { + let nested = list.parse_args_with(Punctuated::::parse_terminated).unwrap(); + for m in nested { + match &m { + Meta::Path(k) => { + match k.to_token_stream().to_string().as_str() { + "break" => { + help_break = true; + }, + i => { + panic!("Unexpected argument in `vark` attr: {:?}", i); + }, + } }, - "id" => { - id = Some(match &kv.lit { - Lit::Str(s) => s.value(), - l => panic!("`vark` `id` argument must be a string, got {}", l.to_token_stream()), - }); + Meta::List(_) => { + panic!("Unexpected tokens in `vark` attr arguments: {:?}", m.to_token_stream()); }, - i => { - panic!("Unexpected argument in `vark` attr: {:?}", i); + Meta::NameValue(kv) => { + match kv.path.require_ident()?.to_string().as_str() { + "literal" => { + literal = Some(match &kv.value { + Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) => s.value(), + l => panic!( + "`vark` `literal` argument must be a string, got {}", + l.to_token_stream() + ), + }); + }, + "id" => { + id = Some(match &kv.value { + Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) => s.value(), + l => panic!( + "`vark` `id` argument must be a string, got {}", + l.to_token_stream() + ), + }); + }, + other => { + panic!("Unexpected argument in `vark` attr: {:?}", other); + }, + } }, } - }, - } + } + }, + other => { + return Err(syn::Error::new(other.span(), "Expected `#[vark(...)]`")); + }, } } - return VarkAttr { + return Ok(VarkAttr { help_break: help_break, literal: literal, id: id, - }; + }); } fn get_docstr(attrs: &Vec) -> String { let mut out = String::new(); for attr in attrs { - if !attr.path.is_ident("doc") { - continue; - } - match attr.parse_meta().unwrap() { - syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(v), .. }) => { - out.push_str(&v.value()); + match &attr.meta { + syn::Meta::NameValue(meta) => { + if !meta.path.is_ident("doc") { + continue; + } + let Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &meta.value else { + continue; + }; + out.push_str(&s.value()); }, _ => continue, } @@ -264,7 +273,7 @@ fn gen_impl_struct( help_docstr: &str, subtype_index: usize, d: &Fields, -) -> GenRec { +) -> Result { match d { Fields::Named(d) => { let mut help_fields = vec![]; @@ -276,7 +285,7 @@ fn gen_impl_struct( let mut vark_copy_fields = vec![]; let mut required_i = 0usize; 'next_field: for (i, f) in d.named.iter().enumerate() { - let field_vark_attr = get_vark(&f.attrs); + let field_vark_attr = get_vark(&f.attrs)?; let field_help_docstr = get_docstr(&f.attrs); let field_ident = f.ident.as_ref().expect("Named field missing name"); let f_local_ident = format_ident!("v{}", i); @@ -286,7 +295,7 @@ fn gen_impl_struct( 'not_optional _; let ty; { - let Type:: Path(t) =& f.ty else { + let Type::Path(t) = &f.ty else { break 'not_optional; }; if t.qself.is_some() { @@ -302,13 +311,13 @@ fn gen_impl_struct( if &s.ident.to_string() != "Option" { break 'not_optional; } - let syn:: PathArguments:: AngleBracketed(a) =& s.arguments else { + let syn::PathArguments::AngleBracketed(a) = &s.arguments else { break 'not_optional; }; if a.args.len() != 1 { break 'not_optional; } - let syn:: GenericArgument:: Type(t) =& a.args[0] else { + let syn::GenericArgument::Type(t) = &a.args[0] else { break 'not_optional; }; ty = t; @@ -466,7 +475,7 @@ fn gen_impl_struct( }; fn parse_optional #decl_generics( optional:& mut Optional #forward_generics, - state: &mut aargvark::VarkState, + state:& mut aargvark:: VarkState, s: String ) -> R < bool > { match s.as_str() { @@ -476,7 +485,7 @@ fn gen_impl_struct( }; } fn build_partial_help #decl_generics( - state: &mut aargvark::HelpState, + state:& mut aargvark:: HelpState, required_i: usize, optional:& Optional #forward_generics, ) -> aargvark:: HelpPartialContent { @@ -533,7 +542,7 @@ fn gen_impl_struct( } } }; - return GenRec { + return Ok(GenRec { vark: vark, help_pattern: quote!{ { @@ -552,38 +561,39 @@ fn gen_impl_struct( aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]) } }, - }; + }); }, Fields::Unnamed(d) => { - return gen_impl_unnamed( - &ident.to_string(), - parent_ident, - ident.to_token_stream(), - help_placeholder, - help_docstr, - subtype_index, - d - .unnamed - .iter() - .map(|f| (get_vark(&f.attrs), get_docstr(&f.attrs), &f.ty)) - .collect::>() - .as_slice(), + let mut fields = vec![]; + for f in &d.unnamed { + fields.push((get_vark(&f.attrs)?, get_docstr(&f.attrs), &f.ty)); + } + return Ok( + gen_impl_unnamed( + &ident.to_string(), + parent_ident, + ident.to_token_stream(), + help_placeholder, + help_docstr, + subtype_index, + &fields, + ), ); }, Fields::Unit => { - return GenRec { + return Ok(GenRec { vark: quote!{ state.r_ok(#ident) }, help_pattern: quote!{ aargvark::HelpPattern(vec![]) }, - }; + }); }, }; } -fn gen_impl(ast: syn::DeriveInput) -> TokenStream { +fn gen_impl(ast: syn::DeriveInput) -> Result { let ident = &ast.ident; let decl_generics = ast.generics.to_token_stream(); let forward_generics; @@ -602,7 +612,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { forward_generics = quote!(< #(#parts), *>); } } - let vark_attr = get_vark(&ast.attrs); + let vark_attr = get_vark(&ast.attrs)?; let help_docstr = get_docstr(&ast.attrs); let help_placeholder = vark_attr.id.clone().unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); let vark; @@ -620,7 +630,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { &help_docstr, 0, &d.fields, - ); + )?; vark = gen.vark; help_build = gen.help_pattern; }, @@ -629,7 +639,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { let mut vark_cases = vec![]; let mut help_variants = vec![]; for (subtype_index, v) in d.variants.iter().enumerate() { - let variant_vark_attr = get_vark(&v.attrs); + let variant_vark_attr = get_vark(&v.attrs)?; let variant_help_docstr = get_docstr(&v.attrs); let variant_ident = &v.ident; let name_str = @@ -648,7 +658,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { "", subtype_index + 1, &v.fields, - ); + )?; all_tags.push(name_str.clone()); let vark = gen.vark; let partial_help_variant_pattern = gen.help_pattern; @@ -683,7 +693,7 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { //. . return aargvark:: HelpPartialProduction { description: #help_docstr.to_string(), - content: aargvark::HelpPartialContent::enum_(variants), + content: aargvark:: HelpPartialContent:: enum_(variants), }; }); }, @@ -711,23 +721,26 @@ fn gen_impl(ast: syn::DeriveInput) -> TokenStream { }, syn::Data::Union(_) => panic!("Union not supported"), }; - return quote!{ + return Ok(quote!{ impl #decl_generics aargvark:: AargvarkTrait for #ident #forward_generics { - fn vark(state: &mut aargvark::VarkState) -> aargvark:: R < #ident #forward_generics > { + fn vark(state:& mut aargvark:: VarkState) -> aargvark:: R < #ident #forward_generics > { use aargvark::R; use aargvark::PeekR; #vark } - fn build_help_pattern(state: &mut aargvark::HelpState) -> aargvark:: HelpPattern { + fn build_help_pattern(state:& mut aargvark:: HelpState) -> aargvark:: HelpPattern { #help_build } } - }; + }); } #[proc_macro_derive(Aargvark, attributes(vark))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - return gen_impl(parse_macro_input!(input as DeriveInput)).into(); + return match gen_impl(parse_macro_input!(input as DeriveInput)) { + Ok(x) => x, + Err(e) => e.to_compile_error(), + }.into(); } #[cfg(test)] @@ -738,6 +751,7 @@ mod tests { use quote::quote; use crate::gen_impl; + /// Used for debugging only, ignore #[test] fn dump() { let got = gen_impl(syn::parse2(quote!{ @@ -745,23 +759,11 @@ mod tests { struct Naya { b: Option<()>, } - }).unwrap()); + }).unwrap()).unwrap(); let cfg = FormatConfig::default(); let mut s = [&got].into_iter().map(|s| genemichaels::format_str(&s.to_string(), &cfg)).collect::>(); let got = s.remove(0).expect(&format!("Failed to format got code:\n{}", got.to_string())).rendered; panic!("{}", got); } - - #[test] - fn newtype_string() { - assert_eq!( - gen_impl( - syn::parse2(TokenStream::from_str("enum Yol { - ToqQuol, - }").unwrap()).unwrap(), - ).to_string(), - quote!().to_string() - ); - } } diff --git a/readme.md b/readme.md index c57a1a9..9583021 100644 --- a/readme.md +++ b/readme.md @@ -64,7 +64,7 @@ To parse command line arguments ``` 2. Vark it - ``` + ```rust let args = aargvark::vark::(); ``` From c6ea877cbff06bed5fd2d7b56860bede77df1f31 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 22 Jul 2024 01:14:26 +0900 Subject: [PATCH 27/58] Switch to genemichaels-lib to avoid cyclical dependency --- Cargo.toml | 4 ++-- proc_macros/Cargo.toml | 4 ++-- proc_macros/mod.rs | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 265a44e..c05b195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.2.4" +version = "0.2.5" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.1.0" } +aargvark_proc_macros = { path = "proc_macros", version = "0.1.1" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index f732255..4366f74 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "ISC" description = "Helper crate for aargvark" @@ -14,7 +14,7 @@ path = "mod.rs" [dependencies] convert_case = "0.6" -genemichaels = "0.3" +genemichaels-lib = "0.5.0-pre2" proc-macro2 = "1" quote = "1" syn = "2" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 849251e..6c6c2f2 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -745,9 +745,7 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[cfg(test)] mod tests { - use std::str::FromStr; - use genemichaels::FormatConfig; - use proc_macro2::TokenStream; + use genemichaels_lib::FormatConfig; use quote::quote; use crate::gen_impl; @@ -762,7 +760,7 @@ mod tests { }).unwrap()).unwrap(); let cfg = FormatConfig::default(); let mut s = - [&got].into_iter().map(|s| genemichaels::format_str(&s.to_string(), &cfg)).collect::>(); + [&got].into_iter().map(|s| genemichaels_lib::format_str(&s.to_string(), &cfg)).collect::>(); let got = s.remove(0).expect(&format!("Failed to format got code:\n{}", got.to_string())).rendered; panic!("{}", got); } From 6a03d3f4c0e9e2df87335c55163053d78370ade5 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Mon, 22 Jul 2024 01:39:01 +0900 Subject: [PATCH 28/58] Fix syn upgrade issues, rename break->stop due to syn2 parsing, doc tweaks and updates --- Cargo.toml | 4 +- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 97 ++++++++++++++++++++++++++---------------- readme.md | 16 ++++--- tests/test.rs | 24 +++++++++++ 5 files changed, 97 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c05b195..585016b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.2.5" +version = "0.3.0" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.1.1" } +aargvark_proc_macros = { path = "proc_macros", version = "0.1.2" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 4366f74..ec402c8 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.1.1" +version = "0.1.2" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 6c6c2f2..90220a6 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -1,26 +1,28 @@ -use convert_case::{ - Case, - Casing, -}; -use proc_macro2::TokenStream; -use quote::{ - format_ident, - quote, - ToTokens, -}; -use syn::{ - self, - parse_macro_input, - punctuated::Punctuated, - spanned::Spanned, - Attribute, - DeriveInput, - Expr, - Fields, - Lit, - Meta, - Token, - Type, +use { + convert_case::{ + Case, + Casing, + }, + proc_macro2::TokenStream, + quote::{ + format_ident, + quote, + ToTokens, + }, + syn::{ + self, + parse_macro_input, + punctuated::Punctuated, + spanned::Spanned, + Attribute, + DeriveInput, + Expr, + Fields, + Lit, + Meta, + Token, + Type, + }, }; /// Break boundary - remove the footgunishness of using loop for this directly @@ -60,48 +62,71 @@ fn get_vark(attrs: &Vec) -> Result { match &m { Meta::Path(k) => { match k.to_token_stream().to_string().as_str() { - "break" => { + "stop" => { help_break = true; }, i => { - panic!("Unexpected argument in `vark` attr: {:?}", i); + return Err( + syn::Error::new( + k.span(), + format!("Unexpected argument in `vark` attr: {:?}", i), + ), + ); }, } }, Meta::List(_) => { - panic!("Unexpected tokens in `vark` attr arguments: {:?}", m.to_token_stream()); + return Err( + syn::Error::new( + m.span(), + format!("Unexpected tokens in `vark` attr arguments: {:?}", m.to_token_stream()), + ), + ); }, Meta::NameValue(kv) => { match kv.path.require_ident()?.to_string().as_str() { "literal" => { literal = Some(match &kv.value { Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) => s.value(), - l => panic!( - "`vark` `literal` argument must be a string, got {}", - l.to_token_stream() + l => return Err( + syn::Error::new( + kv.value.span(), + format!( + "`vark` `literal` argument must be a string, got {}", + l.to_token_stream() + ), + ), ), }); }, "id" => { id = Some(match &kv.value { Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) => s.value(), - l => panic!( - "`vark` `id` argument must be a string, got {}", - l.to_token_stream() + l => return Err( + syn::Error::new( + kv.value.span(), + format!( + "`vark` `id` argument must be a string, got {}", + l.to_token_stream() + ), + ), ), }); }, other => { - panic!("Unexpected argument in `vark` attr: {:?}", other); + return Err( + syn::Error::new( + kv.value.span(), + format!("Unespected argument in `vark` attr: {:?}", other), + ), + ); }, } }, } } }, - other => { - return Err(syn::Error::new(other.span(), "Expected `#[vark(...)]`")); - }, + _ => { }, } } return Ok(VarkAttr { diff --git a/readme.md b/readme.md index 9583021..333f273 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -Self-similar derive-based command line argument parsing, in the same genre as Clap-derive. It supports +Self-similar derive-based command line argument parsing, in the same genre as Clap-derive. It currently supports - Command line parsing - Help @@ -8,7 +8,7 @@ This attempts to support parsing arbitrarily complex command line arguments. Lik ``` $ # This is an example help output, sans light ansi styling $ spagh set -h -Usage: spagh set > IDENTITY DATA +Usage: spagh set IDENTITY DATA IDENTITY: BACKED-IDENTITY-ARG Identity to publish as DATA: | - Data to publish. Must be json in the structure `{KEY: {"ttl": MINUTES, "value": DATA}, ...}` @@ -70,7 +70,7 @@ To parse command line arguments Optional fields in structs become optional (`--long`) arguments. If you want a `bool` long option that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. -You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, most `Ip` and `SocketAddr` types, and `PathBuf` provided. +You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, `Map` with `FromString` keys and values as `K=V` arguments, most `Ip` and `SocketAddr` types, and `PathBuf` provided. Some additional wrappers are provided for automatically loading (and parsing) files: @@ -88,12 +88,14 @@ To parse your own types, implement `AargvarkTrait`, or if your type takes a sing - Prevent recursion in help - Add `#[vark(break)]` to a type, field, or variant to prevent recursing into any of the children. This is useful for subcommand enums - attach this to the enum and it will list the arguments but not the arguments' arguments (unless you do `-h` after specifying one on the command line). + Add `#[vark(stop)]` to a type, field, or variant to prevent recursing into any of the children. This is useful for subcommand enums - attach this to the enum and it will list the arguments but not the arguments' arguments (unless you do `-h` after specifying one on the command line). -- Rename enum variants and option keys +- Rename enum variants and option command line flags - Add `#[vark(name="x")]` to a field. + Ex: you have a field `xyz_id: i32` and you want the flag literal to be `--target-machine`. -- Change placeholder (id) string + Add `#[vark(literal="--target-machine")]` to t field. + +- Change the documentation placeholder (id) string Add `#[vark(id="x")]` to a field. diff --git a/tests/test.rs b/tests/test.rs index afad5ad..2fa3322 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -132,3 +132,27 @@ fn t_map() { m }); } + +#[test] +fn t_docstring() { + #[derive(Aargvark, PartialEq, Debug)] + /// This is a naya + struct Naya {} + + let v: Naya = vark_explicit(None, svec![]).unwrap(); + assert_eq!(v, Naya {}); +} + +#[test] +fn t_varkattr() { + #[derive(Aargvark, PartialEq, Debug)] + #[vark(stop)] + struct Naya { + #[vark(id = "G")] + #[vark(literal = "g")] + f: Option, + } + + let v: Naya = vark_explicit(None, svec!["--g", "3"]).unwrap(); + assert_eq!(v, Naya { f: Some(3) }); +} From c71b7a0759bd6a13c829337404002ac98d6d9236 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 20:55:50 +0900 Subject: [PATCH 29/58] Use explicit flags for any field, documentation tweaks --- Cargo.toml | 4 +- proc_macros/Cargo.toml | 3 +- proc_macros/mod.rs | 370 +++++++++++++++++++---------------------- readme.md | 29 ++-- src/lib.rs | 50 +++--- tests/test.rs | 27 ++- 6 files changed, 241 insertions(+), 242 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 585016b..2b13f30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.3.0" +version = "0.4.0" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.1.2" } +aargvark_proc_macros = { path = "proc_macros", version = "0.4.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index ec402c8..0cb4414 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.1.2" +version = "0.4.0" edition = "2021" license = "ISC" description = "Helper crate for aargvark" @@ -14,6 +14,7 @@ path = "mod.rs" [dependencies] convert_case = "0.6" +darling = "0.20.10" genemichaels-lib = "0.5.0-pre2" proc-macro2 = "1" quote = "1" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 90220a6..320e352 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -3,24 +3,27 @@ use { Case, Casing, }, + darling::{ + FromDeriveInput, + FromField, + FromVariant, + }, proc_macro2::TokenStream, quote::{ format_ident, quote, ToTokens, }, + std::collections::HashSet, syn::{ self, parse_macro_input, - punctuated::Punctuated, spanned::Spanned, Attribute, DeriveInput, Expr, Fields, Lit, - Meta, - Token, Type, }, }; @@ -43,97 +46,28 @@ macro_rules! bb{ }; } -#[derive(Default, Clone)] -struct VarkAttr { - help_break: bool, - literal: Option, - id: Option, +#[derive(Default, Clone, FromDeriveInput)] +#[darling(attributes(vark))] +#[darling(default)] +struct TypeAttr { + break_help: bool, + placeholder: Option, } -fn get_vark(attrs: &Vec) -> Result { - let mut help_break = false; - let mut literal = None; - let mut id = None; - for a in attrs { - match &a.meta { - Meta::List(list) if list.path.is_ident("vark") => { - let nested = list.parse_args_with(Punctuated::::parse_terminated).unwrap(); - for m in nested { - match &m { - Meta::Path(k) => { - match k.to_token_stream().to_string().as_str() { - "stop" => { - help_break = true; - }, - i => { - return Err( - syn::Error::new( - k.span(), - format!("Unexpected argument in `vark` attr: {:?}", i), - ), - ); - }, - } - }, - Meta::List(_) => { - return Err( - syn::Error::new( - m.span(), - format!("Unexpected tokens in `vark` attr arguments: {:?}", m.to_token_stream()), - ), - ); - }, - Meta::NameValue(kv) => { - match kv.path.require_ident()?.to_string().as_str() { - "literal" => { - literal = Some(match &kv.value { - Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) => s.value(), - l => return Err( - syn::Error::new( - kv.value.span(), - format!( - "`vark` `literal` argument must be a string, got {}", - l.to_token_stream() - ), - ), - ), - }); - }, - "id" => { - id = Some(match &kv.value { - Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) => s.value(), - l => return Err( - syn::Error::new( - kv.value.span(), - format!( - "`vark` `id` argument must be a string, got {}", - l.to_token_stream() - ), - ), - ), - }); - }, - other => { - return Err( - syn::Error::new( - kv.value.span(), - format!("Unespected argument in `vark` attr: {:?}", other), - ), - ); - }, - } - }, - } - } - }, - _ => { }, - } - } - return Ok(VarkAttr { - help_break: help_break, - literal: literal, - id: id, - }); +#[derive(Default, Clone, FromField)] +#[darling(default)] +struct FieldAttr { + help_stop: bool, + #[darling(multiple)] + flag: Vec, + placeholder: Option, +} + +#[derive(Default, Clone, FromVariant)] +#[darling(default)] +struct VariantAttr { + break_help: bool, + name: Option, } fn get_docstr(attrs: &Vec) -> String { @@ -180,7 +114,7 @@ fn gen_impl_type(ty: &Type, path: &str) -> GenRec { "TUPLE", "", 0, - t.elems.iter().map(|e| (VarkAttr::default(), String::new(), e)).collect::>().as_slice(), + t.elems.iter().map(|e| (FieldAttr::default(), String::new(), e)).collect::>().as_slice(), ); }, _ => panic!("Unsupported type {} in {}", ty.to_token_stream(), path), @@ -194,14 +128,14 @@ fn gen_impl_unnamed( help_placeholder: &str, help_docstr: &str, subtype_index: usize, - d: &[(VarkAttr, String, &Type)], + fields: &[(FieldAttr, String, &Type)], ) -> GenRec { let mut parse_positional = vec![]; let mut copy_fields = vec![]; let mut help_fields = vec![]; let mut help_field_patterns = vec![]; - let help_unit_transparent = d.len() == 1 && d[0].1.is_empty(); - for (i, (field_vark_attr, field_help_docstr, field_ty)) in d.iter().enumerate() { + let help_unit_transparent = fields.len() == 1 && fields[0].1.is_empty(); + for (i, (field_vark_attr, field_help_docstr, field_ty)) in fields.iter().enumerate() { let eof_code = if i == 0 { quote!{ break R::EOF; @@ -214,7 +148,7 @@ fn gen_impl_unnamed( let f_ident = format_ident!("v{}", i); let gen = gen_impl_type(field_ty, path); let vark = gen.vark; - let placeholder = field_vark_attr.id.clone().unwrap_or_else(|| { + let placeholder = field_vark_attr.placeholder.clone().unwrap_or_else(|| { let mut placeholder = vec![]; let mut placeholder_i = i; loop { @@ -260,7 +194,7 @@ fn gen_impl_unnamed( #(#parse_positional) * break state.r_ok(#ident(#(#copy_fields), *)); } }, - help_pattern: if d.is_empty() { + help_pattern: if fields.is_empty() { quote!{ aargvark::HelpPattern(vec![]) } @@ -288,13 +222,42 @@ fn gen_impl_unnamed( }; } +fn get_optional_type(t: &Type) -> Option<&Type> { + let Type::Path(t) = &t else { + return None; + }; + if t.qself.is_some() { + return None; + } + if t.path.leading_colon.is_some() { + return None; + } + if t.path.segments.len() != 1 { + return None; + } + let s = t.path.segments.first().unwrap(); + if &s.ident.to_string() != "Option" { + return None; + } + let syn::PathArguments::AngleBracketed(a) = &s.arguments else { + return None; + }; + if a.args.len() != 1 { + return None; + } + let syn::GenericArgument::Type(t) = &a.args[0] else { + return None; + }; + return Some(t); +} + fn gen_impl_struct( parent_ident: TokenStream, ident: TokenStream, decl_generics: &TokenStream, forward_generics: &TokenStream, help_placeholder: &str, - vark_attr: &VarkAttr, + type_break_help: bool, help_docstr: &str, subtype_index: usize, d: &Fields, @@ -303,117 +266,121 @@ fn gen_impl_struct( Fields::Named(d) => { let mut help_fields = vec![]; let mut partial_help_fields = vec![]; - let mut vark_optional_fields = vec![]; - let mut vark_optional_fields_default = vec![]; - let mut vark_parse_optional_cases = vec![]; + let mut vark_flag_fields = vec![]; + let mut vark_flag_fields_default = vec![]; + let mut vark_parse_flag_cases = vec![]; let mut vark_parse_positional = vec![]; - let mut vark_copy_fields = vec![]; + let mut vark_copy_flag_fields = vec![]; + let mut seen_flags = HashSet::new(); let mut required_i = 0usize; 'next_field: for (i, f) in d.named.iter().enumerate() { - let field_vark_attr = get_vark(&f.attrs)?; + let field_vark_attr = FieldAttr::from_field(f)?; let field_help_docstr = get_docstr(&f.attrs); let field_ident = f.ident.as_ref().expect("Named field missing name"); let f_local_ident = format_ident!("v{}", i); - // If an optional field, generate opt parsers and skip positional parsing + // If a flag (non-positional) field, generate parsers and skip positional parsing bb!{ - 'not_optional _; - let ty; - { - let Type::Path(t) = &f.ty else { - break 'not_optional; - }; - if t.qself.is_some() { - break 'not_optional; - } - if t.path.leading_colon.is_some() { - break 'not_optional; - } - if t.path.segments.len() != 1 { - break 'not_optional; - } - let s = t.path.segments.first().unwrap(); - if &s.ident.to_string() != "Option" { - break 'not_optional; - } - let syn::PathArguments::AngleBracketed(a) = &s.arguments else { - break 'not_optional; - }; - if a.args.len() != 1 { - break 'not_optional; + 'no_flags _; + let mut flags = field_vark_attr.flag.clone(); + if flags.is_empty() { + flags.push(format!("--{}", field_ident.to_string().to_case(Case::Kebab))); + } + for flag in &flags { + if !seen_flags.insert(flag.clone()) { + return Err( + syn::Error::new(f.span(), format!("Duplicate flag [{}] in [{}]", flag, ident)), + ); } - let syn::GenericArgument::Type(t) = &a.args[0] else { - break 'not_optional; - }; - ty = t; } - let flag = - format!( - "--{}", - field_vark_attr - .literal - .clone() - .unwrap_or_else(|| field_ident.to_string().to_case(Case::Kebab)) - ); - vark_optional_fields.push(quote!{ + let flags_string = format!("{:?}", flags); + let ty; + let copy; + let optional; + if let Some(ty1) = get_optional_type(&f.ty) { + ty = ty1; + copy = quote!(flag_fields.#field_ident); + optional = true; + } + else if ! field_vark_attr.flag.is_empty() { + ty = &f.ty; + copy = quote!(if let Some(f) = flag_fields.#field_ident { + f + } else { + return Err( + format!("One flag of the following flag set {} must be specified.", #flags_string), + ); + }); + optional = false; + } + else { + break 'no_flags; + } + vark_flag_fields.push(quote!{ #field_ident: Option < #ty >, }); - vark_optional_fields_default.push(quote!{ + vark_flag_fields_default.push(quote!{ #field_ident: None, }); - vark_copy_fields.push(quote!{ - #field_ident: optional.#field_ident + vark_copy_flag_fields.push(quote!{ + #field_ident: #copy }); let gen = gen_impl_type(ty, &field_ident.to_string()); let vark = gen.vark; - vark_parse_optional_cases.push(quote!{ - #flag => { - if optional.#field_ident.is_some() { - return state.r_err(format!("The argument {} was already specified", #flag)); - } - state.consume(); - let #f_local_ident = match #vark { - R:: Ok(v) => { - v - }, - R:: Err => { - return R::Err; - }, - R:: EOF => { - return state.r_err(format!("Missing argument for {}", #flag)); + for flag in &flags { + vark_parse_flag_cases.push(quote!{ + #flag => { + if flag_fields.#field_ident.is_some() { + return state.r_err(format!("The argument {} was already specified", #flag)); } - }; - optional.#field_ident = Some(#f_local_ident); - return R::Ok(true); - } - }); + state.consume(); + let #f_local_ident = match #vark { + R:: Ok(v) => { + v + }, + R:: Err => { + return R::Err; + }, + R:: EOF => { + return state.r_err(format!("Missing argument for {}", #flag)); + } + }; + flag_fields.#field_ident = Some(#f_local_ident); + return R::Ok(true); + } + }); + } let field_help_pattern; - if vark_attr.help_break || field_vark_attr.help_break { + if type_break_help || field_vark_attr.help_stop { field_help_pattern = quote!(aargvark::HelpPattern(vec![])); } else { field_help_pattern = gen.help_pattern; } let help_field = quote!{ - aargvark:: HelpOptionalField { - literal: #flag.to_string(), + aargvark:: HelpFlagField { + option: #optional, + flags: vec ![#(#flags.to_string()), *], pattern: #field_help_pattern, description: #field_help_docstr.to_string(), } }; help_fields.push(quote!{ - struct_.optional_fields.push(#help_field); + struct_.flag_fields.push(#help_field); }); partial_help_fields.push(quote!{ - if optional.#field_ident.is_none() { - optional_fields.push(#help_field); + if flag_fields.#field_ident.is_none() { + help_flag_fields.push(#help_field); } }); continue 'next_field; }; // Positional/required parsing - let field_help_placeholder = field_vark_attr.id.unwrap_or_else(|| field_ident.to_string().to_case(Case::UpperKebab)); + let field_help_placeholder = + field_vark_attr + .placeholder + .unwrap_or_else(|| field_ident.to_string().to_case(Case::UpperKebab)); let eof_code = if required_i == 0 { quote!{ break R::EOF; @@ -434,11 +401,11 @@ fn gen_impl_struct( aargvark:: show_help_and_exit(state, | state | { return aargvark:: HelpPartialProduction { description: #help_docstr.to_string(), - content: build_partial_help(state, #required_i, &optional), + content: build_partial_help(state, #required_i, &flag_fields), }; }); }, - PeekR:: Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { + PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { R:: Ok(v) => { v }, @@ -467,7 +434,7 @@ fn gen_impl_struct( } }; }); - vark_copy_fields.push(quote!{ + vark_copy_flag_fields.push(quote!{ #field_ident: #f_local_ident }); let help_field = quote!{ @@ -482,7 +449,7 @@ fn gen_impl_struct( }); partial_help_fields.push(quote!{ if required_i <= #required_i { - fields.push(#help_field); + help_fields.push(#help_field); } }); required_i += 1; @@ -492,19 +459,19 @@ fn gen_impl_struct( let vark = quote!{ { loop { - struct Optional #decl_generics { - #(#vark_optional_fields) * + #[derive(Default)] struct FlagFields #decl_generics { + #(#vark_flag_fields) * } - let mut optional = Optional { - #(#vark_optional_fields_default) * + let mut flag_fields = FlagFields { + #(#vark_flag_fields_default) * }; - fn parse_optional #decl_generics( - optional:& mut Optional #forward_generics, + fn parse_flags #decl_generics( + flag_fields:& mut FlagFields #forward_generics, state:& mut aargvark:: VarkState, s: String ) -> R < bool > { match s.as_str() { - #(#vark_parse_optional_cases) * + #(#vark_parse_flag_cases) * //. . _ => return R:: Ok(false), }; @@ -512,17 +479,17 @@ fn gen_impl_struct( fn build_partial_help #decl_generics( state:& mut aargvark:: HelpState, required_i: usize, - optional:& Optional #forward_generics, + flag_fields:& FlagFields #forward_generics, ) -> aargvark:: HelpPartialContent { - let mut fields = vec![]; - let mut optional_fields = vec![]; + let mut help_fields = vec![]; + let mut help_flag_fields = vec![]; #(#partial_help_fields) * //. . - return aargvark:: HelpPartialContent:: struct_(fields, optional_fields); + return aargvark:: HelpPartialContent:: struct_(help_fields, help_flag_fields); } #(#vark_parse_positional) * // Parse any remaining optional args - let opt_search_res = loop { + let flag_search_res = loop { match state.peek() { PeekR:: None => { break state.r_ok(()); @@ -531,11 +498,11 @@ fn gen_impl_struct( aargvark:: show_help_and_exit(state, | state | { return aargvark:: HelpPartialProduction { description: #help_docstr.to_string(), - content: build_partial_help(state, #required_i, &optional), + content: build_partial_help(state, #required_i, &flag_fields), }; }); }, - PeekR:: Ok(s) => match parse_optional(&mut optional, state, s.to_string()) { + PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { R:: Ok(v) => { if !v { break state.r_ok(()); @@ -550,7 +517,7 @@ fn gen_impl_struct( }, }; }; - match opt_search_res { + match flag_search_res { R::Ok(()) => { }, R::Err => { break R::Err; @@ -561,7 +528,7 @@ fn gen_impl_struct( }; // Build obj + return break state.r_ok(#ident { - #(#vark_copy_fields), + #(#vark_copy_flag_fields), * }); } @@ -591,7 +558,7 @@ fn gen_impl_struct( Fields::Unnamed(d) => { let mut fields = vec![]; for f in &d.unnamed { - fields.push((get_vark(&f.attrs)?, get_docstr(&f.attrs), &f.ty)); + fields.push((FieldAttr::from_field(f)?, get_docstr(&f.attrs), &f.ty)); } return Ok( gen_impl_unnamed( @@ -620,6 +587,8 @@ fn gen_impl_struct( fn gen_impl(ast: syn::DeriveInput) -> Result { let ident = &ast.ident; + let type_attr = TypeAttr::from_derive_input(&ast)?; + let help_docstr = get_docstr(&ast.attrs); let decl_generics = ast.generics.to_token_stream(); let forward_generics; { @@ -637,9 +606,8 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { forward_generics = quote!(< #(#parts), *>); } } - let vark_attr = get_vark(&ast.attrs)?; - let help_docstr = get_docstr(&ast.attrs); - let help_placeholder = vark_attr.id.clone().unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); + let help_placeholder = + type_attr.placeholder.clone().unwrap_or_else(|| ident.to_string().to_case(Case::UpperKebab)); let vark; let help_build; match &ast.data { @@ -651,7 +619,7 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { &decl_generics, &forward_generics, &help_placeholder, - &vark_attr, + type_attr.break_help, &help_docstr, 0, &d.fields, @@ -664,12 +632,12 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { let mut vark_cases = vec![]; let mut help_variants = vec![]; for (subtype_index, v) in d.variants.iter().enumerate() { - let variant_vark_attr = get_vark(&v.attrs)?; + let variant_vark_attr = VariantAttr::from_variant(v)?; let variant_help_docstr = get_docstr(&v.attrs); let variant_ident = &v.ident; let name_str = variant_vark_attr - .literal + .name .clone() .unwrap_or_else(|| variant_ident.to_string().to_case(Case::Kebab)); let gen = @@ -679,7 +647,7 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { &decl_generics, &forward_generics, &name_str, - &variant_vark_attr, + variant_vark_attr.break_help, "", subtype_index + 1, &v.fields, @@ -694,7 +662,7 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { } }); let help_variant_pattern; - if vark_attr.help_break || variant_vark_attr.help_break { + if type_attr.break_help || variant_vark_attr.break_help { help_variant_pattern = quote!(aargvark::HelpPattern(vec![])); } else { help_variant_pattern = partial_help_variant_pattern; diff --git a/readme.md b/readme.md index 333f273..49e8e18 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,10 @@ -Self-similar derive-based command line argument parsing, in the same genre as Clap-derive. It currently supports +A simple and consistent derive-based command line argument parsing, in the same genre as Clap-derive. It currently supports - Command line parsing - Help +Generally speaking this is intended to provide flexible, clear and consistent command line parsing, rather than poweruser-optimized minimal-length parsing. + This attempts to support parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. Just because you can doesn't mean you should. ``` @@ -32,15 +34,14 @@ $ Why this and not Clap? +- It has a super-simple interface (just `#[derive(Aargvark)]` on any enum/structure) - This parses more complex data types, like vectors of sub-structures, or enums - It's more consistent -- It has a super-simple interface (just `#[derive(Aargvark)]`) Why not this? - Some command line parsing conventions were discarded in order to simplify and maintain self-similarity. A lot of command line conventions are inconsistent or break down as you nest things, after all. - Quirky CLI parsing generally isn't supported: Some tricks (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. (Other things just haven't been implemented yet due to lack of time) -- Alpha # Conventions and usage @@ -68,9 +69,9 @@ To parse command line arguments let args = aargvark::vark::(); ``` -Optional fields in structs become optional (`--long`) arguments. If you want a `bool` long option that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. +Optional fields in structs become optional (`--long`) arguments. If you want a `bool` flag that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. -You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, `Map` with `FromString` keys and values as `K=V` arguments, most `Ip` and `SocketAddr` types, and `PathBuf` provided. +You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, `Map` with `FromString` keys and values as `K=V` arguments, most `Ip` and `SocketAddr` types, and `PathBuf` built in. Some additional wrappers are provided for automatically loading (and parsing) files: @@ -78,7 +79,7 @@ Some additional wrappers are provided for automatically loading (and parsing) fi - `AargvarkJson` requires feature `serde_json` - `AargvarkYaml` requires feature `serde_yaml` -To parse your own types, implement `AargvarkTrait`, or if your type takes a single string argument you can implement `AargvarkFromStr`. +To parse your own types, implement `AargvarkTrait`, or if your type takes a single string argument you can implement `AargvarkFromStr` which is slightly simpler. # Advanced usage @@ -88,14 +89,18 @@ To parse your own types, implement `AargvarkTrait`, or if your type takes a sing - Prevent recursion in help - Add `#[vark(stop)]` to a type, field, or variant to prevent recursing into any of the children. This is useful for subcommand enums - attach this to the enum and it will list the arguments but not the arguments' arguments (unless you do `-h` after specifying one on the command line). + Add `#[vark(break_help)]` to a _type_, _field_, or _variant_ to prevent recursing into any of the children when displaying help. This is useful for subcommand enums - attach this to the enum and it will list the variants but not the variants' arguments (unless you do `-h` after specifying one on the command line). + +- Use flags, replace flags, and add additional flags + + Add ex: `#[vark(flag="--target-machine", flag="-tm")]` to a _field_. -- Rename enum variants and option command line flags + If the field was optional, this will replace the default flag. If the field was non-optional, this will make it require a flag instead of being positional. - Ex: you have a field `xyz_id: i32` and you want the flag literal to be `--target-machine`. +- Rename enum variants - Add `#[vark(literal="--target-machine")]` to t field. + Add ex: `#[vark(name="my-variant")]` to a _variant_. -- Change the documentation placeholder (id) string +- Change the help placeholder string - Add `#[vark(id="x")]` to a field. + Add `#[vark(id="TARGET-MACHINE")]` to a _type_, _field_, or _variant_. diff --git a/src/lib.rs b/src/lib.rs index 9009aee..23fc460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![doc= include_str !("../readme.md")] +#![doc = include_str!("../readme.md")] use std::{ any::TypeId, @@ -620,7 +620,7 @@ fn style_literal(s: impl AsRef) -> String { return console::Style::new().bold().apply_to(s.as_ref()).to_string(); } -#[derive(Hash, PartialEq, Eq, Clone, Copy)] +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] pub struct HelpProductionKey { type_id: TypeId, variant: usize, @@ -638,11 +638,11 @@ pub enum HelpPartialContent { } impl HelpPartialContent { - pub fn struct_(fields: Vec, optional_fields: Vec) -> Self { + pub fn struct_(fields: Vec, optional_fields: Vec) -> Self { return HelpPartialContent::Production( HelpProductionType::Struct(Rc::new(RefCell::new(HelpProductionTypeStruct { fields: fields, - optional_fields: optional_fields, + flag_fields: optional_fields, }))), ); } @@ -664,7 +664,7 @@ pub enum HelpProductionType { pub struct HelpProductionTypeStruct { pub fields: Vec, - pub optional_fields: Vec, + pub flag_fields: Vec, } pub struct HelpField { @@ -673,8 +673,9 @@ pub struct HelpField { pub description: String, } -pub struct HelpOptionalField { - pub literal: String, +pub struct HelpFlagField { + pub option: bool, + pub flags: Vec, pub pattern: HelpPattern, pub description: String, } @@ -685,7 +686,7 @@ pub struct HelpVariant { pub description: String, } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct HelpPattern(pub Vec); impl HelpPattern { @@ -701,7 +702,7 @@ impl HelpPattern { } } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum HelpPatternElement { Literal(String), Type(String), @@ -737,6 +738,7 @@ impl HelpPatternElement { } } +#[derive(Default)] pub struct HelpState { // Write during building name_counter: HashMap, @@ -779,7 +781,7 @@ impl HelpState { ) -> (HelpProductionKey, Rc>) { let out = Rc::new(RefCell::new(HelpProductionTypeStruct { fields: vec![], - optional_fields: vec![], + flag_fields: vec![], })); let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Struct(out.clone())); return (key, out); @@ -823,7 +825,7 @@ pub fn show_help_and_exit< out.push_str(" "); out.push_str(&style_id(&f.id)); } - if !struct_.optional_fields.is_empty() { + if !struct_.flag_fields.is_empty() { out.push_str(" "); out.push_str(&style_logical("[ ...OPT]")); } @@ -862,17 +864,20 @@ pub fn show_help_and_exit< ], ); } - for f in &struct_.optional_fields { - let mut elems = vec![HelpPatternElement::Literal(f.literal.clone())]; - elems.extend(f.pattern.0.clone()); + for f in &struct_.flag_fields { + let mut left_col = vec![]; + for flag in &f.flags { + let mut elems = vec![HelpPatternElement::Literal(flag.clone())]; + elems.extend(f.pattern.0.clone()); + left_col.push(format!(" {}", if f.option { + HelpPattern(vec![HelpPatternElement::Option(HelpPattern(elems))]) + } else { + HelpPattern(elems) + }.render(stack, help_state))); + } table.add_row( vec![ - comfy_table::Cell::new( - format!( - " {}", - HelpPatternElement::Option(HelpPattern(elems)).render(stack, help_state) - ), - ), + comfy_table::Cell::new(left_col.join("\n")), Cell::new(style_description(&f.description)) ], ); @@ -899,10 +904,7 @@ pub fn show_help_and_exit< out.push_str("\n\n"); } - let mut help_state = HelpState { - name_counter: HashMap::new(), - productions: HashMap::new(), - }; + let mut help_state = HelpState::default(); let mut stack = Vec::<(HelpProductionKey, Rc)>::new(); let mut seen_productions = HashSet::::new(); let partial = build_root(&mut help_state); diff --git a/tests/test.rs b/tests/test.rs index 2fa3322..79ee88f 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -3,6 +3,8 @@ use aargvark::{ self, vark_explicit, AargvarkTrait, + HelpPatternElement, + HelpState, }; use aargvark_proc_macros::Aargvark; @@ -146,13 +148,34 @@ fn t_docstring() { #[test] fn t_varkattr() { #[derive(Aargvark, PartialEq, Debug)] - #[vark(stop)] + #[vark(break_help)] struct Naya { + // `id` unused here, must go on struct/enum (TODO - validate) #[vark(id = "G")] - #[vark(literal = "g")] + #[vark(flag = "--g")] f: Option, } let v: Naya = vark_explicit(None, svec!["--g", "3"]).unwrap(); assert_eq!(v, Naya { f: Some(3) }); + assert_eq!( + Naya::build_help_pattern(&mut HelpState::default()).0, + vec![HelpPatternElement::Literal("--g".to_string()), HelpPatternElement::Type("INT".to_string())] + ); +} + +#[test] +fn t_flag_nonopt() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + b: String, + #[vark(flag = "--a")] + a: String, + } + + let v: Naya = vark_explicit(None, svec!["--a", "wowo", "noh"]).unwrap(); + assert_eq!(v, Naya { + b: "noh".into(), + a: "wowo".into(), + }); } From d17b15fb6d8a368f4bbe8e0881a1963ec3ecc4cb Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 22:49:41 +0900 Subject: [PATCH 30/58] Fix flag parsing --- Cargo.toml | 2 +- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 6 ++++-- readme.md | 2 +- src/lib.rs | 43 +++++++++++++++++++++++++++++------------- tests/test.rs | 4 ++-- 6 files changed, 39 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b13f30..b440530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "ISC" description = "Self-similar argument parsing" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 0cb4414..79dcd0f 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 320e352..ef5313b 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -55,6 +55,7 @@ struct TypeAttr { } #[derive(Default, Clone, FromField)] +#[darling(attributes(vark))] #[darling(default)] struct FieldAttr { help_stop: bool, @@ -64,6 +65,7 @@ struct FieldAttr { } #[derive(Default, Clone, FromVariant)] +#[darling(attributes(vark))] #[darling(default)] struct VariantAttr { break_help: bool, @@ -307,7 +309,7 @@ fn gen_impl_struct( copy = quote!(if let Some(f) = flag_fields.#field_ident { f } else { - return Err( + return state.r_err( format!("One flag of the following flag set {} must be specified.", #flags_string), ); }); @@ -459,7 +461,7 @@ fn gen_impl_struct( let vark = quote!{ { loop { - #[derive(Default)] struct FlagFields #decl_generics { + struct FlagFields #decl_generics { #(#vark_flag_fields) * } let mut flag_fields = FlagFields { diff --git a/readme.md b/readme.md index 49e8e18..0eca762 100644 --- a/readme.md +++ b/readme.md @@ -103,4 +103,4 @@ To parse your own types, implement `AargvarkTrait`, or if your type takes a sing - Change the help placeholder string - Add `#[vark(id="TARGET-MACHINE")]` to a _type_, _field_, or _variant_. + Add `#[vark(placeholder="TARGET-MACHINE")]` to a _type_, _field_, or _variant_. diff --git a/src/lib.rs b/src/lib.rs index 23fc460..c8ad2bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,17 @@ pub struct VarkState { errors: Vec, } +impl VarkState { + pub fn new(command: Option, args: Vec) -> Self { + return Self { + command: command, + args: args, + i: 0, + errors: vec![], + }; + } +} + impl VarkState { pub fn peek<'a>(&'a self) -> PeekR<'a> { if self.i >= self.args.len() { @@ -180,12 +191,7 @@ impl std::error::Error for Error { } /// Parse the explicitly passed in arguments - don't read application globals. The /// `command` is only used in help and error text. pub fn vark_explicit(command: Option, args: Vec) -> Result { - let mut state = VarkState { - command: command, - args: args, - i: 0, - errors: vec![], - }; + let mut state = VarkState::new(command, args); match T::vark(&mut state) { R::EOF => { return Err(Error { @@ -626,12 +632,14 @@ pub struct HelpProductionKey { variant: usize, } -struct HelpProduction { +#[doc(hidden)] +pub struct HelpProduction { id: String, description: String, content: HelpProductionType, } +#[doc(hidden)] pub enum HelpPartialContent { Pattern(HelpPattern), Production(HelpProductionType), @@ -690,7 +698,7 @@ pub struct HelpVariant { pub struct HelpPattern(pub Vec); impl HelpPattern { - fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { + pub fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { let mut out = String::new(); for (i, e) in self.0.iter().enumerate() { if i > 0 { @@ -800,9 +808,7 @@ impl HelpState { } } -pub fn show_help_and_exit< - F: FnOnce(&mut HelpState) -> HelpPartialProduction, ->(state: &VarkState, build_root: F) -> ! { +pub fn render_help HelpPartialProduction>(state: &VarkState, build_root: F) -> String { fn format_desc(out: &mut String, desc: &str) { if !desc.is_empty() { out.push_str( @@ -826,8 +832,13 @@ pub fn show_help_and_exit< out.push_str(&style_id(&f.id)); } if !struct_.flag_fields.is_empty() { + let all_opt = struct_.flag_fields.iter().all(|x| x.option); out.push_str(" "); - out.push_str(&style_logical("[ ...OPT]")); + if all_opt { + out.push_str(&style_logical("[ ...FLAGS]")); + } else { + out.push_str(&style_logical("...FLAGS")); + } } }, HelpProductionType::Enum(fields) => { @@ -959,6 +970,12 @@ pub fn show_help_and_exit< temp_stack.reverse(); stack.extend(temp_stack); } - print!("{}", out); + return out; +} + +pub fn show_help_and_exit< + F: FnOnce(&mut HelpState) -> HelpPartialProduction, +>(state: &VarkState, build_root: F) -> ! { + print!("{}", render_help(state, build_root)); exit(0); } diff --git a/tests/test.rs b/tests/test.rs index 79ee88f..d5a1443 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -150,9 +150,9 @@ fn t_varkattr() { #[derive(Aargvark, PartialEq, Debug)] #[vark(break_help)] struct Naya { - // `id` unused here, must go on struct/enum (TODO - validate) - #[vark(id = "G")] + #[vark(placeholder = "G")] #[vark(flag = "--g")] + /// Do a thing f: Option, } From 32cc09c15c2d4b3abec59d6f2b43a11d61dd43b6 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 22:56:34 +0900 Subject: [PATCH 31/58] Delete help test since it became unweildy --- tests/test.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test.rs b/tests/test.rs index d5a1443..f26d3c6 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -3,8 +3,6 @@ use aargvark::{ self, vark_explicit, AargvarkTrait, - HelpPatternElement, - HelpState, }; use aargvark_proc_macros::Aargvark; @@ -158,10 +156,6 @@ fn t_varkattr() { let v: Naya = vark_explicit(None, svec!["--g", "3"]).unwrap(); assert_eq!(v, Naya { f: Some(3) }); - assert_eq!( - Naya::build_help_pattern(&mut HelpState::default()).0, - vec![HelpPatternElement::Literal("--g".to_string()), HelpPatternElement::Type("INT".to_string())] - ); } #[test] From 67667408fd2b588a9be017ec4e2e69911d6e0ed0 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 23:07:01 +0900 Subject: [PATCH 32/58] Force an exact proc_macros version harder --- Cargo.toml | 4 ++-- proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b440530..10ea02f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.4.1" +version = "0.4.2" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "0.4.0" } +aargvark_proc_macros = { path = "proc_macros", version = "=1.0.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 79dcd0f..5e31ea3 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "0.4.1" +version = "1.0.0" edition = "2021" license = "ISC" description = "Helper crate for aargvark" From 75a470049e413f87dc4795705d638479738982b1 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 23:25:56 +0900 Subject: [PATCH 33/58] Fix variant placeholder + allow overriding, extra tests --- Cargo.toml | 2 +- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 7 ++++++- tests/test.rs | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10ea02f..6e760a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "=1.0.0" } +aargvark_proc_macros = { path = "proc_macros", version = "=2.0.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 5e31ea3..0abe7f6 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "1.0.0" +version = "2.0.0" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index ef5313b..96011b1 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -70,6 +70,7 @@ struct FieldAttr { struct VariantAttr { break_help: bool, name: Option, + placeholder: Option, } fn get_docstr(attrs: &Vec) -> String { @@ -642,13 +643,17 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { .name .clone() .unwrap_or_else(|| variant_ident.to_string().to_case(Case::Kebab)); + let help_placeholder = + variant_vark_attr + .placeholder + .unwrap_or_else(|| variant_ident.to_string().to_case(Case::UpperKebab)); let gen = gen_impl_struct( ident.to_token_stream(), quote!(#ident:: #variant_ident), &decl_generics, &forward_generics, - &name_str, + &help_placeholder, variant_vark_attr.break_help, "", subtype_index + 1, diff --git a/tests/test.rs b/tests/test.rs index f26d3c6..fea23d6 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -173,3 +173,37 @@ fn t_flag_nonopt() { a: "wowo".into(), }); } + +#[test] +fn t_flag_2_nonopt_ord1() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + #[vark(flag = "--a")] + a: String, + #[vark(flag = "--b")] + b: String, + } + + let v: Naya = vark_explicit(None, svec!["--a", "wowo", "--b", "noh"]).unwrap(); + assert_eq!(v, Naya { + a: "wowo".into(), + b: "noh".into(), + }); +} + +#[test] +fn t_flag_2_nonopt_ord2() { + #[derive(Aargvark, PartialEq, Debug)] + struct Naya { + #[vark(flag = "--a")] + a: String, + #[vark(flag = "--b")] + b: String, + } + + let v: Naya = vark_explicit(None, svec!["--b", "noh", "--a", "wowo"]).unwrap(); + assert_eq!(v, Naya { + a: "wowo".into(), + b: "noh".into(), + }); +} From bd5de29e876be6d5fb0b9da23f7f53a6ae1229a3 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 23:27:29 +0900 Subject: [PATCH 34/58] Forgot to bump root --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6e760a4..3a7194c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.4.2" +version = "0.4.3" edition = "2021" license = "ISC" description = "Self-similar argument parsing" From 4748d7fe60ded28ebd4bc98a01c02a95f6150077 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 24 Aug 2024 23:41:54 +0900 Subject: [PATCH 35/58] break_help attr typo --- Cargo.toml | 4 ++-- proc_macros/Cargo.toml | 2 +- proc_macros/mod.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a7194c..5600588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.4.3" +version = "0.4.4" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "=2.0.0" } +aargvark_proc_macros = { path = "proc_macros", version = "=3.0.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index 0abe7f6..e9bb02c 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "2.0.0" +version = "3.0.0" edition = "2021" license = "ISC" description = "Helper crate for aargvark" diff --git a/proc_macros/mod.rs b/proc_macros/mod.rs index 96011b1..2386e18 100644 --- a/proc_macros/mod.rs +++ b/proc_macros/mod.rs @@ -58,7 +58,7 @@ struct TypeAttr { #[darling(attributes(vark))] #[darling(default)] struct FieldAttr { - help_stop: bool, + break_help: bool, #[darling(multiple)] flag: Vec, placeholder: Option, @@ -354,7 +354,7 @@ fn gen_impl_struct( }); } let field_help_pattern; - if type_break_help || field_vark_attr.help_stop { + if type_break_help || field_vark_attr.break_help { field_help_pattern = quote!(aargvark::HelpPattern(vec![])); } else { From 808010ea7701e565d28762f8744deb73018720a7 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 28 Aug 2024 01:16:11 +0900 Subject: [PATCH 36/58] Bump genemichaels --- Cargo.toml | 4 ++-- proc_macros/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5600588..b11aae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark" -version = "0.4.4" +version = "0.4.5" edition = "2021" license = "ISC" description = "Self-similar argument parsing" @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "=3.0.0" } +aargvark_proc_macros = { path = "proc_macros", version = "=3.0.1" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/proc_macros/Cargo.toml b/proc_macros/Cargo.toml index e9bb02c..5341ec1 100644 --- a/proc_macros/Cargo.toml +++ b/proc_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aargvark_proc_macros" -version = "3.0.0" +version = "3.0.1" edition = "2021" license = "ISC" description = "Helper crate for aargvark" @@ -15,7 +15,7 @@ path = "mod.rs" [dependencies] convert_case = "0.6" darling = "0.20.10" -genemichaels-lib = "0.5.0-pre2" +genemichaels-lib = "0.5.1" proc-macro2 = "1" quote = "1" syn = "2" From bc9541e9cdc274fe75abe4ab91bc681f1a155610 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 28 Aug 2024 01:26:15 +0900 Subject: [PATCH 37/58] Reorg, bump genemichaels --- Cargo.toml | 30 ++++--------------- crates/aargvark/Cargo.toml | 28 +++++++++++++++++ {src => crates/aargvark/src}/lib.rs | 2 +- .../aargvark_proc_macros}/Cargo.toml | 12 ++++---- .../aargvark_proc_macros}/mod.rs | 0 crates/aargvark_tests/Cargo.toml | 6 ++++ .../aargvark_tests/tests}/test.rs | 15 ++++++---- 7 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 crates/aargvark/Cargo.toml rename {src => crates/aargvark/src}/lib.rs (99%) rename {proc_macros => crates/aargvark_proc_macros}/Cargo.toml (61%) rename {proc_macros => crates/aargvark_proc_macros}/mod.rs (100%) create mode 100644 crates/aargvark_tests/Cargo.toml rename {tests => crates/aargvark_tests/tests}/test.rs (96%) diff --git a/Cargo.toml b/Cargo.toml index b11aae1..f9a305b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,11 @@ -[package] -name = "aargvark" -version = "0.4.5" +[workspace] +resolver = "2" +members = ["crates/*"] + +[workspace.package] edition = "2021" license = "ISC" description = "Self-similar argument parsing" homepage = "https://github.com/andrewbaxter/aargvark" repository = "https://github.com/andrewbaxter/aargvark" readme = "readme.md" - -[workspace] -members = ["proc_macros"] - -[features] -default = [] -serde_json = ["dep:serde_json", "dep:serde"] -serde_yaml = ["dep:serde_yaml", "dep:serde"] -http_types = ["dep:http"] - -[dependencies] -aargvark_proc_macros = { path = "proc_macros", version = "=3.0.1" } -serde_json = { version = "1", optional = true } -serde_yaml = { version = "0", optional = true } -convert_case = "0.6" -comfy-table = { version = "7", features = ["custom_styling"] } -url = { version = "2", optional = true } -http = { version = "1", optional = true } -serde = { version = "1", optional = true } -console = "0.15" -textwrap = { version = "0.16", features = ["terminal_size"] } -unicode-width = "0.1" diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml new file mode 100644 index 0000000..1acb9e8 --- /dev/null +++ b/crates/aargvark/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "aargvark" +description = "Self-similar argument parsing" +version = "0.4.5" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true + +[features] +default = [] +serde_json = ["dep:serde_json", "dep:serde"] +serde_yaml = ["dep:serde_yaml", "dep:serde"] +http_types = ["dep:http"] + +[dependencies] +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.0.1" } +serde_json = { version = "1", optional = true } +serde_yaml = { version = "0", optional = true } +convert_case = "0.6" +comfy-table = { version = "7", features = ["custom_styling"] } +url = { version = "2", optional = true } +http = { version = "1", optional = true } +serde = { version = "1", optional = true } +console = "0.15" +textwrap = { version = "0.16", features = ["terminal_size"] } +unicode-width = "0.1" diff --git a/src/lib.rs b/crates/aargvark/src/lib.rs similarity index 99% rename from src/lib.rs rename to crates/aargvark/src/lib.rs index c8ad2bf..990d34a 100644 --- a/src/lib.rs +++ b/crates/aargvark/src/lib.rs @@ -1,4 +1,4 @@ -#![doc = include_str!("../readme.md")] +#![doc = include_str!("../../../readme.md")] use std::{ any::TypeId, diff --git a/proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml similarity index 61% rename from proc_macros/Cargo.toml rename to crates/aargvark_proc_macros/Cargo.toml index 5341ec1..82f3cab 100644 --- a/proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "aargvark_proc_macros" -version = "3.0.1" -edition = "2021" -license = "ISC" description = "Helper crate for aargvark" -homepage = "https://github.com/andrewbaxter/aargvark" -repository = "https://github.com/andrewbaxter/aargvark" -readme = "../readme.md" +version = "3.0.1" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true [lib] proc-macro = true diff --git a/proc_macros/mod.rs b/crates/aargvark_proc_macros/mod.rs similarity index 100% rename from proc_macros/mod.rs rename to crates/aargvark_proc_macros/mod.rs diff --git a/crates/aargvark_tests/Cargo.toml b/crates/aargvark_tests/Cargo.toml new file mode 100644 index 0000000..5b8a110 --- /dev/null +++ b/crates/aargvark_tests/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "aargvark_tests" +publish = false + +[dependencies] +aargvark = { path = "../aargvark" } diff --git a/tests/test.rs b/crates/aargvark_tests/tests/test.rs similarity index 96% rename from tests/test.rs rename to crates/aargvark_tests/tests/test.rs index fea23d6..9c7c2f2 100644 --- a/tests/test.rs +++ b/crates/aargvark_tests/tests/test.rs @@ -1,10 +1,13 @@ -use std::collections::HashMap; -use aargvark::{ - self, - vark_explicit, - AargvarkTrait, +extern crate aargvark; + +use { + std::collections::HashMap, + aargvark::{ + vark_explicit, + AargvarkTrait, + Aargvark, + }, }; -use aargvark_proc_macros::Aargvark; macro_rules! svec{ ($($l: literal), *) => { From 1e91b990dce0a5ae3502b1ac08997f5c239f6eba Mon Sep 17 00:00:00 2001 From: andrew <> Date: Wed, 28 Aug 2024 01:33:04 +0900 Subject: [PATCH 38/58] Hack around cargo being crazy with paths --- crates/aargvark/readme.md | 1 + crates/aargvark/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 120000 crates/aargvark/readme.md diff --git a/crates/aargvark/readme.md b/crates/aargvark/readme.md new file mode 120000 index 0000000..2e75a7b --- /dev/null +++ b/crates/aargvark/readme.md @@ -0,0 +1 @@ +../../readme.md \ No newline at end of file diff --git a/crates/aargvark/src/lib.rs b/crates/aargvark/src/lib.rs index 990d34a..c8ad2bf 100644 --- a/crates/aargvark/src/lib.rs +++ b/crates/aargvark/src/lib.rs @@ -1,4 +1,4 @@ -#![doc = include_str!("../../../readme.md")] +#![doc = include_str!("../readme.md")] use std::{ any::TypeId, From fd84de11d7944f8f590b0fc0ac539817c3b66c8b Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 10:58:12 +0900 Subject: [PATCH 39/58] More reorg, tweaking readme, help_break elipses --- crates/aargvark/src/lib.rs | 12 ++++++++++ crates/aargvark_proc_macros/Cargo.toml | 1 - .../{mod.rs => src/lib.rs} | 7 +++++- readme.md | 24 ++++++++++++------- 4 files changed, 34 insertions(+), 10 deletions(-) rename crates/aargvark_proc_macros/{mod.rs => src/lib.rs} (98%) diff --git a/crates/aargvark/src/lib.rs b/crates/aargvark/src/lib.rs index c8ad2bf..9d7484d 100644 --- a/crates/aargvark/src/lib.rs +++ b/crates/aargvark/src/lib.rs @@ -712,11 +712,20 @@ impl HelpPattern { #[derive(Clone, PartialEq, Eq, Debug)] pub enum HelpPatternElement { + // xyz - denotes literal text user must type Literal(String), + // `` - denotes type of data for user Type(String), + // XYZ - refers to another section of help output Reference(HelpProductionKey), + // Like reference, but the other section might not exist. Used for `...` when + // using `help_break` to limit output. + PseudoReference(String), + // `[XYZ]` - indicates a pattern is optional Option(HelpPattern), + // `XYZ[ ...]` - indicates a pattern can be repeated, space separated Array(HelpPattern), + // `xyz | abc` - indicates one of multiple patterns can be selected Variant(Vec), } @@ -730,6 +739,9 @@ impl HelpPatternElement { stack.push((*i, production.clone())); return style_id(production.id.as_str()) }, + HelpPatternElement::PseudoReference(key) => { + return style_id(key.as_str()) + }, HelpPatternElement::Option(i) => return format!( "{}{}{}", style_logical("["), diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 82f3cab..974a543 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -10,7 +10,6 @@ readme.workspace = true [lib] proc-macro = true -path = "mod.rs" [dependencies] convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/mod.rs b/crates/aargvark_proc_macros/src/lib.rs similarity index 98% rename from crates/aargvark_proc_macros/mod.rs rename to crates/aargvark_proc_macros/src/lib.rs index 2386e18..b0184e6 100644 --- a/crates/aargvark_proc_macros/mod.rs +++ b/crates/aargvark_proc_macros/src/lib.rs @@ -670,7 +670,12 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { }); let help_variant_pattern; if type_attr.break_help || variant_vark_attr.break_help { - help_variant_pattern = quote!(aargvark::HelpPattern(vec![])); + help_variant_pattern = + quote!( + aargvark::HelpPattern( + vec![aargvark::HelpPatternElement::PseudoReference("...".to_string())], + ) + ); } else { help_variant_pattern = partial_help_variant_pattern; } diff --git a/readme.md b/readme.md index 0eca762..c348b9b 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ A simple and consistent derive-based command line argument parsing, in the same - Command line parsing - Help -Generally speaking this is intended to provide flexible, clear and consistent command line parsing, rather than poweruser-optimized minimal-length parsing. +Generally speaking this is intended to make CLI parsing simple by virtue of being simple and consistent, rather than poweruser-optimized keypress-minimizing parsing. This attempts to support parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. Just because you can doesn't mean you should. @@ -40,6 +40,7 @@ Why this and not Clap? Why not this? +- It's newer, with fewer features and limited community ecosystem, extensions - Some command line parsing conventions were discarded in order to simplify and maintain self-similarity. A lot of command line conventions are inconsistent or break down as you nest things, after all. - Quirky CLI parsing generally isn't supported: Some tricks (like `-v` `-vv` `-vvv`) break patterns and probably won't ever be implemented. (Other things just haven't been implemented yet due to lack of time) @@ -56,10 +57,13 @@ To parse command line arguments 1. Define the data type you want to parse them into, like ```rust + /// General description for the command. #[derive(Aargvark)] struct MyArgs { + /// Field documentation. velociraptor: String, deadly: bool, + #[vark(flag = "-c", flag = "--color-pattern")] color_pattern: Option, } ``` @@ -83,13 +87,9 @@ To parse your own types, implement `AargvarkTrait`, or if your type takes a sing # Advanced usage -- Vecs +- Sequences, plural fields, Vecs - Vec elements are space separated. The way vec parsing works is it attempts to parse as many elements as possible. When parsing one element fails, it rewinds to after it parsed the last successful element and proceeds from the next field after the vec. - -- Prevent recursion in help - - Add `#[vark(break_help)]` to a _type_, _field_, or _variant_ to prevent recursing into any of the children when displaying help. This is useful for subcommand enums - attach this to the enum and it will list the variants but not the variants' arguments (unless you do `-h` after specifying one on the command line). + Sequence elements are space separated. The way sequence parsing works is it attempts to parse as many elements as possible. When parsing one element fails, it rewinds to after it parsed the last successful element and proceeds from the next field after the sequence. - Use flags, replace flags, and add additional flags @@ -97,10 +97,18 @@ To parse your own types, implement `AargvarkTrait`, or if your type takes a sing If the field was optional, this will replace the default flag. If the field was non-optional, this will make it require a flag instead of being positional. -- Rename enum variants +- Rename enum variant keys Add ex: `#[vark(name="my-variant")]` to a _variant_. + This changes the command line key used to select a variant. + +- Prevent recursion in help + + Add `#[vark(break_help)]` to a _type_, _field_, or _variant_ to prevent recursing into any of the children when displaying help. This is useful for subcommand enums - attach this to the enum and it will list the variants but not the variants' arguments (unless you do `-h` after specifying one on the command line). + - Change the help placeholder string Add `#[vark(placeholder="TARGET-MACHINE")]` to a _type_, _field_, or _variant_. + + This is the capitalized text (like XYZ) after an option that basically means "see XYZ section for more details" From 5059358117dd9dc80d91715254398ab92952d723 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 11:09:00 +0900 Subject: [PATCH 40/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index 1acb9e8..b4ea2f4 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.4.5" +version = "0.4.6" edition.workspace = true license.workspace = true homepage.workspace = true @@ -15,7 +15,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.0.1" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.0.2" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 974a543..ffe25f7 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.0.1" +version = "3.0.2" edition.workspace = true license.workspace = true homepage.workspace = true From f76c6d17a80e7e0f174fc6d2e53b38b6164f0568 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 11:13:44 +0900 Subject: [PATCH 41/58] More readme tweaks --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index c348b9b..fda6ee5 100644 --- a/readme.md +++ b/readme.md @@ -62,8 +62,8 @@ To parse command line arguments struct MyArgs { /// Field documentation. velociraptor: String, + #[vark(flag = "-d", flag = "--deadly")] deadly: bool, - #[vark(flag = "-c", flag = "--color-pattern")] color_pattern: Option, } ``` @@ -73,7 +73,7 @@ To parse command line arguments let args = aargvark::vark::(); ``` -Optional fields in structs become optional (`--long`) arguments. If you want a `bool` flag that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. +Non-optional fields become positional arguments unless you give them a flag with `#[vark(flag = "--flag")]`. Optional fields become optional (`--long`) arguments. If you want a `bool` flag that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. You can derive structs, enums, and tuples, and there are implementations for `Vec`, `HashSet`, `Map` with `FromString` keys and values as `K=V` arguments, most `Ip` and `SocketAddr` types, and `PathBuf` built in. From faf62e968e05ab913f94f8960702144a6a8e7569 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 11:44:48 +0900 Subject: [PATCH 42/58] More readme tweaks --- readme.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/readme.md b/readme.md index fda6ee5..993b30d 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,13 @@ +
+ +![Crates.io Version](https://img.shields.io/crates/v/aargvark) + + + +![docs.rs](https://img.shields.io/docsrs/aargvark) + +
+ A simple and consistent derive-based command line argument parsing, in the same genre as Clap-derive. It currently supports - Command line parsing From b97d31abeb4cdf343ed045f2d7940373e8ff8c4a Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 12:02:57 +0900 Subject: [PATCH 43/58] More readme tweaks --- readme.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 993b30d..a2f0879 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,7 @@ -
- -![Crates.io Version](https://img.shields.io/crates/v/aargvark) - - - -![docs.rs](https://img.shields.io/docsrs/aargvark) - -
+ + + +
crates.iodocs.rs
A simple and consistent derive-based command line argument parsing, in the same genre as Clap-derive. It currently supports From 1334b26163f8d573d0f7c7df0fe5de82ed9dff41 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 12:52:56 +0900 Subject: [PATCH 44/58] Update readme example --- readme.md | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/readme.md b/readme.md index a2f0879..47f6481 100644 --- a/readme.md +++ b/readme.md @@ -13,26 +13,45 @@ Generally speaking this is intended to make CLI parsing simple by virtue of bein This attempts to support parsing arbitrarily complex command line arguments. Like with Serde, you can combine structs, vecs, enums in any way you want. Just because you can doesn't mean you should. ``` -$ # This is an example help output, sans light ansi styling -$ spagh set -h -Usage: spagh set IDENTITY DATA +$ ; This is an example help output, sans light ansi styling +$ spagh -h +Usage: spagh COMMAND [ ...FLAGS] - IDENTITY: BACKED-IDENTITY-ARG Identity to publish as - DATA: | - Data to publish. Must be json in the structure `{KEY: {"ttl": MINUTES, "value": DATA}, ...}` + A small CLI for querying, publishing, and administrating spaghettinuum. -BACKED-IDENTITY-ARG: local | card + COMMAND: COMMAND + [--debug] - An identity with its associated secret. +COMMAND: ping | get | http | ssh | identity | publish | admin + + ping ... Simple liveness check + get ... Request values associated with provided identity and keys + from a resolver + http ... + ssh ... + identity ... Commands for managing identities + publish ... Commands for publishing data + admin ... Commands for node administration + +``` - local A file containing a generated key - card card PC/SC card with ED25519 key +``` +$ spagh publish set -h +Usage: spagh publish set IDENTITY DATA + + IDENTITY: IDENTITY-SECRET-ARG Identity to publish as + DATA: | - Data to publish. Must be json in the + structure `{KEY: {"ttl": MINUTES, "value": + DATA}, ...}`. `KEY` is a string that's a + dotted list of key segments, with `/` to + escape dots and escape characters. -card: PCSC-ID PIN +IDENTITY-SECRET-ARG: local + + An identity with its associated secret. - PCSC-ID: Card to register, using id per pcscd (not identity id) - PIN: Card pin + local A file containing a generated key -$ ``` # Why or why not From fee57660bdafc0e1e3ab881d6e824acc14961394 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 14:11:32 +0900 Subject: [PATCH 45/58] Return error instead of calling exit() for help in vark_explicit; doc improvements --- crates/aargvark/Cargo.toml | 3 + crates/aargvark/src/lib.rs | 441 ++++++++++++++----------- crates/aargvark_proc_macros/src/lib.rs | 102 +++--- crates/aargvark_tests/tests/test.rs | 69 +++- 4 files changed, 343 insertions(+), 272 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index b4ea2f4..41f4510 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -8,6 +8,9 @@ homepage.workspace = true repository.workspace = true readme.workspace = true +[package.metadata.docs.rs] +all-features = true + [features] default = [] serde_json = ["dep:serde_json", "dep:serde"] diff --git a/crates/aargvark/src/lib.rs b/crates/aargvark/src/lib.rs index 9d7484d..aab14e5 100644 --- a/crates/aargvark/src/lib.rs +++ b/crates/aargvark/src/lib.rs @@ -38,17 +38,25 @@ pub struct VarkFailure { pub error: String, } -#[doc(hidden)] +/// Return type enum (like `Result`) during parsing. pub enum R { + /// Ran out of arguments before parsing successfully completed. EOF, + /// Parsing failed due to incorrect arguments. Err, + /// Encountered `-h` or `--help` and aborted. + Help(Box HelpPartialProduction>), + /// Successfully parsed value. Ok(T), } -#[doc(hidden)] +/// Possible results of peeking the output. pub enum PeekR<'a> { + /// There's another argument Ok(&'a str), + /// No more arguments remain None, + /// The next argument is `-h` or `--help` - caller should return `R::Help`. Help, } @@ -72,6 +80,7 @@ impl VarkState { } impl VarkState { + /// Return the next argument without consuming it. pub fn peek<'a>(&'a self) -> PeekR<'a> { if self.i >= self.args.len() { return PeekR::None; @@ -83,22 +92,30 @@ impl VarkState { return PeekR::Ok(v); } + /// The argument the current argument pointer is pointing to. pub fn position(&self) -> usize { return self.i; } + /// Reset the agument pointer to an earlier argument (i.e. after consuming N + /// arguments but finding the required final argument missing). pub fn rewind(&mut self, i: usize) { self.i = i; } + /// Move the argument pointer to the next argument (ex: after inspecting it using + /// `peek`). pub fn consume(&mut self) { self.i += 1; } + /// Produce a "parse successful" return value. pub fn r_ok(&self, v: T) -> R { return R::Ok(v); } + /// Produce a "parse failed" return value (includes which argument was being + /// inspected when the failure occured). pub fn r_err(&mut self, text: String) -> R { self.errors.push(VarkFailure { arg_offset: self.i, @@ -109,8 +126,8 @@ impl VarkState { } pub enum ErrorDetail { - /// The command was empty - Empty, + /// The parser needed more command line arguments. + TooLittle, /// Fully parsed command but additional unconsumed arguments follow (offset of /// first unrecognized argument) TooMuch(usize), @@ -131,7 +148,7 @@ pub struct Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.detail { - ErrorDetail::Empty => { + ErrorDetail::TooLittle => { return "Missing arguments, use --help for more info".fmt(f); }, ErrorDetail::TooMuch(first) => { @@ -188,16 +205,201 @@ impl std::fmt::Debug for Error { impl std::error::Error for Error { } +/// State required for building a help string. +pub struct VarkRetHelp { + state: VarkState, + builder: Box HelpPartialProduction>, +} + +impl VarkRetHelp { + /// Build a help string using current operating environment line widths. + pub fn render(self) -> String { + fn format_desc(out: &mut String, desc: &str) { + if !desc.is_empty() { + out.push_str( + &style_description( + textwrap::wrap( + desc, + &textwrap::Options::with_termwidth().initial_indent(" ").subsequent_indent(" "), + ).join("\n"), + ), + ); + out.push_str("\n\n"); + } + } + + fn format_pattern(out: &mut String, content: &HelpProductionType) { + match content { + HelpProductionType::Struct(struct_) => { + let struct_ = struct_.borrow(); + for f in &struct_.fields { + out.push_str(" "); + out.push_str(&style_id(&f.id)); + } + if !struct_.flag_fields.is_empty() { + let all_opt = struct_.flag_fields.iter().all(|x| x.option); + out.push_str(" "); + if all_opt { + out.push_str(&style_logical("[ ...FLAGS]")); + } else { + out.push_str(&style_logical("...FLAGS")); + } + } + }, + HelpProductionType::Enum(fields) => { + for (i, f) in fields.borrow().iter().enumerate() { + if i > 0 { + out.push_str(" |"); + } + out.push_str(" "); + out.push_str(&style_literal(&f.literal)); + } + }, + } + } + + fn format_content( + out: &mut String, + stack: &mut Vec<(HelpProductionKey, Rc)>, + help_state: &HelpState, + content: &HelpProductionType, + ) { + let mut table = comfy_table::Table::new(); + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + match content { + HelpProductionType::Struct(struct_) => { + let struct_ = struct_.borrow(); + for f in &struct_.fields { + table.add_row( + vec![ + comfy_table::Cell::new( + format!(" {}: {}", style_id(&f.id), f.pattern.render(stack, help_state)), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + for f in &struct_.flag_fields { + let mut left_col = vec![]; + for flag in &f.flags { + let mut elems = vec![HelpPatternElement::Literal(flag.clone())]; + elems.extend(f.pattern.0.clone()); + left_col.push(format!(" {}", if f.option { + HelpPattern(vec![HelpPatternElement::Option(HelpPattern(elems))]) + } else { + HelpPattern(elems) + }.render(stack, help_state))); + } + table.add_row( + vec![ + comfy_table::Cell::new(left_col.join("\n")), + Cell::new(style_description(&f.description)) + ], + ); + } + }, + HelpProductionType::Enum(fields) => { + for f in &*fields.borrow() { + table.add_row( + vec![ + comfy_table::Cell::new( + format!( + " {} {}", + style_literal(&f.literal), + f.pattern.render(stack, help_state) + ), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + }, + } + table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { + lower: comfy_table::Width::Percentage(20), + upper: comfy_table::Width::Percentage(60), + }]); + out.push_str(&table.to_string()); + out.push_str("\n\n"); + } + + let mut help_state = HelpState::default(); + let mut stack = Vec::<(HelpProductionKey, Rc)>::new(); + let mut seen_productions = HashSet::::new(); + let partial = (self.builder)(&mut help_state); + + // Write initial partial production + let mut out = style_usage("Usage:"); + if let Some(s) = &self.state.command { + out.push_str(" "); + out.push_str(&style_usage(s)); + } + for s in self.state.args.iter().take(self.state.i) { + out.push_str(" "); + out.push_str(&style_usage(s)); + } + let mut temp_stack = vec![]; + match &partial.content { + HelpPartialContent::Pattern(p) => { + if !p.0.is_empty() { + out.push_str(" "); + out.push_str(&p.render(&mut temp_stack, &help_state)); + } + }, + HelpPartialContent::Production(content) => { + format_pattern(&mut out, &content); + }, + } + out.push_str("\n\n"); + format_desc(&mut out, &partial.description); + match &partial.content { + HelpPartialContent::Pattern(_) => { + out.push_str("\n\n"); + }, + HelpPartialContent::Production(content) => { + format_content(&mut out, &mut temp_stack, &mut help_state, content); + }, + } + temp_stack.reverse(); + stack.extend(temp_stack); + + // Recurse productions + while let Some((key, top)) = stack.pop() { + if !seen_productions.insert(key) { + continue; + } + out.push_str(&style_id(&top.id)); + out.push_str(":"); + format_pattern(&mut out, &top.content); + out.push_str("\n\n"); + format_desc(&mut out, &top.description); + let mut temp_stack = vec![]; + format_content(&mut out, &mut temp_stack, &mut help_state, &top.content); + temp_stack.reverse(); + stack.extend(temp_stack); + } + return out; + } +} + +/// Result of varking when no errors occurred. Either results in parsed value or +/// the parsing was interrupted because help was requested. +pub enum VarkRet { + Ok(T), + Help(VarkRetHelp), +} + /// Parse the explicitly passed in arguments - don't read application globals. The /// `command` is only used in help and error text. -pub fn vark_explicit(command: Option, args: Vec) -> Result { +pub fn vark_explicit(command: Option, args: Vec) -> Result, Error> { let mut state = VarkState::new(command, args); match T::vark(&mut state) { R::EOF => { return Err(Error { command: state.command, args: state.args, - detail: ErrorDetail::Empty, + detail: ErrorDetail::TooLittle, }); }, R::Err => { @@ -207,6 +409,12 @@ pub fn vark_explicit(command: Option, args: Vec { + return Ok(VarkRet::Help(VarkRetHelp { + state: state, + builder: builder, + })); + }, R::Ok(v) => { if state.i != state.args.len() { return Err(Error { @@ -215,17 +423,25 @@ pub fn vark_explicit(command: Option, args: Vec() -> T { let mut args = args(); let command = args.next(); match vark_explicit(command, args.collect::>()) { - Ok(v) => return v, + Ok(v) => match v { + VarkRet::Ok(v) => return v, + VarkRet::Help(h) => { + println!("{}", h.render()); + exit(0); + }, + }, Err(e) => { eprintln!("{:?}", e); exit(1); @@ -236,7 +452,11 @@ pub fn vark() -> T { /// Anything that implements this trait can be parsed and used as a field in other /// parsable enums/structs. pub trait AargvarkTrait: Sized { + /// Called when this argument is reached. Should parse data until no more data can + /// be parsed. fn vark(state: &mut VarkState) -> R; + + /// Called when `-h` is specified. fn build_help_pattern(state: &mut HelpState) -> HelpPattern; } @@ -251,14 +471,12 @@ impl AargvarkTrait for T { fn vark(state: &mut VarkState) -> R { let s = match state.peek() { PeekR::None => return R::EOF, - PeekR::Help => { - show_help_and_exit(state, |state| { - return HelpPartialProduction { - description: "".to_string(), - content: HelpPartialContent::Pattern(::build_help_pattern(state)), - }; - }); - }, + PeekR::Help => return R::Help(Box::new(|state| { + return HelpPartialProduction { + description: "".to_string(), + content: HelpPartialContent::Pattern(::build_help_pattern(state)), + }; + })), PeekR::Ok(s) => s, }; match T::from_str(s) { @@ -500,6 +718,7 @@ pub fn vark_from_iter>(state: &mut VarkStat out.push(v); rewind_to = state.position(); }, + R::Help(b) => return R::Help(b), R::Err | R::EOF => { state.rewind(rewind_to); return state.r_ok(C::from_iter(out.into_iter())); @@ -534,7 +753,7 @@ impl AargvarkTrait for HashSet { /// /// This is used for the `HashMap` implementation which takes a series of arguments /// like `a=a b=b c=123`. -struct AargvarkKV { +pub struct AargvarkKV { pub key: K, pub value: V, } @@ -545,6 +764,7 @@ impl AargvarkTrait for AargvarkKV let res = match res { R::EOF => return R::EOF, R::Err => return R::Err, + R::Help(b) => return R::Help(b), R::Ok(r) => r, }; let mut res = res.into_bytes().into_iter(); @@ -592,6 +812,7 @@ impl AargvarkTrait for HashM let res = match >>::vark(state) { R::EOF => return R::EOF, R::Err => return R::Err, + R::Help(b) => return R::Help(b), R::Ok(r) => r, }; return state.r_ok(res.into_iter().map(|kv| (kv.key, kv.value)).collect()); @@ -626,6 +847,7 @@ fn style_literal(s: impl AsRef) -> String { return console::Style::new().bold().apply_to(s.as_ref()).to_string(); } +#[doc(hidden)] #[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] pub struct HelpProductionKey { type_id: TypeId, @@ -660,27 +882,32 @@ impl HelpPartialContent { } } +/// State for a partially-parsed field for rendering help. pub struct HelpPartialProduction { pub description: String, pub content: HelpPartialContent, } +#[doc(hidden)] pub enum HelpProductionType { Struct(Rc>), Enum(Rc>>), } +#[doc(hidden)] pub struct HelpProductionTypeStruct { pub fields: Vec, pub flag_fields: Vec, } +#[doc(hidden)] pub struct HelpField { pub id: String, pub pattern: HelpPattern, pub description: String, } +#[doc(hidden)] pub struct HelpFlagField { pub option: bool, pub flags: Vec, @@ -688,12 +915,15 @@ pub struct HelpFlagField { pub description: String, } +#[doc(hidden)] pub struct HelpVariant { pub literal: String, pub pattern: HelpPattern, pub description: String, } +/// Structured help information - this list of pattern elements that is styled and +/// joined with spaces on output. #[derive(Clone, PartialEq, Eq, Debug)] pub struct HelpPattern(pub Vec); @@ -758,6 +988,7 @@ impl HelpPatternElement { } } +#[doc(hidden)] #[derive(Default)] pub struct HelpState { // Write during building @@ -819,175 +1050,3 @@ impl HelpState { return (key, out); } } - -pub fn render_help HelpPartialProduction>(state: &VarkState, build_root: F) -> String { - fn format_desc(out: &mut String, desc: &str) { - if !desc.is_empty() { - out.push_str( - &style_description( - textwrap::wrap( - desc, - &textwrap::Options::with_termwidth().initial_indent(" ").subsequent_indent(" "), - ).join("\n"), - ), - ); - out.push_str("\n\n"); - } - } - - fn format_pattern(out: &mut String, content: &HelpProductionType) { - match content { - HelpProductionType::Struct(struct_) => { - let struct_ = struct_.borrow(); - for f in &struct_.fields { - out.push_str(" "); - out.push_str(&style_id(&f.id)); - } - if !struct_.flag_fields.is_empty() { - let all_opt = struct_.flag_fields.iter().all(|x| x.option); - out.push_str(" "); - if all_opt { - out.push_str(&style_logical("[ ...FLAGS]")); - } else { - out.push_str(&style_logical("...FLAGS")); - } - } - }, - HelpProductionType::Enum(fields) => { - for (i, f) in fields.borrow().iter().enumerate() { - if i > 0 { - out.push_str(" |"); - } - out.push_str(" "); - out.push_str(&style_literal(&f.literal)); - } - }, - } - } - - fn format_content( - out: &mut String, - stack: &mut Vec<(HelpProductionKey, Rc)>, - help_state: &HelpState, - content: &HelpProductionType, - ) { - let mut table = comfy_table::Table::new(); - table.load_preset(comfy_table::presets::NOTHING); - table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); - match content { - HelpProductionType::Struct(struct_) => { - let struct_ = struct_.borrow(); - for f in &struct_.fields { - table.add_row( - vec![ - comfy_table::Cell::new( - format!(" {}: {}", style_id(&f.id), f.pattern.render(stack, help_state)), - ), - Cell::new(style_description(&f.description)) - ], - ); - } - for f in &struct_.flag_fields { - let mut left_col = vec![]; - for flag in &f.flags { - let mut elems = vec![HelpPatternElement::Literal(flag.clone())]; - elems.extend(f.pattern.0.clone()); - left_col.push(format!(" {}", if f.option { - HelpPattern(vec![HelpPatternElement::Option(HelpPattern(elems))]) - } else { - HelpPattern(elems) - }.render(stack, help_state))); - } - table.add_row( - vec![ - comfy_table::Cell::new(left_col.join("\n")), - Cell::new(style_description(&f.description)) - ], - ); - } - }, - HelpProductionType::Enum(fields) => { - for f in &*fields.borrow() { - table.add_row( - vec![ - comfy_table::Cell::new( - format!(" {} {}", style_literal(&f.literal), f.pattern.render(stack, help_state)), - ), - Cell::new(style_description(&f.description)) - ], - ); - } - }, - } - table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { - lower: comfy_table::Width::Percentage(20), - upper: comfy_table::Width::Percentage(60), - }]); - out.push_str(&table.to_string()); - out.push_str("\n\n"); - } - - let mut help_state = HelpState::default(); - let mut stack = Vec::<(HelpProductionKey, Rc)>::new(); - let mut seen_productions = HashSet::::new(); - let partial = build_root(&mut help_state); - - // Write initial partial production - let mut out = style_usage("Usage:"); - if let Some(s) = &state.command { - out.push_str(" "); - out.push_str(&style_literal(s)); - } - for s in state.args.iter().take(state.i) { - out.push_str(" "); - out.push_str(&style_literal(s)); - } - let mut temp_stack = vec![]; - match &partial.content { - HelpPartialContent::Pattern(p) => { - if !p.0.is_empty() { - out.push_str(" "); - out.push_str(&p.render(&mut temp_stack, &help_state)); - } - }, - HelpPartialContent::Production(content) => { - format_pattern(&mut out, content); - }, - } - out.push_str("\n\n"); - format_desc(&mut out, &partial.description); - match &partial.content { - HelpPartialContent::Pattern(_) => { - out.push_str("\n\n"); - }, - HelpPartialContent::Production(content) => { - format_content(&mut out, &mut temp_stack, &mut help_state, content); - }, - } - temp_stack.reverse(); - stack.extend(temp_stack); - - // Recurse productions - while let Some((key, top)) = stack.pop() { - if !seen_productions.insert(key) { - continue; - } - out.push_str(&style_id(&top.id)); - out.push_str(":"); - format_pattern(&mut out, &top.content); - out.push_str("\n\n"); - format_desc(&mut out, &top.description); - let mut temp_stack = vec![]; - format_content(&mut out, &mut temp_stack, &mut help_state, &top.content); - temp_stack.reverse(); - stack.extend(temp_stack); - } - return out; -} - -pub fn show_help_and_exit< - F: FnOnce(&mut HelpState) -> HelpPartialProduction, ->(state: &VarkState, build_root: F) -> ! { - print!("{}", render_help(state, build_root)); - exit(0); -} diff --git a/crates/aargvark_proc_macros/src/lib.rs b/crates/aargvark_proc_macros/src/lib.rs index b0184e6..53e9155 100644 --- a/crates/aargvark_proc_macros/src/lib.rs +++ b/crates/aargvark_proc_macros/src/lib.rs @@ -169,12 +169,9 @@ fn gen_impl_unnamed( let r = #vark; //. . let #f_ident = match r { - R:: Ok(v) => { - v - }, - R:: Err => { - break R::Err; - }, + R:: Ok(v) => v, + R:: Help(b) => break R:: Help(b), + R:: Err => break R:: Err, R:: EOF => { #eof_code } @@ -338,12 +335,9 @@ fn gen_impl_struct( } state.consume(); let #f_local_ident = match #vark { - R:: Ok(v) => { - v - }, - R:: Err => { - return R::Err; - }, + R:: Ok(v) => v, + R:: Help(b) => return R:: Help(b), + R:: Err => return R:: Err, R:: EOF => { return state.r_err(format!("Missing argument for {}", #flag)); } @@ -400,21 +394,16 @@ fn gen_impl_struct( let #f_local_ident = loop { if match state.peek() { PeekR:: None => false, - PeekR:: Help => { - aargvark:: show_help_and_exit(state, | state | { - return aargvark:: HelpPartialProduction { - description: #help_docstr.to_string(), - content: build_partial_help(state, #required_i, &flag_fields), - }; - }); - }, + PeekR:: Help => return R:: Help(Box:: new(move | state | { + return aargvark:: HelpPartialProduction { + description: #help_docstr.to_string(), + content: build_partial_help(state, #required_i, &flag_fields), + }; + })), PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { - R:: Ok(v) => { - v - }, - R:: Err => { - break R::Err; - }, + R:: Ok(v) => v, + R:: Help(b) => break R:: Help(b), + R:: Err => break R:: Err, R:: EOF => { unreachable!(); } @@ -426,12 +415,9 @@ fn gen_impl_struct( break #vark; }; let #f_local_ident = match #f_local_ident { - R:: Ok(v) => { - v - }, - R:: Err => { - break R::Err; - }, + R:: Ok(v) => v, + R:: Help(b) => break R:: Help(b), + R:: Err => break R:: Err, R:: EOF => { #eof_code } @@ -497,37 +483,29 @@ fn gen_impl_struct( PeekR:: None => { break state.r_ok(()); }, - PeekR:: Help => { - aargvark:: show_help_and_exit(state, | state | { - return aargvark:: HelpPartialProduction { - description: #help_docstr.to_string(), - content: build_partial_help(state, #required_i, &flag_fields), - }; - }); - }, + PeekR:: Help => return R:: Help(Box:: new(move | state | { + return aargvark:: HelpPartialProduction { + description: #help_docstr.to_string(), + content: build_partial_help(state, #required_i, &flag_fields), + }; + })), PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { R:: Ok(v) => { if !v { break state.r_ok(()); } }, - R:: Err => { - break R::Err; - }, - R:: EOF => { - unreachable!(); - }, + R:: Help(b) => break R:: Help(b), + R:: Err => break R:: Err, + R:: EOF => unreachable !(), }, }; }; match flag_search_res { R::Ok(()) => { }, - R::Err => { - break R::Err; - }, - R::EOF => { - unreachable!(); - }, + R::Help(b) => break R::Help(b), + R::Err => break R::Err, + R::EOF => unreachable!(), }; // Build obj + return break state.r_ok(#ident { @@ -691,17 +669,15 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { { let tag = match state.peek() { PeekR:: None => return R:: EOF, - PeekR:: Help => { - aargvark:: show_help_and_exit(state, | state | { - let mut variants = vec![]; - #(#help_variants) * - //. . - return aargvark:: HelpPartialProduction { - description: #help_docstr.to_string(), - content: aargvark:: HelpPartialContent:: enum_(variants), - }; - }); - }, + PeekR:: Help => return R:: Help(Box:: new(move | state | { + let mut variants = vec![]; + #(#help_variants) * + //. . + return aargvark:: HelpPartialProduction { + description: #help_docstr.to_string(), + content: aargvark:: HelpPartialContent:: enum_(variants), + }; + })), PeekR:: Ok(s) => s, }; match tag { diff --git a/crates/aargvark_tests/tests/test.rs b/crates/aargvark_tests/tests/test.rs index 9c7c2f2..a418b14 100644 --- a/crates/aargvark_tests/tests/test.rs +++ b/crates/aargvark_tests/tests/test.rs @@ -1,12 +1,13 @@ extern crate aargvark; use { - std::collections::HashMap, aargvark::{ vark_explicit, - AargvarkTrait, Aargvark, + AargvarkTrait, + VarkRet, }, + std::collections::HashMap, }; macro_rules! svec{ @@ -17,13 +18,17 @@ macro_rules! svec{ #[test] fn t_str() { - let v: String = vark_explicit(None, svec!["a"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["a"]).unwrap() else { + panic!(); + }; assert_eq!(v, "a"); } #[test] fn t_vec() { - let v: Vec = vark_explicit(None, svec!["a", "b"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::>(None, svec!["a", "b"]).unwrap() else { + panic!(); + }; assert_eq!(v, svec!["a", "b"]); } @@ -34,7 +39,9 @@ fn t_enum_unit() { ToqQuol, } - let v: Yol = vark_explicit(None, svec!["toq-quol"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["toq-quol"]).unwrap() else { + panic!(); + }; assert_eq!(v, Yol::ToqQuol); } @@ -45,7 +52,9 @@ fn t_enum_tuple() { ToqQuol(String, String), } - let v: Yol = vark_explicit(None, svec!["toq-quol", "yon", "nor"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["toq-quol", "yon", "nor"]).unwrap() else { + panic!(); + }; assert_eq!(v, Yol::ToqQuol("yon".into(), "nor".into())); } @@ -58,7 +67,9 @@ fn t_enum_struct() { }, } - let v: Yol = vark_explicit(None, svec!["toq-quol", "pahla"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["toq-quol", "pahla"]).unwrap() else { + panic!(); + }; assert_eq!(v, Yol::ToqQuol { a: "pahla".into() }); } @@ -69,7 +80,9 @@ fn t_struct() { a: String, } - let v: Naya = vark_explicit(None, svec!["wowo"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["wowo"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { a: "wowo".into() }); } @@ -80,7 +93,9 @@ fn t_struct_opt_only() { a: Option, } - let v: Naya = vark_explicit(None, svec!["--a", "wowo"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["--a", "wowo"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { a: Some("wowo".into()) }); } @@ -92,7 +107,9 @@ fn t_struct_opt_first() { a: Option, } - let v: Naya = vark_explicit(None, svec!["--a", "wowo", "noh"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["--a", "wowo", "noh"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { b: "noh".into(), a: Some("wowo".into()), @@ -107,7 +124,9 @@ fn t_struct_opt_last() { a: Option, } - let v: Naya = vark_explicit(None, svec!["noh", "--a", "wowo"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["noh", "--a", "wowo"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { b: "noh".into(), a: Some("wowo".into()), @@ -121,13 +140,17 @@ fn t_generic() { b: Option, } - let v: Naya = vark_explicit(None, svec!["--b", "hi"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::>(None, svec!["--b", "hi"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { b: Some("hi".to_string()) }); } #[test] fn t_map() { - let v = vark_explicit::>(None, svec!["a=2", "b=3"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::>(None, svec!["a=2", "b=3"]).unwrap() else { + panic!(); + }; assert_eq!(v, { let mut m = HashMap::new(); m.insert("a".to_string(), 2); @@ -142,7 +165,9 @@ fn t_docstring() { /// This is a naya struct Naya {} - let v: Naya = vark_explicit(None, svec![]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec![]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya {}); } @@ -157,7 +182,9 @@ fn t_varkattr() { f: Option, } - let v: Naya = vark_explicit(None, svec!["--g", "3"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["--g", "3"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { f: Some(3) }); } @@ -170,7 +197,9 @@ fn t_flag_nonopt() { a: String, } - let v: Naya = vark_explicit(None, svec!["--a", "wowo", "noh"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["--a", "wowo", "noh"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { b: "noh".into(), a: "wowo".into(), @@ -187,7 +216,9 @@ fn t_flag_2_nonopt_ord1() { b: String, } - let v: Naya = vark_explicit(None, svec!["--a", "wowo", "--b", "noh"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["--a", "wowo", "--b", "noh"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { a: "wowo".into(), b: "noh".into(), @@ -204,7 +235,9 @@ fn t_flag_2_nonopt_ord2() { b: String, } - let v: Naya = vark_explicit(None, svec!["--b", "noh", "--a", "wowo"]).unwrap(); + let VarkRet::Ok(v) = vark_explicit::(None, svec!["--b", "noh", "--a", "wowo"]).unwrap() else { + panic!(); + }; assert_eq!(v, Naya { a: "wowo".into(), b: "noh".into(), From 49a6f3bb5ba0102c9decc5e2adad7c7adae31fdd Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 14:12:33 +0900 Subject: [PATCH 46/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index 41f4510..c2a20d4 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.4.6" +version = "0.5.0" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.0.2" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.1.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index ffe25f7..6f34966 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.0.2" +version = "3.1.0" edition.workspace = true license.workspace = true homepage.workspace = true From eb3e82d874503896cd5974dc4e27c012d1427ad8 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 14:57:03 +0900 Subject: [PATCH 47/58] Break up root module --- crates/aargvark/src/base.rs | 179 +++++ crates/aargvark/src/help.rs | 415 ++++++++++ crates/aargvark/src/lib.rs | 1002 +----------------------- crates/aargvark/src/traits_impls.rs | 444 +++++++++++ crates/aargvark_proc_macros/src/lib.rs | 137 ++-- crates/aargvark_tests/tests/test.rs | 2 +- readme.md | 2 +- 7 files changed, 1130 insertions(+), 1051 deletions(-) create mode 100644 crates/aargvark/src/base.rs create mode 100644 crates/aargvark/src/help.rs create mode 100644 crates/aargvark/src/traits_impls.rs diff --git a/crates/aargvark/src/base.rs b/crates/aargvark/src/base.rs new file mode 100644 index 0000000..447acbe --- /dev/null +++ b/crates/aargvark/src/base.rs @@ -0,0 +1,179 @@ +use { + crate::help::{ + HelpPartialProduction, + HelpState, + }, + unicode_width::UnicodeWidthStr, +}; + +pub struct VarkFailure { + pub arg_offset: usize, + pub error: String, +} + +/// Return type enum (like `Result`) during parsing. +pub enum R { + /// Ran out of arguments before parsing successfully completed. + EOF, + /// Parsing failed due to incorrect arguments. + Err, + /// Encountered `-h` or `--help` and aborted. + Help(Box HelpPartialProduction>), + /// Successfully parsed value. + Ok(T), +} + +/// Possible results of peeking the output. +pub enum PeekR<'a> { + /// There's another argument + Ok(&'a str), + /// No more arguments remain + None, + /// The next argument is `-h` or `--help` - caller should return `R::Help`. + Help, +} + +#[doc(hidden)] +pub struct VarkState { + pub(crate) command: Option, + pub(crate) args: Vec, + pub(crate) i: usize, + pub(crate) errors: Vec, +} + +impl VarkState { + pub fn new(command: Option, args: Vec) -> Self { + return Self { + command: command, + args: args, + i: 0, + errors: vec![], + }; + } +} + +impl VarkState { + /// Return the next argument without consuming it. + pub fn peek<'a>(&'a self) -> PeekR<'a> { + if self.i >= self.args.len() { + return PeekR::None; + } + let v = &self.args[self.i]; + if v == "-h" || v == "--help" { + return PeekR::Help; + } + return PeekR::Ok(v); + } + + /// The argument the current argument pointer is pointing to. + pub fn position(&self) -> usize { + return self.i; + } + + /// Reset the agument pointer to an earlier argument (i.e. after consuming N + /// arguments but finding the required final argument missing). + pub fn rewind(&mut self, i: usize) { + self.i = i; + } + + /// Move the argument pointer to the next argument (ex: after inspecting it using + /// `peek`). + pub fn consume(&mut self) { + self.i += 1; + } + + /// Produce a "parse successful" return value. + pub fn r_ok(&self, v: T) -> R { + return R::Ok(v); + } + + /// Produce a "parse failed" return value (includes which argument was being + /// inspected when the failure occured). + pub fn r_err(&mut self, text: String) -> R { + self.errors.push(VarkFailure { + arg_offset: self.i, + error: text, + }); + return R::Err; + } +} + +pub enum ErrorDetail { + /// The parser needed more command line arguments. + TooLittle, + /// Fully parsed command but additional unconsumed arguments follow (offset of + /// first unrecognized argument) + TooMuch(usize), + /// Aargvark considers multiple possible parses. This is a list of considered + /// parses, in order of when they were ruled out. + Incorrect(Vec), +} + +/// Returned by `vark_explicit`. `command` is whatever is passed as `command` to +/// `vark_explicit`, the first of argv if using `vark`. `args` is the remaining +/// arguments. +pub struct Error { + pub command: Option, + pub args: Vec, + pub detail: ErrorDetail, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.detail { + ErrorDetail::TooLittle => { + return "Missing arguments, use --help for more info".fmt(f); + }, + ErrorDetail::TooMuch(first) => { + return format_args!( + "Error parsing command line arguments: final arguments are unrecognized\n{:?}", + &self.args[*first..] + ).fmt(f); + }, + ErrorDetail::Incorrect(failures) => { + let mut display_args = vec![]; + let mut offset_offset = 0; + if let Some(c) = &self.command { + display_args.push(c.clone()); + offset_offset = 1; + } + display_args.extend(self.args.iter().map(|a| format!("{:?}", a))); + let mut display_arg_offsets = vec![]; + { + let mut offset = 0; + for d in &display_args { + display_arg_offsets.push(offset); + offset += d.width() + 1; + } + display_arg_offsets.push(offset); + } + let mut display_args = display_args.join(" "); + display_args.push_str(" "); + let mut text = "Error parsing arguments.\n".to_string(); + for e in failures.iter().rev() { + text.push_str("\n"); + text.push_str(&format!(" * {}\n", e.error)); + text.push_str(" "); + text.push_str(&display_args); + text.push_str("\n"); + text.push_str(" "); + text.push_str( + &" ".repeat( + display_arg_offsets.get(e.arg_offset + offset_offset).cloned().unwrap_or(0usize), + ), + ); + text.push_str("^\n"); + } + return text.fmt(f); + }, + } + } +} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return std::fmt::Display::fmt(self, f); + } +} + +impl std::error::Error for Error { } diff --git a/crates/aargvark/src/help.rs b/crates/aargvark/src/help.rs new file mode 100644 index 0000000..15c0b5b --- /dev/null +++ b/crates/aargvark/src/help.rs @@ -0,0 +1,415 @@ +use { + comfy_table::Cell, + std::{ + any::TypeId, + cell::RefCell, + collections::{ + HashMap, + HashSet, + }, + rc::Rc, + }, +}; + +fn style_usage(s: impl AsRef) -> String { + return s.as_ref().to_string(); +} + +fn style_description(s: impl AsRef) -> String { + return s.as_ref().to_string(); +} + +fn style_id(s: impl AsRef) -> String { + return console::Style::new().blue().dim().apply_to(s.as_ref()).to_string(); +} + +fn style_type(s: impl AsRef) -> String { + return console::Style::new().magenta().apply_to(s.as_ref()).to_string(); +} + +fn style_logical(s: impl AsRef) -> String { + return console::Style::new().dim().apply_to(s.as_ref()).to_string(); +} + +fn style_literal(s: impl AsRef) -> String { + return console::Style::new().bold().apply_to(s.as_ref()).to_string(); +} + +#[doc(hidden)] +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +pub struct HelpProductionKey { + type_id: TypeId, + variant: usize, +} + +#[doc(hidden)] +pub struct HelpProduction { + id: String, + description: String, + content: HelpProductionType, +} + +#[doc(hidden)] +pub enum HelpPartialContent { + Pattern(HelpPattern), + Production(HelpProductionType), +} + +impl HelpPartialContent { + pub fn struct_(fields: Vec, optional_fields: Vec) -> Self { + return HelpPartialContent::Production( + HelpProductionType::Struct(Rc::new(RefCell::new(HelpProductionTypeStruct { + fields: fields, + flag_fields: optional_fields, + }))), + ); + } + + pub fn enum_(variants: Vec) -> Self { + return HelpPartialContent::Production(HelpProductionType::Enum(Rc::new(RefCell::new(variants)))); + } +} + +/// State for a partially-parsed field for rendering help. +pub struct HelpPartialProduction { + pub description: String, + pub content: HelpPartialContent, +} + +pub enum HelpProductionType { + Struct(Rc>), + Enum(Rc>>), +} + +pub struct HelpProductionTypeStruct { + pub fields: Vec, + pub flag_fields: Vec, +} + +pub struct HelpField { + pub id: String, + pub pattern: HelpPattern, + pub description: String, +} + +pub struct HelpFlagField { + pub option: bool, + pub flags: Vec, + pub pattern: HelpPattern, + pub description: String, +} + +pub struct HelpVariant { + pub literal: String, + pub pattern: HelpPattern, + pub description: String, +} + +/// Structured help information - this list of pattern elements that is styled and +/// joined with spaces on output. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct HelpPattern(pub Vec); + +impl HelpPattern { + pub fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { + let mut out = String::new(); + for (i, e) in self.0.iter().enumerate() { + if i > 0 { + out.push_str(" "); + } + out.push_str(&e.render(stack, state)); + } + return out; + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum HelpPatternElement { + // xyz - denotes literal text user must type + Literal(String), + // `` - denotes type of data for user + Type(String), + // XYZ - refers to another section of help output + Reference(HelpProductionKey), + // Like reference, but the other section might not exist. Used for `...` when + // using `help_break` to limit output. + PseudoReference(String), + // `[XYZ]` - indicates a pattern is optional + Option(HelpPattern), + // `XYZ[ ...]` - indicates a pattern can be repeated, space separated + Array(HelpPattern), + // `xyz | abc` - indicates one of multiple patterns can be selected + Variant(Vec), +} + +impl HelpPatternElement { + fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { + match self { + HelpPatternElement::Literal(l) => return style_literal(l), + HelpPatternElement::Type(i) => return style_type(format!("<{}>", i)), + HelpPatternElement::Reference(i) => { + let production = state.productions.get(i).unwrap(); + stack.push((*i, production.clone())); + return style_id(production.id.as_str()) + }, + HelpPatternElement::PseudoReference(key) => { + return style_id(key.as_str()) + }, + HelpPatternElement::Option(i) => return format!( + "{}{}{}", + style_logical("["), + i.render(stack, state), + style_logical("]") + ), + HelpPatternElement::Array(i) => return format!("{}{}", i.render(stack, state), style_logical("[ ...]")), + HelpPatternElement::Variant(i) => return i + .iter() + .map(|x| x.render(stack, state)) + .collect::>() + .join(&style_logical(" | ")), + } + } +} + +#[doc(hidden)] +#[derive(Default)] +pub struct HelpState { + // Write during building + name_counter: HashMap, + // Write during building, read during rendering + productions: HashMap>, +} + +impl HelpState { + fn add( + &mut self, + type_id: TypeId, + type_id_variant: usize, + id: impl ToString, + description: impl ToString, + content: HelpProductionType, + ) -> HelpProductionKey { + let mut id = id.to_string(); + let count = *self.name_counter.entry(id.clone()).and_modify(|x| *x += 1).or_insert(1); + if count > 1 { + id = format!("{} ({})", id, count); + } + let key = HelpProductionKey { + type_id: type_id, + variant: type_id_variant, + }; + self.productions.insert(key, Rc::new(HelpProduction { + id: id, + description: description.to_string(), + content: content, + })); + return key; + } + + pub fn add_struct( + &mut self, + type_id: TypeId, + type_id_variant: usize, + id: impl ToString, + description: impl ToString, + ) -> (HelpProductionKey, Rc>) { + let out = Rc::new(RefCell::new(HelpProductionTypeStruct { + fields: vec![], + flag_fields: vec![], + })); + let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Struct(out.clone())); + return (key, out); + } + + pub fn add_enum( + &mut self, + type_id: TypeId, + type_id_variant: usize, + id: impl ToString, + description: impl ToString, + ) -> (HelpProductionKey, Rc>>) { + let out = Rc::new(RefCell::new(vec![])); + let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Enum(out.clone())); + return (key, out); + } +} + +/// State required for building a help string. +pub struct VarkRetHelp { + pub(crate) command: Option, + pub(crate) args: Vec, + pub(crate) consumed_args: usize, + pub(crate) builder: Box HelpPartialProduction>, +} + +impl VarkRetHelp { + /// Build a help string using current operating environment line widths. + pub fn render(self) -> String { + fn format_desc(out: &mut String, desc: &str) { + if !desc.is_empty() { + out.push_str( + &style_description( + textwrap::wrap( + desc, + &textwrap::Options::with_termwidth().initial_indent(" ").subsequent_indent(" "), + ).join("\n"), + ), + ); + out.push_str("\n\n"); + } + } + + fn format_pattern(out: &mut String, content: &HelpProductionType) { + match content { + HelpProductionType::Struct(struct_) => { + let struct_ = struct_.borrow(); + for f in &struct_.fields { + out.push_str(" "); + out.push_str(&style_id(&f.id)); + } + if !struct_.flag_fields.is_empty() { + let all_opt = struct_.flag_fields.iter().all(|x| x.option); + out.push_str(" "); + if all_opt { + out.push_str(&style_logical("[ ...FLAGS]")); + } else { + out.push_str(&style_logical("...FLAGS")); + } + } + }, + HelpProductionType::Enum(fields) => { + for (i, f) in fields.borrow().iter().enumerate() { + if i > 0 { + out.push_str(" |"); + } + out.push_str(" "); + out.push_str(&style_literal(&f.literal)); + } + }, + } + } + + fn format_content( + out: &mut String, + stack: &mut Vec<(HelpProductionKey, Rc)>, + help_state: &HelpState, + content: &HelpProductionType, + ) { + let mut table = comfy_table::Table::new(); + table.load_preset(comfy_table::presets::NOTHING); + table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); + match content { + HelpProductionType::Struct(struct_) => { + let struct_ = struct_.borrow(); + for f in &struct_.fields { + table.add_row( + vec![ + comfy_table::Cell::new( + format!(" {}: {}", style_id(&f.id), f.pattern.render(stack, help_state)), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + for f in &struct_.flag_fields { + let mut left_col = vec![]; + for flag in &f.flags { + let mut elems = vec![HelpPatternElement::Literal(flag.clone())]; + elems.extend(f.pattern.0.clone()); + left_col.push(format!(" {}", if f.option { + HelpPattern(vec![HelpPatternElement::Option(HelpPattern(elems))]) + } else { + HelpPattern(elems) + }.render(stack, help_state))); + } + table.add_row( + vec![ + comfy_table::Cell::new(left_col.join("\n")), + Cell::new(style_description(&f.description)) + ], + ); + } + }, + HelpProductionType::Enum(fields) => { + for f in &*fields.borrow() { + table.add_row( + vec![ + comfy_table::Cell::new( + format!( + " {} {}", + style_literal(&f.literal), + f.pattern.render(stack, help_state) + ), + ), + Cell::new(style_description(&f.description)) + ], + ); + } + }, + } + table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { + lower: comfy_table::Width::Percentage(20), + upper: comfy_table::Width::Percentage(60), + }]); + out.push_str(&table.to_string()); + out.push_str("\n\n"); + } + + let mut help_state = HelpState::default(); + let mut stack = Vec::<(HelpProductionKey, Rc)>::new(); + let mut seen_productions = HashSet::::new(); + let partial = (self.builder)(&mut help_state); + + // Write initial partial production + let mut out = style_usage("Usage:"); + if let Some(s) = &self.command { + out.push_str(" "); + out.push_str(&style_usage(s)); + } + for s in self.args.iter().take(self.consumed_args) { + out.push_str(" "); + out.push_str(&style_usage(s)); + } + let mut temp_stack = vec![]; + match &partial.content { + HelpPartialContent::Pattern(p) => { + if !p.0.is_empty() { + out.push_str(" "); + out.push_str(&p.render(&mut temp_stack, &help_state)); + } + }, + HelpPartialContent::Production(content) => { + format_pattern(&mut out, &content); + }, + } + out.push_str("\n\n"); + format_desc(&mut out, &partial.description); + match &partial.content { + HelpPartialContent::Pattern(_) => { + out.push_str("\n\n"); + }, + HelpPartialContent::Production(content) => { + format_content(&mut out, &mut temp_stack, &mut help_state, content); + }, + } + temp_stack.reverse(); + stack.extend(temp_stack); + + // Recurse productions + while let Some((key, top)) = stack.pop() { + if !seen_productions.insert(key) { + continue; + } + out.push_str(&style_id(&top.id)); + out.push_str(":"); + format_pattern(&mut out, &top.content); + out.push_str("\n\n"); + format_desc(&mut out, &top.description); + let mut temp_stack = vec![]; + format_content(&mut out, &mut temp_stack, &mut help_state, &top.content); + temp_stack.reverse(); + stack.extend(temp_stack); + } + return out; + } +} diff --git a/crates/aargvark/src/lib.rs b/crates/aargvark/src/lib.rs index aab14e5..490df1c 100644 --- a/crates/aargvark/src/lib.rs +++ b/crates/aargvark/src/lib.rs @@ -1,387 +1,25 @@ #![doc = include_str!("../readme.md")] -use std::{ - any::TypeId, - cell::RefCell, - collections::{ - HashMap, - HashSet, +use { + base::{ + Error, + ErrorDetail, + VarkState, + R, }, - env::args, - ffi::{ - OsString, - }, - fs, - hash::Hash, - io::{ - stdin, - Read, - }, - net::{ - SocketAddr, - SocketAddrV4, - SocketAddrV6, - IpAddr, - Ipv4Addr, - Ipv6Addr, - }, - path::PathBuf, - process::exit, - rc::Rc, + help::VarkRetHelp, + traits_impls::AargvarkTrait, }; pub use aargvark_proc_macros::Aargvark; -use comfy_table::Cell; -use unicode_width::UnicodeWidthStr; - -pub struct VarkFailure { - pub arg_offset: usize, - pub error: String, -} - -/// Return type enum (like `Result`) during parsing. -pub enum R { - /// Ran out of arguments before parsing successfully completed. - EOF, - /// Parsing failed due to incorrect arguments. - Err, - /// Encountered `-h` or `--help` and aborted. - Help(Box HelpPartialProduction>), - /// Successfully parsed value. - Ok(T), -} - -/// Possible results of peeking the output. -pub enum PeekR<'a> { - /// There's another argument - Ok(&'a str), - /// No more arguments remain - None, - /// The next argument is `-h` or `--help` - caller should return `R::Help`. - Help, -} - -#[doc(hidden)] -pub struct VarkState { - command: Option, - args: Vec, - i: usize, - errors: Vec, -} - -impl VarkState { - pub fn new(command: Option, args: Vec) -> Self { - return Self { - command: command, - args: args, - i: 0, - errors: vec![], - }; - } -} - -impl VarkState { - /// Return the next argument without consuming it. - pub fn peek<'a>(&'a self) -> PeekR<'a> { - if self.i >= self.args.len() { - return PeekR::None; - } - let v = &self.args[self.i]; - if v == "-h" || v == "--help" { - return PeekR::Help; - } - return PeekR::Ok(v); - } - - /// The argument the current argument pointer is pointing to. - pub fn position(&self) -> usize { - return self.i; - } - - /// Reset the agument pointer to an earlier argument (i.e. after consuming N - /// arguments but finding the required final argument missing). - pub fn rewind(&mut self, i: usize) { - self.i = i; - } - - /// Move the argument pointer to the next argument (ex: after inspecting it using - /// `peek`). - pub fn consume(&mut self) { - self.i += 1; - } - - /// Produce a "parse successful" return value. - pub fn r_ok(&self, v: T) -> R { - return R::Ok(v); - } - - /// Produce a "parse failed" return value (includes which argument was being - /// inspected when the failure occured). - pub fn r_err(&mut self, text: String) -> R { - self.errors.push(VarkFailure { - arg_offset: self.i, - error: text, - }); - return R::Err; - } -} - -pub enum ErrorDetail { - /// The parser needed more command line arguments. - TooLittle, - /// Fully parsed command but additional unconsumed arguments follow (offset of - /// first unrecognized argument) - TooMuch(usize), - /// Aargvark considers multiple possible parses. This is a list of considered - /// parses, in order of when they were ruled out. - Incorrect(Vec), -} - -/// Returned by `vark_explicit`. `command` is whatever is passed as `command` to -/// `vark_explicit`, the first of argv if using `vark`. `args` is the remaining -/// arguments. -pub struct Error { - pub command: Option, - pub args: Vec, - pub detail: ErrorDetail, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.detail { - ErrorDetail::TooLittle => { - return "Missing arguments, use --help for more info".fmt(f); - }, - ErrorDetail::TooMuch(first) => { - return format_args!( - "Error parsing command line arguments: final arguments are unrecognized\n{:?}", - &self.args[*first..] - ).fmt(f); - }, - ErrorDetail::Incorrect(failures) => { - let mut display_args = vec![]; - let mut offset_offset = 0; - if let Some(c) = &self.command { - display_args.push(c.clone()); - offset_offset = 1; - } - display_args.extend(self.args.iter().map(|a| format!("{:?}", a))); - let mut display_arg_offsets = vec![]; - { - let mut offset = 0; - for d in &display_args { - display_arg_offsets.push(offset); - offset += d.width() + 1; - } - display_arg_offsets.push(offset); - } - let mut display_args = display_args.join(" "); - display_args.push_str(" "); - let mut text = "Error parsing arguments.\n".to_string(); - for e in failures.iter().rev() { - text.push_str("\n"); - text.push_str(&format!(" * {}\n", e.error)); - text.push_str(" "); - text.push_str(&display_args); - text.push_str("\n"); - text.push_str(" "); - text.push_str( - &" ".repeat( - display_arg_offsets.get(e.arg_offset + offset_offset).cloned().unwrap_or(0usize), - ), - ); - text.push_str("^\n"); - } - return text.fmt(f); - }, - } - } -} -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - return std::fmt::Display::fmt(self, f); - } -} +/// Types related to producing help text. +pub mod help; -impl std::error::Error for Error { } +/// Base types - return types, errors, etc. +pub mod base; -/// State required for building a help string. -pub struct VarkRetHelp { - state: VarkState, - builder: Box HelpPartialProduction>, -} - -impl VarkRetHelp { - /// Build a help string using current operating environment line widths. - pub fn render(self) -> String { - fn format_desc(out: &mut String, desc: &str) { - if !desc.is_empty() { - out.push_str( - &style_description( - textwrap::wrap( - desc, - &textwrap::Options::with_termwidth().initial_indent(" ").subsequent_indent(" "), - ).join("\n"), - ), - ); - out.push_str("\n\n"); - } - } - - fn format_pattern(out: &mut String, content: &HelpProductionType) { - match content { - HelpProductionType::Struct(struct_) => { - let struct_ = struct_.borrow(); - for f in &struct_.fields { - out.push_str(" "); - out.push_str(&style_id(&f.id)); - } - if !struct_.flag_fields.is_empty() { - let all_opt = struct_.flag_fields.iter().all(|x| x.option); - out.push_str(" "); - if all_opt { - out.push_str(&style_logical("[ ...FLAGS]")); - } else { - out.push_str(&style_logical("...FLAGS")); - } - } - }, - HelpProductionType::Enum(fields) => { - for (i, f) in fields.borrow().iter().enumerate() { - if i > 0 { - out.push_str(" |"); - } - out.push_str(" "); - out.push_str(&style_literal(&f.literal)); - } - }, - } - } - - fn format_content( - out: &mut String, - stack: &mut Vec<(HelpProductionKey, Rc)>, - help_state: &HelpState, - content: &HelpProductionType, - ) { - let mut table = comfy_table::Table::new(); - table.load_preset(comfy_table::presets::NOTHING); - table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); - match content { - HelpProductionType::Struct(struct_) => { - let struct_ = struct_.borrow(); - for f in &struct_.fields { - table.add_row( - vec![ - comfy_table::Cell::new( - format!(" {}: {}", style_id(&f.id), f.pattern.render(stack, help_state)), - ), - Cell::new(style_description(&f.description)) - ], - ); - } - for f in &struct_.flag_fields { - let mut left_col = vec![]; - for flag in &f.flags { - let mut elems = vec![HelpPatternElement::Literal(flag.clone())]; - elems.extend(f.pattern.0.clone()); - left_col.push(format!(" {}", if f.option { - HelpPattern(vec![HelpPatternElement::Option(HelpPattern(elems))]) - } else { - HelpPattern(elems) - }.render(stack, help_state))); - } - table.add_row( - vec![ - comfy_table::Cell::new(left_col.join("\n")), - Cell::new(style_description(&f.description)) - ], - ); - } - }, - HelpProductionType::Enum(fields) => { - for f in &*fields.borrow() { - table.add_row( - vec![ - comfy_table::Cell::new( - format!( - " {} {}", - style_literal(&f.literal), - f.pattern.render(stack, help_state) - ), - ), - Cell::new(style_description(&f.description)) - ], - ); - } - }, - } - table.set_constraints(vec![comfy_table::ColumnConstraint::Boundaries { - lower: comfy_table::Width::Percentage(20), - upper: comfy_table::Width::Percentage(60), - }]); - out.push_str(&table.to_string()); - out.push_str("\n\n"); - } - - let mut help_state = HelpState::default(); - let mut stack = Vec::<(HelpProductionKey, Rc)>::new(); - let mut seen_productions = HashSet::::new(); - let partial = (self.builder)(&mut help_state); - - // Write initial partial production - let mut out = style_usage("Usage:"); - if let Some(s) = &self.state.command { - out.push_str(" "); - out.push_str(&style_usage(s)); - } - for s in self.state.args.iter().take(self.state.i) { - out.push_str(" "); - out.push_str(&style_usage(s)); - } - let mut temp_stack = vec![]; - match &partial.content { - HelpPartialContent::Pattern(p) => { - if !p.0.is_empty() { - out.push_str(" "); - out.push_str(&p.render(&mut temp_stack, &help_state)); - } - }, - HelpPartialContent::Production(content) => { - format_pattern(&mut out, &content); - }, - } - out.push_str("\n\n"); - format_desc(&mut out, &partial.description); - match &partial.content { - HelpPartialContent::Pattern(_) => { - out.push_str("\n\n"); - }, - HelpPartialContent::Production(content) => { - format_content(&mut out, &mut temp_stack, &mut help_state, content); - }, - } - temp_stack.reverse(); - stack.extend(temp_stack); - - // Recurse productions - while let Some((key, top)) = stack.pop() { - if !seen_productions.insert(key) { - continue; - } - out.push_str(&style_id(&top.id)); - out.push_str(":"); - format_pattern(&mut out, &top.content); - out.push_str("\n\n"); - format_desc(&mut out, &top.description); - let mut temp_stack = vec![]; - format_content(&mut out, &mut temp_stack, &mut help_state, &top.content); - temp_stack.reverse(); - stack.extend(temp_stack); - } - return out; - } -} +/// Traits and helper traits for parsing arguments and default implementations. +pub mod traits_impls; /// Result of varking when no errors occurred. Either results in parsed value or /// the parsing was interrupted because help was requested. @@ -411,7 +49,9 @@ pub fn vark_explicit(command: Option, args: Vec { return Ok(VarkRet::Help(VarkRetHelp { - state: state, + command: state.command, + args: state.args, + consumed_args: state.i, builder: builder, })); }, @@ -432,621 +72,19 @@ pub fn vark_explicit(command: Option, args: Vec() -> T { - let mut args = args(); + let mut args = std::env::args(); let command = args.next(); match vark_explicit(command, args.collect::>()) { Ok(v) => match v { VarkRet::Ok(v) => return v, VarkRet::Help(h) => { println!("{}", h.render()); - exit(0); + std::process::exit(0); }, }, Err(e) => { eprintln!("{:?}", e); - exit(1); + std::process::exit(1); }, } } - -/// Anything that implements this trait can be parsed and used as a field in other -/// parsable enums/structs. -pub trait AargvarkTrait: Sized { - /// Called when this argument is reached. Should parse data until no more data can - /// be parsed. - fn vark(state: &mut VarkState) -> R; - - /// Called when `-h` is specified. - fn build_help_pattern(state: &mut HelpState) -> HelpPattern; -} - -/// A helper enum, providing a simpler interface for types that can be parsed from -/// a single primitive string. -pub trait AargvarkFromStr: Sized { - fn from_str(s: &str) -> Result; - fn build_help_pattern(state: &mut HelpState) -> HelpPattern; -} - -impl AargvarkTrait for T { - fn vark(state: &mut VarkState) -> R { - let s = match state.peek() { - PeekR::None => return R::EOF, - PeekR::Help => return R::Help(Box::new(|state| { - return HelpPartialProduction { - description: "".to_string(), - content: HelpPartialContent::Pattern(::build_help_pattern(state)), - }; - })), - PeekR::Ok(s) => s, - }; - match T::from_str(s) { - Ok(v) => { - state.consume(); - return state.r_ok(v); - }, - Err(e) => return state.r_err(e), - } - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return ::build_help_pattern(state); - } -} - -macro_rules! auto_from_str{ - ($placeholder: literal, $t: ty) => { - impl AargvarkFromStr for $t { - fn from_str(s: &str) -> Result { - return ::from_str(s).map_err(|e| e.to_string()); - } - - fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { - return HelpPattern(vec![HelpPatternElement:: Type($placeholder.to_string())]); - } - } - }; -} - -auto_from_str!("STRING", String); - -auto_from_str!("INT", u8); - -auto_from_str!("INT", u16); - -auto_from_str!("INT", u32); - -auto_from_str!("INT", u64); - -auto_from_str!("INT", usize); - -auto_from_str!("INT", i8); - -auto_from_str!("INT", i16); - -auto_from_str!("INT", i32); - -auto_from_str!("INT", i64); - -auto_from_str!("INT", f32); - -auto_from_str!("NUM", f64); - -auto_from_str!("STRING", OsString); - -auto_from_str!("SOCKET", SocketAddr); - -auto_from_str!("SOCKETV4", SocketAddrV4); - -auto_from_str!("SOCKETV6", SocketAddrV6); - -auto_from_str!("IP", IpAddr); - -auto_from_str!("IPV4", Ipv4Addr); - -auto_from_str!("IPV6", Ipv6Addr); - -auto_from_str!("PATH", PathBuf); - -#[cfg(feature = "http_types")] -auto_from_str!("URI", http::Uri); - -impl AargvarkFromStr for bool { - fn from_str(s: &str) -> Result { - return ::from_str(s).map_err(|e| e.to_string()); - } - - fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { - return HelpPattern( - vec![ - HelpPatternElement::Variant( - vec![ - HelpPattern(vec![HelpPatternElement::Literal("true".to_string())]), - HelpPattern(vec![HelpPatternElement::Literal("false".to_string())]) - ], - ) - ], - ); - } -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub enum Source { - Stdin, - File(PathBuf), -} - -/// This parses a path (or - for stdin) passed on the command line into bytes. -pub struct AargvarkFile { - pub value: Vec, - pub source: Source, -} - -impl AargvarkFromStr for AargvarkFile { - fn from_str(s: &str) -> Result { - if s == "-" { - let mut out = vec![]; - match stdin().read_to_end(&mut out) { - Ok(_) => return Ok(Self { - value: out, - source: Source::Stdin, - }), - Err(e) => return Err(format!("Error reading stdin: {}", e)), - }; - } else { - match fs::read(s) { - Ok(v) => return Ok(Self { - value: v, - source: Source::File(PathBuf::from(s)), - }), - Err(e) => return Err(format!("Error reading {}: {}", s, e)), - }; - } - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return HelpPattern( - vec![ - HelpPatternElement::Variant( - vec![ - ::build_help_pattern(state), - HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) - ], - ) - ], - ); - } -} - -/// This parses a path (or - for stdin) passed on the command line as json into the -/// specified type. -#[cfg(feature = "serde_json")] -pub struct AargvarkJson { - pub value: T, - pub source: Source, -} - -#[cfg(feature = "serde_json")] -impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { - fn from_str(s: &str) -> Result { - let b = AargvarkFile::from_str(s)?; - match serde_json::from_slice(&b.value) { - Ok(v) => return Ok(Self { - value: v, - source: b.source, - }), - Err(e) => return Err(e.to_string()), - }; - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return HelpPattern( - vec![ - HelpPatternElement::Variant( - vec![ - ::build_help_pattern(state), - HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) - ], - ) - ], - ); - } -} - -#[cfg(feature = "serde_json")] -impl Clone for AargvarkJson { - fn clone(&self) -> Self { - return AargvarkJson { - value: self.value.clone(), - source: self.source.clone(), - }; - } -} - -/// This parses a path (or - for stdin) passed on the command line as yaml into the -/// specified type. -#[cfg(feature = "serde_yaml")] -pub struct AargvarkYaml { - pub value: T, - pub source: Source, -} - -#[cfg(feature = "serde_yaml")] -impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { - fn from_str(s: &str) -> Result { - let b = AargvarkFile::from_str(s)?; - match serde_yaml::from_slice(&b.value) { - Ok(v) => return Ok(Self { - value: v, - source: b.source, - }), - Err(e) => return Err(e.to_string()), - }; - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return HelpPattern( - vec![ - HelpPatternElement::Variant( - vec![ - ::build_help_pattern(state), - HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) - ], - ) - ], - ); - } -} - -#[cfg(feature = "serde_yaml")] -impl Clone for AargvarkYaml { - fn clone(&self) -> Self { - return AargvarkYaml { - value: self.value.clone(), - source: self.source.clone(), - }; - } -} - -#[doc(hidden)] -pub fn vark_from_iter>(state: &mut VarkState) -> R { - let mut out = vec![]; - let mut rewind_to = state.position(); - loop { - let r = T::vark(state); - match r { - R::Ok(v) => { - out.push(v); - rewind_to = state.position(); - }, - R::Help(b) => return R::Help(b), - R::Err | R::EOF => { - state.rewind(rewind_to); - return state.r_ok(C::from_iter(out.into_iter())); - }, - }; - } -} - -impl AargvarkTrait for Vec { - fn vark(state: &mut VarkState) -> R { - return vark_from_iter(state); - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return HelpPattern(vec![HelpPatternElement::Array(T::build_help_pattern(state))]); - } -} - -impl AargvarkTrait for HashSet { - fn vark(state: &mut VarkState) -> R { - return vark_from_iter(state); - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return HelpPattern(vec![HelpPatternElement::Array(T::build_help_pattern(state))]); - } -} - -/// A key-value argument type (single argument) that takes the format `K=V` where -/// both `K` and `V` need to be string-parsable types. The argument is split at the -/// first unescaped `=` - additional `=` in the key can be escaped with `\`. -/// -/// This is used for the `HashMap` implementation which takes a series of arguments -/// like `a=a b=b c=123`. -pub struct AargvarkKV { - pub key: K, - pub value: V, -} - -impl AargvarkTrait for AargvarkKV { - fn vark(state: &mut VarkState) -> R { - let res = String::vark(state); - let res = match res { - R::EOF => return R::EOF, - R::Err => return R::Err, - R::Help(b) => return R::Help(b), - R::Ok(r) => r, - }; - let mut res = res.into_bytes().into_iter(); - let mut k = vec![]; - let mut escape = false; - for c in &mut res { - if escape { - k.push(c); - escape = false; - } else { - if c == b'\\' { - escape = true; - } else if c == b'=' { - break; - } else { - k.push(c); - } - } - } - let key = match K::from_str(&unsafe { - String::from_utf8_unchecked(k) - }) { - Ok(r) => r, - Err(e) => return state.r_err(format!("Error parsing map key: {}", e)), - }; - let value = match V::from_str(&unsafe { - String::from_utf8_unchecked(res.collect()) - }) { - Ok(r) => r, - Err(e) => return state.r_err(format!("Error parsing map value: {}", e)), - }; - return state.r_ok(AargvarkKV { - key: key, - value: value, - }); - } - - fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { - return HelpPattern(vec![HelpPatternElement::Literal("K=V".to_string())]); - } -} - -impl AargvarkTrait for HashMap { - fn vark(state: &mut VarkState) -> R { - let res = match >>::vark(state) { - R::EOF => return R::EOF, - R::Err => return R::Err, - R::Help(b) => return R::Help(b), - R::Ok(r) => r, - }; - return state.r_ok(res.into_iter().map(|kv| (kv.key, kv.value)).collect()); - } - - fn build_help_pattern(state: &mut HelpState) -> HelpPattern { - return >>::build_help_pattern(state); - } -} - -fn style_usage(s: impl AsRef) -> String { - return s.as_ref().to_string(); -} - -fn style_description(s: impl AsRef) -> String { - return s.as_ref().to_string(); -} - -fn style_id(s: impl AsRef) -> String { - return console::Style::new().blue().dim().apply_to(s.as_ref()).to_string(); -} - -fn style_type(s: impl AsRef) -> String { - return console::Style::new().magenta().apply_to(s.as_ref()).to_string(); -} - -fn style_logical(s: impl AsRef) -> String { - return console::Style::new().dim().apply_to(s.as_ref()).to_string(); -} - -fn style_literal(s: impl AsRef) -> String { - return console::Style::new().bold().apply_to(s.as_ref()).to_string(); -} - -#[doc(hidden)] -#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] -pub struct HelpProductionKey { - type_id: TypeId, - variant: usize, -} - -#[doc(hidden)] -pub struct HelpProduction { - id: String, - description: String, - content: HelpProductionType, -} - -#[doc(hidden)] -pub enum HelpPartialContent { - Pattern(HelpPattern), - Production(HelpProductionType), -} - -impl HelpPartialContent { - pub fn struct_(fields: Vec, optional_fields: Vec) -> Self { - return HelpPartialContent::Production( - HelpProductionType::Struct(Rc::new(RefCell::new(HelpProductionTypeStruct { - fields: fields, - flag_fields: optional_fields, - }))), - ); - } - - pub fn enum_(variants: Vec) -> Self { - return HelpPartialContent::Production(HelpProductionType::Enum(Rc::new(RefCell::new(variants)))); - } -} - -/// State for a partially-parsed field for rendering help. -pub struct HelpPartialProduction { - pub description: String, - pub content: HelpPartialContent, -} - -#[doc(hidden)] -pub enum HelpProductionType { - Struct(Rc>), - Enum(Rc>>), -} - -#[doc(hidden)] -pub struct HelpProductionTypeStruct { - pub fields: Vec, - pub flag_fields: Vec, -} - -#[doc(hidden)] -pub struct HelpField { - pub id: String, - pub pattern: HelpPattern, - pub description: String, -} - -#[doc(hidden)] -pub struct HelpFlagField { - pub option: bool, - pub flags: Vec, - pub pattern: HelpPattern, - pub description: String, -} - -#[doc(hidden)] -pub struct HelpVariant { - pub literal: String, - pub pattern: HelpPattern, - pub description: String, -} - -/// Structured help information - this list of pattern elements that is styled and -/// joined with spaces on output. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct HelpPattern(pub Vec); - -impl HelpPattern { - pub fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { - let mut out = String::new(); - for (i, e) in self.0.iter().enumerate() { - if i > 0 { - out.push_str(" "); - } - out.push_str(&e.render(stack, state)); - } - return out; - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum HelpPatternElement { - // xyz - denotes literal text user must type - Literal(String), - // `` - denotes type of data for user - Type(String), - // XYZ - refers to another section of help output - Reference(HelpProductionKey), - // Like reference, but the other section might not exist. Used for `...` when - // using `help_break` to limit output. - PseudoReference(String), - // `[XYZ]` - indicates a pattern is optional - Option(HelpPattern), - // `XYZ[ ...]` - indicates a pattern can be repeated, space separated - Array(HelpPattern), - // `xyz | abc` - indicates one of multiple patterns can be selected - Variant(Vec), -} - -impl HelpPatternElement { - fn render(&self, stack: &mut Vec<(HelpProductionKey, Rc)>, state: &HelpState) -> String { - match self { - HelpPatternElement::Literal(l) => return style_literal(l), - HelpPatternElement::Type(i) => return style_type(format!("<{}>", i)), - HelpPatternElement::Reference(i) => { - let production = state.productions.get(i).unwrap(); - stack.push((*i, production.clone())); - return style_id(production.id.as_str()) - }, - HelpPatternElement::PseudoReference(key) => { - return style_id(key.as_str()) - }, - HelpPatternElement::Option(i) => return format!( - "{}{}{}", - style_logical("["), - i.render(stack, state), - style_logical("]") - ), - HelpPatternElement::Array(i) => return format!("{}{}", i.render(stack, state), style_logical("[ ...]")), - HelpPatternElement::Variant(i) => return i - .iter() - .map(|x| x.render(stack, state)) - .collect::>() - .join(&style_logical(" | ")), - } - } -} - -#[doc(hidden)] -#[derive(Default)] -pub struct HelpState { - // Write during building - name_counter: HashMap, - // Write during building, read during rendering - productions: HashMap>, -} - -impl HelpState { - fn add( - &mut self, - type_id: TypeId, - type_id_variant: usize, - id: impl ToString, - description: impl ToString, - content: HelpProductionType, - ) -> HelpProductionKey { - let mut id = id.to_string(); - let count = *self.name_counter.entry(id.clone()).and_modify(|x| *x += 1).or_insert(1); - if count > 1 { - id = format!("{} ({})", id, count); - } - let key = HelpProductionKey { - type_id: type_id, - variant: type_id_variant, - }; - self.productions.insert(key, Rc::new(HelpProduction { - id: id, - description: description.to_string(), - content: content, - })); - return key; - } - - pub fn add_struct( - &mut self, - type_id: TypeId, - type_id_variant: usize, - id: impl ToString, - description: impl ToString, - ) -> (HelpProductionKey, Rc>) { - let out = Rc::new(RefCell::new(HelpProductionTypeStruct { - fields: vec![], - flag_fields: vec![], - })); - let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Struct(out.clone())); - return (key, out); - } - - pub fn add_enum( - &mut self, - type_id: TypeId, - type_id_variant: usize, - id: impl ToString, - description: impl ToString, - ) -> (HelpProductionKey, Rc>>) { - let out = Rc::new(RefCell::new(vec![])); - let key = self.add(type_id, type_id_variant, id, description, HelpProductionType::Enum(out.clone())); - return (key, out); - } -} diff --git a/crates/aargvark/src/traits_impls.rs b/crates/aargvark/src/traits_impls.rs new file mode 100644 index 0000000..20fa8d2 --- /dev/null +++ b/crates/aargvark/src/traits_impls.rs @@ -0,0 +1,444 @@ +use { + crate::{ + base::{ + PeekR, + VarkState, + R, + }, + help::{ + HelpPartialContent, + HelpPartialProduction, + HelpPattern, + HelpPatternElement, + HelpState, + }, + }, + std::{ + collections::{ + HashMap, + HashSet, + }, + ffi::OsString, + io::Read, + net::{ + IpAddr, + Ipv4Addr, + Ipv6Addr, + SocketAddr, + SocketAddrV4, + SocketAddrV6, + }, + path::PathBuf, + }, +}; + +/// Anything that implements this trait can be parsed and used as a field in other +/// parsable enums/structs. +pub trait AargvarkTrait: Sized { + /// Called when this argument is reached. Should parse data until no more data can + /// be parsed. + fn vark(state: &mut VarkState) -> R; + + /// Called when `-h` is specified. + fn build_help_pattern(state: &mut HelpState) -> HelpPattern; +} + +/// A helper enum, providing a simpler interface for types that can be parsed from +/// a single primitive string. +pub trait AargvarkFromStr: Sized { + fn from_str(s: &str) -> Result; + fn build_help_pattern(state: &mut HelpState) -> HelpPattern; +} + +impl AargvarkTrait for T { + fn vark(state: &mut VarkState) -> R { + let s = match state.peek() { + PeekR::None => return R::EOF, + PeekR::Help => return R::Help(Box::new(|state| { + return HelpPartialProduction { + description: "".to_string(), + content: HelpPartialContent::Pattern(::build_help_pattern(state)), + }; + })), + PeekR::Ok(s) => s, + }; + match T::from_str(s) { + Ok(v) => { + state.consume(); + return state.r_ok(v); + }, + Err(e) => return state.r_err(e), + } + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return ::build_help_pattern(state); + } +} + +macro_rules! auto_from_str{ + ($placeholder: literal, $t: ty) => { + impl AargvarkFromStr for $t { + fn from_str(s: &str) -> Result { + return ::from_str(s).map_err(|e| e.to_string()); + } + + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement:: Type($placeholder.to_string())]); + } + } + }; +} + +auto_from_str!("STRING", String); + +auto_from_str!("INT", u8); + +auto_from_str!("INT", u16); + +auto_from_str!("INT", u32); + +auto_from_str!("INT", u64); + +auto_from_str!("INT", usize); + +auto_from_str!("INT", i8); + +auto_from_str!("INT", i16); + +auto_from_str!("INT", i32); + +auto_from_str!("INT", i64); + +auto_from_str!("INT", f32); + +auto_from_str!("NUM", f64); + +auto_from_str!("STRING", OsString); + +auto_from_str!("SOCKET", SocketAddr); + +auto_from_str!("SOCKETV4", SocketAddrV4); + +auto_from_str!("SOCKETV6", SocketAddrV6); + +auto_from_str!("IP", IpAddr); + +auto_from_str!("IPV4", Ipv4Addr); + +auto_from_str!("IPV6", Ipv6Addr); + +auto_from_str!("PATH", PathBuf); + +#[cfg(feature = "http_types")] +auto_from_str!("URI", http::Uri); + +impl AargvarkFromStr for bool { + fn from_str(s: &str) -> Result { + return ::from_str(s).map_err(|e| e.to_string()); + } + + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + HelpPattern(vec![HelpPatternElement::Literal("true".to_string())]), + HelpPattern(vec![HelpPatternElement::Literal("false".to_string())]) + ], + ) + ], + ); + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Source { + Stdin, + File(PathBuf), +} + +/// This parses a path (or - for stdin) passed on the command line into bytes. +pub struct AargvarkFile { + pub value: Vec, + pub source: Source, +} + +impl AargvarkFromStr for AargvarkFile { + fn from_str(s: &str) -> Result { + if s == "-" { + let mut out = vec![]; + match std::io::stdin().read_to_end(&mut out) { + Ok(_) => return Ok(Self { + value: out, + source: Source::Stdin, + }), + Err(e) => return Err(format!("Error reading stdin: {}", e)), + }; + } else { + match std::fs::read(s) { + Ok(v) => return Ok(Self { + value: v, + source: Source::File(PathBuf::from(s)), + }), + Err(e) => return Err(format!("Error reading {}: {}", s, e)), + }; + } + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + ::build_help_pattern(state), + HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) + ], + ) + ], + ); + } +} + +/// This parses a path (or - for stdin) passed on the command line as json into the +/// specified type. +#[cfg(feature = "serde_json")] +pub struct AargvarkJson { + pub value: T, + pub source: Source, +} + +#[cfg(feature = "serde_json")] +impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkJson { + fn from_str(s: &str) -> Result { + let b = AargvarkFile::from_str(s)?; + match serde_json::from_slice(&b.value) { + Ok(v) => return Ok(Self { + value: v, + source: b.source, + }), + Err(e) => return Err(e.to_string()), + }; + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + ::build_help_pattern(state), + HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) + ], + ) + ], + ); + } +} + +#[cfg(feature = "serde_json")] +impl Clone for AargvarkJson { + fn clone(&self) -> Self { + return AargvarkJson { + value: self.value.clone(), + source: self.source.clone(), + }; + } +} + +/// This parses a path (or - for stdin) passed on the command line as yaml into the +/// specified type. +#[cfg(feature = "serde_yaml")] +pub struct AargvarkYaml { + pub value: T, + pub source: Source, +} + +#[cfg(feature = "serde_yaml")] +impl serde::Deserialize<'a>> AargvarkFromStr for AargvarkYaml { + fn from_str(s: &str) -> Result { + let b = AargvarkFile::from_str(s)?; + match serde_yaml::from_slice(&b.value) { + Ok(v) => return Ok(Self { + value: v, + source: b.source, + }), + Err(e) => return Err(e.to_string()), + }; + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern( + vec![ + HelpPatternElement::Variant( + vec![ + ::build_help_pattern(state), + HelpPattern(vec![HelpPatternElement::Literal("-".to_string())]) + ], + ) + ], + ); + } +} + +#[cfg(feature = "serde_yaml")] +impl Clone for AargvarkYaml { + fn clone(&self) -> Self { + return AargvarkYaml { + value: self.value.clone(), + source: self.source.clone(), + }; + } +} + +#[doc(hidden)] +pub fn vark_from_iter>(state: &mut VarkState) -> R { + let mut out = vec![]; + let mut rewind_to = state.position(); + loop { + let r = T::vark(state); + match r { + R::Ok(v) => { + out.push(v); + rewind_to = state.position(); + }, + R::Help(b) => return R::Help(b), + R::Err | R::EOF => { + state.rewind(rewind_to); + return state.r_ok(C::from_iter(out.into_iter())); + }, + }; + } +} + +impl AargvarkTrait for Vec { + fn vark(state: &mut VarkState) -> R { + return vark_from_iter(state); + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Array(T::build_help_pattern(state))]); + } +} + +impl AargvarkTrait for HashSet { + fn vark(state: &mut VarkState) -> R { + return vark_from_iter(state); + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Array(T::build_help_pattern(state))]); + } +} + +/// A key-value argument type (single argument) that takes the format `K=V` where +/// both `K` and `V` need to be string-parsable types. The argument is split at the +/// first unescaped `=` - additional `=` in the key can be escaped with `\`. +/// +/// This is used for the `HashMap` implementation which takes a series of arguments +/// like `a=a b=b c=123`. +pub struct AargvarkKV { + pub key: K, + pub value: V, +} + +impl AargvarkTrait for AargvarkKV { + fn vark(state: &mut VarkState) -> R { + let res = String::vark(state); + let res = match res { + R::EOF => return R::EOF, + R::Err => return R::Err, + R::Help(b) => return R::Help(b), + R::Ok(r) => r, + }; + let mut res = res.into_bytes().into_iter(); + let mut k = vec![]; + let mut escape = false; + for c in &mut res { + if escape { + k.push(c); + escape = false; + } else { + if c == b'\\' { + escape = true; + } else if c == b'=' { + break; + } else { + k.push(c); + } + } + } + let key = match K::from_str(&unsafe { + String::from_utf8_unchecked(k) + }) { + Ok(r) => r, + Err(e) => return state.r_err(format!("Error parsing map key: {}", e)), + }; + let value = match V::from_str(&unsafe { + String::from_utf8_unchecked(res.collect()) + }) { + Ok(r) => r, + Err(e) => return state.r_err(format!("Error parsing map value: {}", e)), + }; + return state.r_ok(AargvarkKV { + key: key, + value: value, + }); + } + + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Literal("K=V".to_string())]); + } +} + +impl AargvarkTrait for HashMap { + fn vark(state: &mut VarkState) -> R { + let res = match >>::vark(state) { + R::EOF => return R::EOF, + R::Err => return R::Err, + R::Help(b) => return R::Help(b), + R::Ok(r) => r, + }; + return state.r_ok(res.into_iter().map(|kv| (kv.key, kv.value)).collect()); + } + + fn build_help_pattern(state: &mut HelpState) -> HelpPattern { + return >>::build_help_pattern(state); + } +} + +/// A simple wrapper around `String` that only accepts strings that don't start +/// with `-`. Having an argument of `Vec` will cause it to consume all +/// remaining arguments (since all of them are valid strings), while `Vec` +/// will only consume until the next flag (or the end of the arguments). +pub struct NotFlag(pub String); + +impl AargvarkFromStr for NotFlag { + fn from_str(s: &str) -> Result { + if s.starts_with("-") { + return Err("Argument looks like a flag (starts with -)".to_string()); + } + return Ok(NotFlag(s.to_string())); + } + + fn build_help_pattern(_state: &mut HelpState) -> HelpPattern { + return HelpPattern(vec![HelpPatternElement::Type("STRING".to_string())]); + } +} + +impl ToString for NotFlag { + fn to_string(&self) -> String { + return self.0.clone(); + } +} + +impl Into for NotFlag { + fn into(self) -> String { + return self.0; + } +} + +impl AsRef for NotFlag { + fn as_ref(&self) -> &str { + return self.0.as_str(); + } +} diff --git a/crates/aargvark_proc_macros/src/lib.rs b/crates/aargvark_proc_macros/src/lib.rs index 53e9155..f28d341 100644 --- a/crates/aargvark_proc_macros/src/lib.rs +++ b/crates/aargvark_proc_macros/src/lib.rs @@ -105,7 +105,7 @@ fn gen_impl_type(ty: &Type, path: &str) -> GenRec { < #t >:: vark(state) }, help_pattern: quote!{ - < #t as aargvark:: AargvarkTrait >:: build_help_pattern(state) + < #t as a:: AargvarkTrait >:: build_help_pattern(state) }, }; }, @@ -141,7 +141,7 @@ fn gen_impl_unnamed( for (i, (field_vark_attr, field_help_docstr, field_ty)) in fields.iter().enumerate() { let eof_code = if i == 0 { quote!{ - break R::EOF; + break a::R::EOF; } } else { quote!{ @@ -169,10 +169,10 @@ fn gen_impl_unnamed( let r = #vark; //. . let #f_ident = match r { - R:: Ok(v) => v, - R:: Help(b) => break R:: Help(b), - R:: Err => break R:: Err, - R:: EOF => { + a:: R:: Ok(v) => v, + a:: R:: Help(b) => break a:: R:: Help(b), + a:: R:: Err => break a:: R:: Err, + a:: R:: EOF => { #eof_code } }; @@ -180,7 +180,7 @@ fn gen_impl_unnamed( copy_fields.push(f_ident.to_token_stream()); let field_help_pattern = gen.help_pattern; help_fields.push(quote!{ - struct_.fields.push(aargvark:: HelpField { + struct_.fields.push(a:: HelpField { id: #placeholder.to_string(), pattern: #field_help_pattern, description: #field_help_docstr.to_string(), @@ -196,7 +196,7 @@ fn gen_impl_unnamed( }, help_pattern: if fields.is_empty() { quote!{ - aargvark::HelpPattern(vec![]) + a::HelpPattern(vec![]) } } else if help_unit_transparent { help_field_patterns.pop().unwrap() @@ -215,7 +215,7 @@ fn gen_impl_unnamed( let mut struct_ = struct_.borrow_mut(); #(#help_fields) * //. . - aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]) + a:: HelpPattern(vec![a::HelpPatternElement::Reference(key)]) } } }, @@ -335,27 +335,27 @@ fn gen_impl_struct( } state.consume(); let #f_local_ident = match #vark { - R:: Ok(v) => v, - R:: Help(b) => return R:: Help(b), - R:: Err => return R:: Err, - R:: EOF => { + a:: R:: Ok(v) => v, + a:: R:: Help(b) => return a:: R:: Help(b), + a:: R:: Err => return a:: R:: Err, + a:: R:: EOF => { return state.r_err(format!("Missing argument for {}", #flag)); } }; flag_fields.#field_ident = Some(#f_local_ident); - return R::Ok(true); + return a::R::Ok(true); } }); } let field_help_pattern; if type_break_help || field_vark_attr.break_help { - field_help_pattern = quote!(aargvark::HelpPattern(vec![])); + field_help_pattern = quote!(a::HelpPattern(vec![])); } else { field_help_pattern = gen.help_pattern; } let help_field = quote!{ - aargvark:: HelpFlagField { + a:: HelpFlagField { option: #optional, flags: vec ![#(#flags.to_string()), *], pattern: #field_help_pattern, @@ -380,7 +380,7 @@ fn gen_impl_struct( .unwrap_or_else(|| field_ident.to_string().to_case(Case::UpperKebab)); let eof_code = if required_i == 0 { quote!{ - break R::EOF; + break a::R::EOF; } } else { quote!{ @@ -393,18 +393,18 @@ fn gen_impl_struct( vark_parse_positional.push(quote!{ let #f_local_ident = loop { if match state.peek() { - PeekR:: None => false, - PeekR:: Help => return R:: Help(Box:: new(move | state | { - return aargvark:: HelpPartialProduction { + a:: PeekR:: None => false, + a:: PeekR:: Help => return a:: R:: Help(Box:: new(move | state | { + return a:: HelpPartialProduction { description: #help_docstr.to_string(), content: build_partial_help(state, #required_i, &flag_fields), }; })), - PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { - R:: Ok(v) => v, - R:: Help(b) => break R:: Help(b), - R:: Err => break R:: Err, - R:: EOF => { + a:: PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { + a:: R:: Ok(v) => v, + a:: R:: Help(b) => break a:: R:: Help(b), + a:: R:: Err => break a:: R:: Err, + a:: R:: EOF => { unreachable!(); } }, @@ -415,10 +415,10 @@ fn gen_impl_struct( break #vark; }; let #f_local_ident = match #f_local_ident { - R:: Ok(v) => v, - R:: Help(b) => break R:: Help(b), - R:: Err => break R:: Err, - R:: EOF => { + a:: R:: Ok(v) => v, + a:: R:: Help(b) => break a:: R:: Help(b), + a:: R:: Err => break a:: R:: Err, + a:: R:: EOF => { #eof_code } }; @@ -427,7 +427,7 @@ fn gen_impl_struct( #field_ident: #f_local_ident }); let help_field = quote!{ - aargvark:: HelpField { + a:: HelpField { id: #field_help_placeholder.to_string(), pattern: #field_help_pattern, description: #field_help_docstr.to_string(), @@ -456,56 +456,56 @@ fn gen_impl_struct( }; fn parse_flags #decl_generics( flag_fields:& mut FlagFields #forward_generics, - state:& mut aargvark:: VarkState, + state:& mut a:: VarkState, s: String - ) -> R < bool > { + ) -> a:: R < bool > { match s.as_str() { #(#vark_parse_flag_cases) * //. . - _ => return R:: Ok(false), + _ => return a:: R:: Ok(false), }; } fn build_partial_help #decl_generics( - state:& mut aargvark:: HelpState, + state:& mut a:: HelpState, required_i: usize, flag_fields:& FlagFields #forward_generics, - ) -> aargvark:: HelpPartialContent { + ) -> a:: HelpPartialContent { let mut help_fields = vec![]; let mut help_flag_fields = vec![]; #(#partial_help_fields) * //. . - return aargvark:: HelpPartialContent:: struct_(help_fields, help_flag_fields); + return a:: HelpPartialContent:: struct_(help_fields, help_flag_fields); } #(#vark_parse_positional) * // Parse any remaining optional args let flag_search_res = loop { match state.peek() { - PeekR:: None => { + a:: PeekR:: None => { break state.r_ok(()); }, - PeekR:: Help => return R:: Help(Box:: new(move | state | { - return aargvark:: HelpPartialProduction { + a:: PeekR:: Help => return a:: R:: Help(Box:: new(move | state | { + return a:: HelpPartialProduction { description: #help_docstr.to_string(), content: build_partial_help(state, #required_i, &flag_fields), }; })), - PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { - R:: Ok(v) => { + a:: PeekR:: Ok(s) => match parse_flags(&mut flag_fields, state, s.to_string()) { + a:: R:: Ok(v) => { if !v { break state.r_ok(()); } }, - R:: Help(b) => break R:: Help(b), - R:: Err => break R:: Err, - R:: EOF => unreachable !(), + a:: R:: Help(b) => break a:: R:: Help(b), + a:: R:: Err => break a:: R:: Err, + a:: R:: EOF => unreachable !(), }, }; }; match flag_search_res { - R::Ok(()) => { }, - R::Help(b) => break R::Help(b), - R::Err => break R::Err, - R::EOF => unreachable!(), + a::R::Ok(()) => { }, + a::R::Help(b) => break a::R::Help(b), + a::R::Err => break a::R::Err, + a::R::EOF => unreachable!(), }; // Build obj + return break state.r_ok(#ident { @@ -531,7 +531,7 @@ fn gen_impl_struct( let mut struct_ = struct_.borrow_mut(); #(#help_fields) * //. . - aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]) + a:: HelpPattern(vec![a::HelpPatternElement::Reference(key)]) } }, }); @@ -559,7 +559,7 @@ fn gen_impl_struct( state.r_ok(#ident) }, help_pattern: quote!{ - aargvark::HelpPattern(vec![]) + a::HelpPattern(vec![]) }, }); }, @@ -649,16 +649,12 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { let help_variant_pattern; if type_attr.break_help || variant_vark_attr.break_help { help_variant_pattern = - quote!( - aargvark::HelpPattern( - vec![aargvark::HelpPatternElement::PseudoReference("...".to_string())], - ) - ); + quote!(a::HelpPattern(vec![a::HelpPatternElement::PseudoReference("...".to_string())])); } else { help_variant_pattern = partial_help_variant_pattern; } help_variants.push(quote!{ - variants.push(aargvark:: HelpVariant { + variants.push(a:: HelpVariant { literal: #name_str.to_string(), pattern: #help_variant_pattern, description: #variant_help_docstr.to_string(), @@ -668,17 +664,17 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { vark = quote!{ { let tag = match state.peek() { - PeekR:: None => return R:: EOF, - PeekR:: Help => return R:: Help(Box:: new(move | state | { + a:: PeekR:: None => return a:: R:: EOF, + a:: PeekR:: Help => return a:: R:: Help(Box:: new(move | state | { let mut variants = vec![]; #(#help_variants) * //. . - return aargvark:: HelpPartialProduction { + return a:: HelpPartialProduction { description: #help_docstr.to_string(), - content: aargvark:: HelpPartialContent:: enum_(variants), + content: a:: HelpPartialContent:: enum_(variants), }; })), - PeekR:: Ok(s) => s, + a:: PeekR:: Ok(s) => s, }; match tag { #(#vark_cases) * _ => { @@ -697,19 +693,26 @@ fn gen_impl(ast: syn::DeriveInput) -> Result { let mut variants = variants.borrow_mut(); #(#help_variants) * //. . - return aargvark:: HelpPattern(vec![aargvark::HelpPatternElement::Reference(key)]); + return a:: HelpPattern(vec![a::HelpPatternElement::Reference(key)]); }; }, syn::Data::Union(_) => panic!("Union not supported"), }; return Ok(quote!{ - impl #decl_generics aargvark:: AargvarkTrait for #ident #forward_generics { - fn vark(state:& mut aargvark:: VarkState) -> aargvark:: R < #ident #forward_generics > { - use aargvark::R; - use aargvark::PeekR; + impl #decl_generics aargvark:: traits_impls:: AargvarkTrait for #ident #forward_generics { + fn vark(state:& mut aargvark:: base:: VarkState) -> aargvark:: base:: R < #ident #forward_generics > { + mod a { + pub use aargvark::help::*; + pub use aargvark::base::*; + pub use aargvark::traits_impls::*; + } #vark } - fn build_help_pattern(state:& mut aargvark:: HelpState) -> aargvark:: HelpPattern { + fn build_help_pattern(state:& mut aargvark:: help:: HelpState) -> aargvark:: help:: HelpPattern { + mod a { + pub use aargvark::help::*; + pub use aargvark::traits_impls::*; + } #help_build } } diff --git a/crates/aargvark_tests/tests/test.rs b/crates/aargvark_tests/tests/test.rs index a418b14..72f33ed 100644 --- a/crates/aargvark_tests/tests/test.rs +++ b/crates/aargvark_tests/tests/test.rs @@ -2,9 +2,9 @@ extern crate aargvark; use { aargvark::{ + traits_impls::AargvarkTrait, vark_explicit, Aargvark, - AargvarkTrait, VarkRet, }, std::collections::HashMap, diff --git a/readme.md b/readme.md index 47f6481..fbee4b6 100644 --- a/readme.md +++ b/readme.md @@ -94,7 +94,7 @@ To parse command line arguments 2. Vark it ```rust - let args = aargvark::vark::(); + let args = vark::(); ``` Non-optional fields become positional arguments unless you give them a flag with `#[vark(flag = "--flag")]`. Optional fields become optional (`--long`) arguments. If you want a `bool` flag that's enabled if the flag is specified (i.e. doesn't take a value), use `Option<()>`. From 6287e1a4e2975d5cd4e189e6a50ac0201bd60381 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 14:58:05 +0900 Subject: [PATCH 48/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index c2a20d4..e314116 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.5.0" +version = "0.6.0" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.1.0" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.0" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 6f34966..0c81cf4 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.1.0" +version = "3.2.0" edition.workspace = true license.workspace = true homepage.workspace = true From 1c1f6b704297ff72553214de2af4648cef2161f6 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 15:06:32 +0900 Subject: [PATCH 49/58] Clarify synonym flags in help output --- crates/aargvark/src/help.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/aargvark/src/help.rs b/crates/aargvark/src/help.rs index 15c0b5b..32c1eaf 100644 --- a/crates/aargvark/src/help.rs +++ b/crates/aargvark/src/help.rs @@ -312,22 +312,31 @@ impl VarkRetHelp { ); } for f in &struct_.flag_fields { - let mut left_col = vec![]; - for flag in &f.flags { + let first_flag = f.flags.first().unwrap(); + for (i, flag) in f.flags.iter().enumerate() { let mut elems = vec![HelpPatternElement::Literal(flag.clone())]; elems.extend(f.pattern.0.clone()); - left_col.push(format!(" {}", if f.option { + let left_col = format!(" {}", if f.option { HelpPattern(vec![HelpPatternElement::Option(HelpPattern(elems))]) } else { HelpPattern(elems) - }.render(stack, help_state))); + }.render(stack, help_state)); + if i == 0 { + table.add_row( + vec![ + comfy_table::Cell::new(left_col), + Cell::new(style_description(&f.description)) + ], + ); + } else { + table.add_row( + vec![ + comfy_table::Cell::new(left_col), + Cell::new(style_description(&format!("(synonym for `{}`)", first_flag))) + ], + ); + } } - table.add_row( - vec![ - comfy_table::Cell::new(left_col.join("\n")), - Cell::new(style_description(&f.description)) - ], - ); } }, HelpProductionType::Enum(fields) => { From a93f56f925c210d263d03313b5f41755e31103bb Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 15:08:45 +0900 Subject: [PATCH 50/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index e314116..498f691 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.6.0" +version = "0.6.1" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.0" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.1" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 0c81cf4..8aebc11 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.2.0" +version = "3.2.1" edition.workspace = true license.workspace = true homepage.workspace = true From 7fb112fa58fc6d5d2e890014c3f5f30880eeba4e Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 16:41:29 +0900 Subject: [PATCH 51/58] Adding some missing boilerplate to NotFlag --- crates/aargvark/src/traits_impls.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/aargvark/src/traits_impls.rs b/crates/aargvark/src/traits_impls.rs index 20fa8d2..30c3f95 100644 --- a/crates/aargvark/src/traits_impls.rs +++ b/crates/aargvark/src/traits_impls.rs @@ -28,6 +28,7 @@ use { SocketAddrV4, SocketAddrV6, }, + ops::Deref, path::PathBuf, }, }; @@ -410,6 +411,7 @@ impl AargvarkTrai /// with `-`. Having an argument of `Vec` will cause it to consume all /// remaining arguments (since all of them are valid strings), while `Vec` /// will only consume until the next flag (or the end of the arguments). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NotFlag(pub String); impl AargvarkFromStr for NotFlag { @@ -431,6 +433,14 @@ impl ToString for NotFlag { } } +impl Deref for NotFlag { + type Target = str; + + fn deref(&self) -> &Self::Target { + return &self.0; + } +} + impl Into for NotFlag { fn into(self) -> String { return self.0; From ed79086a684fe216baa3f1d423420b9d53619877 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sat, 7 Sep 2024 16:41:56 +0900 Subject: [PATCH 52/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index 498f691..7e4b666 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.6.1" +version = "0.6.2" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.1" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.2" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 8aebc11..6945c13 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.2.1" +version = "3.2.2" edition.workspace = true license.workspace = true homepage.workspace = true From b8d434930d6882f98a836408d84cb884ada764c1 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 8 Sep 2024 11:14:25 +0900 Subject: [PATCH 53/58] Remove unnecessary unsafe in KV unescaping --- crates/aargvark/src/traits_impls.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/aargvark/src/traits_impls.rs b/crates/aargvark/src/traits_impls.rs index 30c3f95..a751490 100644 --- a/crates/aargvark/src/traits_impls.rs +++ b/crates/aargvark/src/traits_impls.rs @@ -351,7 +351,7 @@ impl AargvarkTrait for AargvarkKV R::Help(b) => return R::Help(b), R::Ok(r) => r, }; - let mut res = res.into_bytes().into_iter(); + let mut res = res.chars().into_iter(); let mut k = vec![]; let mut escape = false; for c in &mut res { @@ -359,24 +359,20 @@ impl AargvarkTrait for AargvarkKV k.push(c); escape = false; } else { - if c == b'\\' { + if c == '\\' { escape = true; - } else if c == b'=' { + } else if c == '=' { break; } else { k.push(c); } } } - let key = match K::from_str(&unsafe { - String::from_utf8_unchecked(k) - }) { + let key = match K::from_str(&k.into_iter().collect::()) { Ok(r) => r, Err(e) => return state.r_err(format!("Error parsing map key: {}", e)), }; - let value = match V::from_str(&unsafe { - String::from_utf8_unchecked(res.collect()) - }) { + let value = match V::from_str(&res.collect::()) { Ok(r) => r, Err(e) => return state.r_err(format!("Error parsing map value: {}", e)), }; From 460861f58947c56b4945231afa66186ec5cf1123 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 8 Sep 2024 11:18:25 +0900 Subject: [PATCH 54/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index 7e4b666..f040976 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.6.2" +version = "0.6.3" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.2" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.3" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 6945c13..a544966 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.2.2" +version = "3.2.3" edition.workspace = true license.workspace = true homepage.workspace = true From 84be70a3dd4748e5126375dad00d5196cff0b9eb Mon Sep 17 00:00:00 2001 From: andrew <> Date: Thu, 26 Sep 2024 20:40:40 +0900 Subject: [PATCH 55/58] Bump genemichaels --- crates/aargvark_proc_macros/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index a544966..a889928 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] convert_case = "0.6" darling = "0.20.10" -genemichaels-lib = "0.5.1" +genemichaels-lib = "0.5.2" proc-macro2 = "1" quote = "1" syn = "2" From f90f0ecbd3e3033f13af8f6ee775925ec0d73f2f Mon Sep 17 00:00:00 2001 From: andrew <> Date: Thu, 26 Sep 2024 20:40:59 +0900 Subject: [PATCH 56/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index f040976..5c5030a 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.6.3" +version = "0.6.4" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.3" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.4" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index a889928..4356e9c 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.2.3" +version = "3.2.4" edition.workspace = true license.workspace = true homepage.workspace = true From 94f820f98f4ce299e266e5ebab24549282a81bf8 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 10 Nov 2024 03:41:08 +0900 Subject: [PATCH 57/58] Update genemichaels --- crates/aargvark_proc_macros/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index 4356e9c..c001a82 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -13,8 +13,8 @@ proc-macro = true [dependencies] convert_case = "0.6" -darling = "0.20.10" -genemichaels-lib = "0.5.2" +darling = "0.20" +genemichaels-lib = "0.5" proc-macro2 = "1" quote = "1" syn = "2" From 3b586b71e8f3f748463cab60f20e7fcc80792547 Mon Sep 17 00:00:00 2001 From: andrew <> Date: Sun, 10 Nov 2024 03:42:12 +0900 Subject: [PATCH 58/58] chore: Release --- crates/aargvark/Cargo.toml | 4 ++-- crates/aargvark_proc_macros/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aargvark/Cargo.toml b/crates/aargvark/Cargo.toml index 5c5030a..e457595 100644 --- a/crates/aargvark/Cargo.toml +++ b/crates/aargvark/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark" description = "Self-similar argument parsing" -version = "0.6.4" +version = "0.6.5" edition.workspace = true license.workspace = true homepage.workspace = true @@ -18,7 +18,7 @@ serde_yaml = ["dep:serde_yaml", "dep:serde"] http_types = ["dep:http"] [dependencies] -aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.4" } +aargvark_proc_macros = { path = "../aargvark_proc_macros", version = "=3.2.5" } serde_json = { version = "1", optional = true } serde_yaml = { version = "0", optional = true } convert_case = "0.6" diff --git a/crates/aargvark_proc_macros/Cargo.toml b/crates/aargvark_proc_macros/Cargo.toml index c001a82..29f9d04 100644 --- a/crates/aargvark_proc_macros/Cargo.toml +++ b/crates/aargvark_proc_macros/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aargvark_proc_macros" description = "Helper crate for aargvark" -version = "3.2.4" +version = "3.2.5" edition.workspace = true license.workspace = true homepage.workspace = true