├── .gitignore ├── Cargo.toml ├── demos ├── Cargo.toml ├── grep-naive │ ├── src │ │ └── main.rs │ └── Cargo.toml ├── grep-var │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── cargo-naked │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── grep-better │ ├── Cargo.toml │ └── src │ │ └── main.rs └── grep-named │ ├── Cargo.toml │ └── src │ └── main.rs ├── tests ├── Cargo.toml └── src │ ├── fmt.rs │ ├── misc.rs │ └── lib.rs ├── type-cli ├── Cargo.toml └── src │ ├── args.rs │ └── lib.rs ├── type-cli-derive ├── Cargo.toml └── src │ ├── struct_cmd │ ├── mod.rs │ ├── tuple.rs │ └── named.rs │ ├── enum_cmd.rs │ └── lib.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*/target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "type-cli", "type-cli-derive", "tests" 4 | ] 5 | exclude = ["demos"] 6 | -------------------------------------------------------------------------------- /demos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "grep-naive", "grep-better", "grep-var", "grep-named", "cargo-naked" 4 | ] 5 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | type-cli = { path = "../type-cli" } -------------------------------------------------------------------------------- /demos/grep-naive/src/main.rs: -------------------------------------------------------------------------------- 1 | use type_cli::CLI; 2 | 3 | #[derive(CLI)] 4 | struct Grep(String, String); 5 | 6 | fn main() { 7 | let Grep(pattern, file) = Grep::process(); 8 | let pattern = regex::Regex::new(&pattern).unwrap(); 9 | 10 | eprintln!("Searching for `{}` in {}", pattern, file); 11 | } 12 | -------------------------------------------------------------------------------- /demos/grep-var/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grep-var" 3 | version = "0.1.0" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | type-cli = { path = "../../type-cli" } 11 | regex = "1.4" -------------------------------------------------------------------------------- /demos/cargo-naked/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-naked" 3 | version = "0.1.0" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | type-cli = { path = "../../type-cli" } 11 | regex = "1.4" -------------------------------------------------------------------------------- /demos/grep-better/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grep-better" 3 | version = "0.1.0" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | type-cli = { path = "../../type-cli" } 11 | regex = "1.4" 12 | -------------------------------------------------------------------------------- /demos/grep-naive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grep-naive" 3 | version = "0.1.0" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | type-cli = { path = "../../type-cli" } 11 | regex = "1.4" 12 | -------------------------------------------------------------------------------- /demos/grep-named/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grep-named" 3 | version = "0.1.0" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | type-cli = { path = "../../type-cli" } 11 | regex = "1.4" 12 | -------------------------------------------------------------------------------- /demos/grep-better/src/main.rs: -------------------------------------------------------------------------------- 1 | use type_cli::CLI; 2 | 3 | #[derive(CLI)] 4 | struct Grep(regex::Regex, #[optional] Option); 5 | 6 | fn main() { 7 | match Grep::process() { 8 | Grep(pattern, Some(file)) => eprintln!("Serching for `{}` in {}", pattern, file), 9 | Grep(pattern, None) => eprintln!("Searching for `{}` in stdin", pattern), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demos/grep-var/src/main.rs: -------------------------------------------------------------------------------- 1 | use type_cli::CLI; 2 | 3 | #[derive(CLI)] 4 | struct Grep(regex::Regex, #[variadic] Vec); 5 | 6 | fn main(){ 7 | let Grep(pattern, file_list) = Grep::process(); 8 | if file_list.is_empty() { 9 | eprintln!("Searching for `{}` in stdin", pattern); 10 | } else { 11 | eprint!("Searching for `{}` in ", pattern); 12 | file_list.iter().for_each(|f| eprint!("{}, ", f)); 13 | } 14 | } -------------------------------------------------------------------------------- /demos/grep-named/src/main.rs: -------------------------------------------------------------------------------- 1 | use type_cli::CLI; 2 | 3 | #[derive(CLI)] 4 | struct Grep { 5 | pattern: regex::Regex, 6 | 7 | #[named] 8 | file: String, 9 | 10 | #[flag(short = "i")] 11 | ignore_case: bool, 12 | } 13 | 14 | fn main() { 15 | let Grep { pattern, file, ignore_case } = Grep::process(); 16 | eprint!("Searching for `{}` in {}", pattern, file); 17 | if ignore_case { 18 | eprint!(", ignoring case"); 19 | } 20 | eprintln!(); 21 | } 22 | -------------------------------------------------------------------------------- /type-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "type-cli" 3 | version = "0.0.3" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | description = "A convenient, strongly-typed CLI parser." 7 | readme = "../README.md" 8 | repository = "https://github.com/JoJoJet/type-cli" 9 | license = "MIT" 10 | keywords = ["cli", "command", "clap", "derive"] 11 | categories = ["command-line-interface"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | type-cli-derive = "0.0.1" 17 | thiserror = "1.0" 18 | -------------------------------------------------------------------------------- /type-cli-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "type-cli-derive" 3 | version = "0.0.1" 4 | authors = ["JoJoJet "] 5 | edition = "2018" 6 | description = "Derive macro for a convenient, type-safe CLI parser." 7 | repository = "https://github.com/JoJoJet/type-cli" 8 | license = "MIT" 9 | keywords = ["cli", "command", "clap", "derive"] 10 | categories = ["command-line-interface"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | proc-macro2 = "1.0" 19 | syn = {version = "1.0", features = [ "derive", "full" ] } 20 | quote = "1.0" 21 | proc-macro-crate = "0.1" 22 | regex = "1.4" 23 | -------------------------------------------------------------------------------- /demos/cargo-naked/src/main.rs: -------------------------------------------------------------------------------- 1 | use type_cli::CLI; 2 | 3 | #[derive(CLI)] 4 | #[help = "Build manager tool for rust"] 5 | enum Cargo { 6 | New(String), 7 | 8 | #[help = "Build the current crate."] 9 | Build { 10 | #[named] #[optional] 11 | #[help = "the target platform"] 12 | target: Option, 13 | 14 | #[flag] 15 | #[help = "build for release mode"] 16 | release: bool, 17 | }, 18 | 19 | #[help = "Lint your code"] 20 | Clippy { 21 | #[flag] 22 | #[help = "include annoying and subjective lints"] 23 | pedantic: bool, 24 | } 25 | } 26 | 27 | fn main() { 28 | match Cargo::process() { 29 | Cargo::New(name) => eprintln!("Creating new crate `{}`", name), 30 | Cargo::Build { target, release } => { 31 | let target = target.as_deref().unwrap_or("windows"); 32 | if release { 33 | eprintln!("Building for {} in release", target); 34 | } else { 35 | eprintln!("Building for {}", target); 36 | } 37 | } 38 | Cargo::Clippy { pedantic: true } => eprintln!("Annoyingly checking your code."), 39 | Cargo::Clippy { pedantic: false } => eprintln!("Checking your code."), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/src/fmt.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Eq, Debug, type_cli::CLI)] 2 | #[help = "Format a string with an arbitrary number of values"] 3 | pub struct Format(String, #[variadic] Vec); 4 | 5 | #[derive(PartialEq, Eq, Debug, type_cli::CLI)] 6 | #[help = "Print one or two strings"] 7 | pub struct Print(String, #[optional] Option); 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::*; 12 | 13 | #[test] 14 | fn format() { 15 | assert_eq!( 16 | process!(Format, "fmt" "arg1" "arg2").unwrap(), 17 | Format( 18 | "fmt".to_string(), 19 | vec!["arg1".to_string(), "arg2".to_string()] 20 | ) 21 | ); 22 | } 23 | 24 | #[test] 25 | fn print() { 26 | assert_eq!( 27 | process!(Print, "foo" "bar").unwrap(), 28 | Print("foo".to_string(), Some("bar".to_string())) 29 | ); 30 | } 31 | #[test] 32 | fn print_opt() { 33 | assert_eq!( 34 | process!(Print, "foo").unwrap(), 35 | Print("foo".to_string(), None) 36 | ); 37 | } 38 | #[test] 39 | #[should_panic(expected = "Unexpected positional argument `extra-arg`")] 40 | fn print_err() { 41 | process!(Print, "foo" "bar" "extra-arg").unwrap(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /type-cli-derive/src/struct_cmd/mod.rs: -------------------------------------------------------------------------------- 1 | use super::to_snake; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::{format_ident, quote}; 4 | use syn::{self, Attribute, Fields, Ident}; 5 | 6 | mod named; 7 | mod tuple; 8 | 9 | pub(super) fn parse( 10 | cmd_ident: Ident, 11 | attr: Vec, 12 | fields: Fields, 13 | iter_ident: &Ident, 14 | ) -> TokenStream2 { 15 | let mut helpmsg = format!("Help - {}\n", to_snake(&cmd_ident)); 16 | if let Some(help) = try_help!(attr.iter()) { 17 | helpmsg.push_str(&help); 18 | helpmsg.push_str("\n\n"); 19 | } 20 | 21 | let help_ident = format_ident!("HELP"); 22 | 23 | let ctor = match fields { 24 | // 25 | // Named structs. 26 | Fields::Named(fields) => { 27 | let parser = match named::Parser::collect_args(cmd_ident, fields) { 28 | Ok(parser) => parser, 29 | Err(e) => return e.to_compile_error(), 30 | }; 31 | parser.build_help(&mut helpmsg); 32 | parser.into_ctor(iter_ident, &help_ident) 33 | } 34 | 35 | // 36 | // Tuple structs. 37 | Fields::Unnamed(fields) => { 38 | let parser = tuple::Parser::collect_args(cmd_ident, fields); 39 | parser.into_ctor(iter_ident, &help_ident) 40 | } 41 | Fields::Unit => todo!(), 42 | }; 43 | 44 | quote! { 45 | const #help_ident: &str = #helpmsg; 46 | #ctor 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /type-cli/src/args.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::AsRef, str::FromStr}; 2 | use super::{Error, ArgRef}; 3 | use std::error::Error as StdError; 4 | 5 | pub trait Argument : Sized { 6 | fn parse(arg: impl AsRef, arg: ArgRef) -> Result; 7 | } 8 | 9 | impl Argument for T 10 | where ::Err : StdError + 'static 11 | { 12 | fn parse(val: impl AsRef, arg: ArgRef) -> Result { 13 | let val = val.as_ref(); 14 | T::from_str(val).map_err(|e| Error::Parse(arg, Box::new(e))) 15 | } 16 | } 17 | 18 | 19 | pub trait OptionalArg : Sized { 20 | fn parse(arg: impl AsRef, arg: ArgRef) -> Result; 21 | fn default() -> Self; 22 | 23 | fn map_parse(val: Option>, arg: ArgRef) -> Result { 24 | match val { 25 | Some(val) => Self::parse(val, arg), 26 | None => Ok(Self::default()) 27 | } 28 | } 29 | } 30 | 31 | impl OptionalArg for Option { 32 | fn parse(val: impl AsRef, arg: ArgRef) -> Result { 33 | Some(T::parse(val, arg)).transpose() 34 | } 35 | fn default() -> Self { 36 | None 37 | } 38 | } 39 | 40 | 41 | pub trait Flag : Default { 42 | fn increment(&mut self); 43 | } 44 | 45 | impl Flag for bool { 46 | fn increment(&mut self) { 47 | *self = true; 48 | } 49 | } 50 | 51 | impl Flag for Option<()> { 52 | fn increment(&mut self) { 53 | *self = Some(()); 54 | } 55 | } 56 | 57 | macro_rules! int_flag { 58 | ($int: ty) => { 59 | impl $crate::Flag for $int { 60 | fn increment(&mut self){ 61 | *self += 1; 62 | } 63 | } 64 | } 65 | } 66 | 67 | int_flag!(usize); 68 | int_flag!(u8); 69 | int_flag!(u16); 70 | int_flag!(u32); 71 | int_flag!(u64); 72 | int_flag!(isize); 73 | int_flag!(i8); 74 | int_flag!(i16); 75 | int_flag!(i32); 76 | int_flag!(i64); -------------------------------------------------------------------------------- /type-cli-derive/src/enum_cmd.rs: -------------------------------------------------------------------------------- 1 | use std::iter::IntoIterator as IntoIter; 2 | 3 | use super::to_snake; 4 | use proc_macro2::TokenStream as TokenStream2; 5 | use quote::quote; 6 | use syn::{self, Attribute, Ident, Variant}; 7 | 8 | pub(super) fn parse( 9 | cmd_ident: &Ident, 10 | attrs: Vec, 11 | variants: impl IntoIter, 12 | iter_ident: &Ident, 13 | ) -> TokenStream2 { 14 | let parse_ty = crate_path!(Parse); 15 | let help_ty = crate_path!(HelpInfo); 16 | let err_ty = crate_path!(Error); 17 | 18 | let mut subc: Vec = Vec::new(); 19 | 20 | let mut _match = quote! {}; 21 | 22 | for Variant { 23 | ident, 24 | attrs, 25 | fields, 26 | .. 27 | } in variants 28 | { 29 | let name = to_snake(&ident); 30 | 31 | let mut helpmsg = name.clone(); 32 | if let Some(help) = try_help!(attrs.iter()) { 33 | helpmsg.push('\t'); 34 | helpmsg.push_str(&help); 35 | } 36 | subc.push(helpmsg); 37 | 38 | let ctor = super::struct_cmd::parse(ident, attrs, fields, iter_ident); 39 | _match = quote! { 40 | #_match 41 | Some(#name) => { 42 | #ctor 43 | } , 44 | }; 45 | } 46 | 47 | let mut helpmsg = format!("Help - {}\n", to_snake(cmd_ident)); 48 | if let Some(help) = attrs.iter().find(|a| a.path.is_ident("help")) { 49 | match super::parse_help(help) { 50 | Ok(help) => { 51 | helpmsg.push_str(&help); 52 | helpmsg.push_str("\n\n"); 53 | } 54 | Err(e) => return e.to_compile_error().into(), 55 | } 56 | } 57 | 58 | helpmsg.push_str("SUBCOMMANDS:\n"); 59 | for subc in subc { 60 | helpmsg.push_str(" "); 61 | helpmsg.push_str(&subc); 62 | helpmsg.push('\n'); 63 | } 64 | 65 | quote! { 66 | use #cmd_ident::*; 67 | 68 | const HELP: &str = #helpmsg; 69 | 70 | match #iter_ident.next().as_deref() { 71 | #_match 72 | Some("--help") | Some("-h") | None => return Ok(#parse_ty::Help(#help_ty(HELP))), 73 | Some(sub) => return Err(#err_ty::UnknownSub(sub.to_string())), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/src/misc.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Eq, Debug, type_cli::CLI)] 2 | #[help = "Format a person's name"] 3 | pub struct Name { 4 | #[help = "First name"] 5 | first: String, 6 | #[optional] 7 | #[help = "Last name"] 8 | last: Option, 9 | } 10 | 11 | #[derive(PartialEq, Eq, Debug, type_cli::CLI)] 12 | pub struct Oof { 13 | ouch: String, 14 | #[named(short = "c")] 15 | #[optional] 16 | count: Option, 17 | } 18 | 19 | #[derive(PartialEq, Eq, Debug, type_cli::CLI)] 20 | pub struct Ls { 21 | #[optional] 22 | dir: Option, 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | #[test] 30 | fn name() { 31 | assert_eq!( 32 | process!(Name, "Robb" "Stark").unwrap(), 33 | Name { 34 | first: "Robb".to_string(), 35 | last: Some("Stark".to_string()) 36 | } 37 | ); 38 | } 39 | #[test] 40 | fn name_op() { 41 | assert_eq!( 42 | process!(Name, "Pate").unwrap(), 43 | Name { 44 | first: "Pate".to_string(), 45 | last: None 46 | } 47 | ); 48 | } 49 | #[test] 50 | #[should_panic(expected = "Help - name")] 51 | fn name_help() { 52 | process!(Name, "--help").unwrap(); 53 | } 54 | #[test] 55 | #[should_panic(expected = "Help - name")] 56 | fn name_help2() { 57 | process!(Name, "-h").unwrap(); 58 | } 59 | #[test] 60 | #[should_panic(expected = "Help - name")] 61 | fn name_help3() { 62 | process!(Name,).unwrap(); 63 | } 64 | 65 | #[test] 66 | fn oof() { 67 | assert_eq!( 68 | process!(Oof, "foo" "--count" "4").unwrap(), 69 | Oof { 70 | ouch: "foo".to_string(), 71 | count: Some(4) 72 | } 73 | ); 74 | assert_eq!( 75 | process!(Oof, "foo").unwrap(), 76 | Oof { 77 | ouch: "foo".to_string(), 78 | count: None 79 | } 80 | ); 81 | } 82 | #[test] 83 | fn oof_short() { 84 | assert_eq!( 85 | process!(Oof, "-c" "12" "foo").unwrap(), 86 | Oof { 87 | ouch: "foo".to_string(), 88 | count: Some(12) 89 | } 90 | ); 91 | } 92 | #[test] 93 | #[should_panic(expected = "Error parsing argument `--count`")] 94 | fn oof_err() { 95 | process!(Oof, "foo" "--count" "kevin").unwrap(); 96 | } 97 | 98 | #[test] 99 | fn ls_arg() { 100 | assert_eq!(process!(Ls, "dir").unwrap(), Ls { dir: Some("dir".to_string()) }); 101 | } 102 | #[test] 103 | fn ls_none() { 104 | assert_eq!(process!(Ls,).unwrap(), Ls { dir: None }); 105 | } 106 | #[test] 107 | #[should_panic(expected = "Help - ls")] 108 | fn ls_help() { 109 | process!(Ls, "--help").unwrap(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /type-cli-derive/src/struct_cmd/tuple.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream as TokenStream2; 2 | use quote::quote; 3 | use syn::{self, Ident}; 4 | 5 | struct Arg { 6 | required: bool, 7 | variadic: bool, 8 | } 9 | 10 | pub(super) struct Parser { 11 | cmd_ident: Ident, 12 | args: Vec, 13 | } 14 | impl Parser { 15 | /// Process the fields of the tuple struct from `syn` into a form relevant to CLI. 16 | pub fn collect_args(cmd_ident: Ident, fields: syn::FieldsUnnamed) -> Self { 17 | let mut args: Vec = Vec::new(); 18 | for (i, syn::Field { attrs, .. }) in fields.unnamed.into_iter().enumerate() { 19 | if args.last().map_or(false, |a| a.variadic) { 20 | panic!("Variadic arguments must come last."); 21 | } 22 | let required = !attrs.iter().any(|a| a.path.is_ident("optional")); 23 | if required && args.last().map_or(false, |a| !a.required) { 24 | panic!( 25 | "Required argument at position `{}` must come before any optional arguments.", 26 | i + 1 27 | ); 28 | } 29 | let variadic = attrs.iter().any(|a| a.path.is_ident("variadic")); 30 | args.push(Arg { required, variadic }); 31 | } 32 | 33 | Self { cmd_ident, args } 34 | } 35 | /// Convert this parser into ctor code for a CLI parser. 36 | pub fn into_ctor(self, iter: &Ident, _help_ident: &Ident) -> TokenStream2 { 37 | let arg_ty = crate_path!(Argument); 38 | let opt_ty = crate_path!(OptionalArg); 39 | let err_ty = crate_path!(Error); 40 | let argref_ty = crate_path!(ArgRef); 41 | 42 | let Self { cmd_ident, args } = self; 43 | let mut ctor = quote! {}; 44 | for (i, Arg { required, variadic }) in args.into_iter().enumerate() { 45 | let i = i + 1; 46 | // Variadic arguments. 47 | ctor = if variadic { 48 | // Run collect `by_ref` so it doesn't move the iterator. 49 | quote! { 50 | #ctor 51 | #iter.by_ref() 52 | .enumerate() 53 | .map(|(i, val)| #arg_ty::parse(val, #argref_ty::Positional(#i + i))) 54 | .collect::>()? , 55 | } 56 | } 57 | // Required arguments. 58 | else if required { 59 | quote! { 60 | #ctor 61 | #arg_ty::parse(#iter.next().ok_or(#err_ty::ExpectedPositional(#i))?, #argref_ty::Positional(#i))? , 62 | } 63 | } 64 | // Optional arguments. 65 | else { 66 | quote! { 67 | #ctor 68 | #opt_ty::map_parse(#iter.next(), #argref_ty::Positional(#i))? , 69 | } 70 | } 71 | } 72 | quote! { 73 | let val = #cmd_ident ( 74 | #ctor 75 | ); 76 | if let Some(a) = #iter.next() { 77 | return Err(#err_ty::ExtraArg(a)); 78 | } 79 | val 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /type-cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | 3 | pub use type_cli_derive::CLI; 4 | 5 | mod args; 6 | pub use args::{Argument, Flag, OptionalArg}; 7 | 8 | pub trait CLI: Sized { 9 | /// 10 | /// Parses the arguments as a command-line interface of the current type, 11 | /// returning errors as a value for manul handling. 12 | /// 13 | /// If you don't need fine control over program flow, use `CLI::processs` instead. 14 | fn parse(args: impl std::iter::Iterator) -> Result, Error>; 15 | /// 16 | /// Parses `std::env::args()` as a command-line interface of the current type. 17 | /// 18 | /// If an error occurs while parsing, it will be send to stderr and the process will exit. 19 | /// If the user enters `--help` or `-h`, help info will be sent to stderr and the process will exit. 20 | /// 21 | /// If you want finer control over program flow, use `CLI::parse` instead. 22 | fn process() -> Self { 23 | match Self::parse(std::env::args()) { 24 | Ok(Parse::Success(val)) => val, 25 | Ok(Parse::Help(help)) => { 26 | eprintln!("{}", help); 27 | std::process::exit(1); 28 | } 29 | Err(e) => { 30 | eprintln!("{}", e); 31 | std::process::exit(101); 32 | } 33 | } 34 | } 35 | } 36 | 37 | /// 38 | /// A result of successful command-line interface parsing. 39 | /// 40 | /// This is either a data structure holding the arguments passed to the program, 41 | /// or a string containing help info about the current command. 42 | pub enum Parse { 43 | Success(T), 44 | Help(HelpInfo), 45 | } 46 | 47 | pub struct HelpInfo(pub &'static str); 48 | 49 | impl std::fmt::Display for HelpInfo { 50 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 51 | write!(f, "{}", self.0) 52 | } 53 | } 54 | 55 | #[derive(thiserror::Error)] 56 | pub enum Error { 57 | #[error("Expected an argument named `{0}`")] 58 | ExpectedNamed(&'static str), 59 | #[error("Expected an argument at position `{0}`")] 60 | ExpectedPositional(usize), 61 | #[error("Expected a value after argument `{0}`")] 62 | ExpectedValue(&'static str), 63 | #[error("Unknown flag `{0}`")] 64 | UnknownFlag(String), 65 | #[error("Unexpected positional argument `{0}`")] 66 | ExtraArg(String), 67 | #[error("Unknown subcommand `{0}`")] 68 | UnknownSub(String), 69 | #[error("Error parsing {0}:\n{1}")] 70 | Parse(ArgRef, Box), 71 | } 72 | 73 | /// A way to refer to an argument in an error. 74 | pub enum ArgRef { 75 | Positional(usize), 76 | Named(&'static str), 77 | } 78 | use std::fmt::{self, Display}; 79 | impl Display for ArgRef { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | match self { 82 | &ArgRef::Positional(index) => { 83 | write!(f, "positional argument `{}`", index) 84 | } 85 | &ArgRef::Named(name) => { 86 | write!(f, "argument `{}`", name) 87 | } 88 | } 89 | } 90 | } 91 | 92 | impl std::fmt::Debug for Error { 93 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 94 | std::fmt::Display::fmt(self, f) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /type-cli-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{self, Attribute, Item}; 4 | 5 | macro_rules! crate_path { 6 | ($typ: tt) => {{ 7 | let crate_name = proc_macro_crate::crate_name("type-cli") 8 | .expect("`type-cli` is present in `Cargo.toml`"); 9 | let crate_name = quote::format_ident!("{}", crate_name); 10 | quote::quote! { ::#crate_name::$typ } 11 | }}; 12 | () => {{ 13 | let crate_name = proc_macro_crate::crate_name("type-cli") 14 | .expect("`type-cli` is present in `Cargo.toml`"); 15 | let crate_name = quote::format_ident!("{}", crate_name); 16 | quote::quote! { ::#crate_name } 17 | }}; 18 | } 19 | 20 | macro_rules! try_help { 21 | ($iter: expr) => {{ 22 | let mut iter = $iter; 23 | if let Some(help) = iter.find(|a| a.path.is_ident("help")) { 24 | match $crate::parse_help(help) { 25 | Ok(help) => Some(help), 26 | Err(e) => return e.to_compile_error().into(), 27 | } 28 | } else { 29 | None 30 | } 31 | }}; 32 | } 33 | 34 | mod enum_cmd; 35 | mod struct_cmd; 36 | 37 | #[proc_macro_derive(CLI, attributes(help, named, flag, optional, variadic))] 38 | pub fn cli(item: TokenStream) -> TokenStream { 39 | let parse_ty = crate_path!(Parse); 40 | let err_ty = crate_path!(Error); 41 | let cli_ty = crate_path!(CLI); 42 | 43 | let input: Item = syn::parse(item).expect("failed to parse"); 44 | 45 | let iter_ident = format_ident!("ARGS_ITER"); 46 | let cmd_ident; 47 | 48 | let body = match input { 49 | Item::Enum(item) => { 50 | cmd_ident = item.ident; 51 | enum_cmd::parse(&cmd_ident, item.attrs, item.variants, &iter_ident) 52 | } 53 | Item::Struct(item) => { 54 | cmd_ident = item.ident.clone(); 55 | struct_cmd::parse(item.ident, item.attrs, item.fields, &iter_ident) 56 | } 57 | _ => panic!("Only allowed on structs and enums."), 58 | }; 59 | 60 | let ret = quote! { 61 | impl #cli_ty for #cmd_ident { 62 | fn parse(mut #iter_ident : impl std::iter::Iterator) -> Result<#parse_ty<#cmd_ident>, #err_ty> { 63 | let _ = #iter_ident.next(); 64 | let ret = { 65 | #body 66 | }; 67 | Ok(#parse_ty::Success(ret)) 68 | } 69 | } 70 | }; 71 | ret.into() 72 | } 73 | 74 | fn parse_help(help: &Attribute) -> syn::Result { 75 | match help.parse_meta()? { 76 | syn::Meta::NameValue(meta) => { 77 | if let syn::Lit::Str(help) = meta.lit { 78 | Ok(help.value()) 79 | } else { 80 | Err(syn::Error::new_spanned( 81 | help.tokens.clone(), 82 | "Help message must be a string literal", 83 | )) 84 | } 85 | } 86 | _ => Err(syn::Error::new_spanned( 87 | help.tokens.clone(), 88 | r#"Help must be formatted as #[help = "msg"]"#, 89 | )), 90 | } 91 | } 92 | 93 | fn to_snake(ident: &impl ToString) -> String { 94 | let ident = ident.to_string(); 95 | let mut val = String::with_capacity(ident.len()); 96 | for (i, ch) in ident.chars().enumerate() { 97 | if ch.is_uppercase() { 98 | if i > 0 { 99 | val.push('-'); 100 | } 101 | val.push(ch.to_ascii_lowercase()); 102 | } else { 103 | val.push(ch); 104 | } 105 | } 106 | val 107 | } 108 | -------------------------------------------------------------------------------- /tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Eq, Debug, type_cli::CLI)] 2 | #[help = "Save or load files."] 3 | pub enum FileSystem { 4 | #[help = "Save a file"] 5 | Save { 6 | #[help = "Name of the destination file"] 7 | name: String, 8 | #[flag(short = "v")] 9 | #[help = "Print on success"] 10 | verbose: bool, 11 | }, 12 | #[help = "Load a file"] 13 | LoadFile { 14 | #[help = "The file to load"] 15 | file: String, 16 | #[variadic] 17 | #[help = "Some options or something idk"] 18 | bytes: Vec, 19 | #[named] 20 | #[help = "How long to wait before cancelling (ms)"] 21 | time_out: u64, 22 | }, 23 | } 24 | 25 | #[cfg(test)] 26 | macro_rules! args { 27 | ($($st : literal)*) => { 28 | vec![$($st.to_string()),*].into_iter() 29 | } 30 | } 31 | #[cfg(test)] 32 | macro_rules! parse { 33 | ($ty: ty, $($st: literal)*) => { 34 | <$ty as type_cli::CLI>::parse(args!("type-cli" $($st)*)) 35 | } 36 | } 37 | #[cfg(test)] 38 | macro_rules! process { 39 | ($ty: ty, $($st: literal)*) => { 40 | match parse!($ty, $($st)*) { 41 | Ok(type_cli::Parse::Success(val)) => Ok(val), 42 | Ok(type_cli::Parse::Help(h)) => panic!("{}", h), 43 | Err(e) => Err(e), 44 | } 45 | } 46 | } 47 | 48 | mod fmt; 49 | mod misc; 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | 55 | #[test] 56 | #[should_panic(expected = "Help - file-system")] 57 | fn help() { 58 | process!(FileSystem,).unwrap(); 59 | } 60 | #[test] 61 | #[should_panic(expected = "Help - file-system")] 62 | fn help2() { 63 | process!(FileSystem, "--help").unwrap(); 64 | } 65 | #[test] 66 | #[should_panic(expected = "Help - file-system")] 67 | fn help3() { 68 | process!(FileSystem, "-h").unwrap(); 69 | } 70 | 71 | #[test] 72 | fn save() { 73 | assert_eq!( 74 | process!(FileSystem, "save" "foo").unwrap(), 75 | FileSystem::Save { 76 | name: "foo".to_string(), 77 | verbose: false 78 | } 79 | ); 80 | } 81 | #[test] 82 | fn save_verbose() { 83 | assert_eq!( 84 | process!(FileSystem, "save" "--verbose" "foo").unwrap(), 85 | FileSystem::Save { 86 | name: "foo".to_string(), 87 | verbose: true 88 | } 89 | ); 90 | } 91 | #[test] 92 | fn save_v() { 93 | assert_eq!( 94 | process!(FileSystem, "save" "foo" "-v").unwrap(), 95 | FileSystem::Save { 96 | name: "foo".to_string(), 97 | verbose: true 98 | } 99 | ); 100 | } 101 | #[test] 102 | #[should_panic(expected = "Expected an argument at position `1`")] 103 | fn save_err() { 104 | process!(FileSystem, "save" "-v").unwrap(); 105 | } 106 | #[test] 107 | #[should_panic(expected = "Unexpected positional argument `too-many`")] 108 | fn save_err2() { 109 | process!(FileSystem, "save" "foo" "too-many").unwrap(); 110 | } 111 | #[test] 112 | #[should_panic(expected = "Help - save")] 113 | fn save_help() { 114 | process!(FileSystem, "save" "--help").unwrap(); 115 | } 116 | #[test] 117 | #[should_panic(expected = "Help - save")] 118 | fn save_help2() { 119 | process!(FileSystem, "save").unwrap(); 120 | } 121 | 122 | #[test] 123 | fn load_file() { 124 | assert_eq!( 125 | process!(FileSystem, "load-file" "foo" "--time-out" "8").unwrap(), 126 | FileSystem::LoadFile { 127 | file: "foo".to_string(), 128 | bytes: Vec::new(), 129 | time_out: 8 130 | } 131 | ); 132 | } 133 | #[test] 134 | fn load_file_bytes() { 135 | assert_eq!( 136 | process!(FileSystem, "load-file" "foo" "7" "255" "--time-out" "8").unwrap(), 137 | FileSystem::LoadFile { 138 | file: "foo".to_string(), 139 | bytes: vec![7, 255], 140 | time_out: 8 141 | } 142 | ); 143 | } 144 | #[test] 145 | fn load_file_bytes2() { 146 | assert_eq!( 147 | process!(FileSystem, "load-file" "foo" "15" "48" "--time-out" "8" "29").unwrap(), 148 | FileSystem::LoadFile { 149 | file: "foo".to_string(), 150 | bytes: vec![15, 48, 29], 151 | time_out: 8 152 | } 153 | ); 154 | } 155 | #[test] 156 | #[should_panic(expected = "Expected an argument named `--time-out`")] 157 | fn load_file_err() { 158 | process!(FileSystem, "load-file" "foo").unwrap(); 159 | } 160 | #[test] 161 | #[should_panic(expected = "Expected a value after argument `--time-out`")] 162 | fn load_file_err2() { 163 | process!(FileSystem, "load-file" "foo" "--time-out").unwrap(); 164 | } 165 | #[test] 166 | #[should_panic(expected = "Unknown flag `--lime-out`")] 167 | fn load_file_err3() { 168 | process!(FileSystem, "load-file" "foo" "--lime-out").unwrap(); 169 | } 170 | #[test] 171 | #[should_panic(expected = "Help - load-file")] 172 | fn load_file_help() { 173 | process!(FileSystem, "load-file" "--help").unwrap(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # type-cli 2 | `type-cli` is a convenient, strongly-typed command-line interface parser. 3 | 4 | To start, let's create an interface for `grep`. 5 | 6 | ## Basics 7 | 8 | ```rust 9 | use type_cli::CLI; 10 | 11 | #[derive(CLI)] 12 | struct Grep(String, String); 13 | 14 | fn main() { 15 | let Grep(pattern, file) = Grep::process(); 16 | let pattern = regex::Regex::new(&pattern).unwrap(); 17 | 18 | eprintln!("Searching for `{}` in {}", pattern, file); 19 | } 20 | ``` 21 | 22 | Now, if we run the binary with arguments, they'll be properly parsed. 23 | And if we miss an argument, it'll give a helpful error. 24 | 25 | ``` 26 | $ grep foo* myFile 27 | Searching for `foo*` in myFile 28 | 29 | $ grep foo* 30 | Expected an argument at position `2` 31 | ``` 32 | 33 | However, this isn't exactly a faithful grep interface: in grep, the file is optional. Plus, that `unwrap()` is a little gross. 34 | 35 | # 36 | 37 | ```rust 38 | use type_cli::CLI; 39 | 40 | #[derive(CLI)] 41 | struct Grep(regex::Regex, #[optional] Option); 42 | 43 | fn main() { 44 | match Grep::process() { 45 | Grep(pattern, Some(file)) => eprintln!("Serching for `{}` in {}", pattern, file), 46 | Grep(pattern, None) => eprintln!("Searching for `{}` in stdin", pattern), 47 | } 48 | } 49 | ``` 50 | 51 | What's that? We're accepting a `Regex` directly as an argument? In `type-cli`, any type that implements `FromStr` can be an argument. 52 | Any parsing errors will be gracefully passed back to the user without you having to worry about it. 53 | 54 | ``` 55 | $ grep foo( 56 | Error parsing positional argument `1`: 57 | regex parse error: 58 | foo( 59 | ^ 60 | error: unclosed group 61 | ``` 62 | 63 | Here, you can also see that optional arguments must be annotated with `#[optional]`. 64 | 65 | ``` 66 | $ grep foo* myFile 67 | Serching for `foo*` in myFile 68 | 69 | $ grep foo* 70 | Searching for `foo*` in stdin 71 | ``` 72 | 73 | This interface _still_ isn't faithful though; `grep` allows multiple files to be searched. 74 | 75 | # 76 | 77 | ```rust 78 | use type_cli::CLI; 79 | 80 | #[derive(CLI)] 81 | struct Grep(regex::Regex, #[variadic] Vec); 82 | 83 | fn main(){ 84 | let Grep(pattern, file_list) = Grep::process(); 85 | if file_list.is_empty() { 86 | eprintln!("Searching for `{}` in stdin", pattern); 87 | } else { 88 | eprint!("Searching for `{}` in ", pattern); 89 | file_list.iter().for_each(|f| eprint!("{}, ", f)); 90 | } 91 | } 92 | ``` 93 | 94 | If you annote the final field with `#[variadic]`, it will parse an arbitrary number of arguments. 95 | This works for any collection that implements `FromIterator`. 96 | 97 | ``` 98 | $ grep foo* 99 | Searching for `foo*` in stdin 100 | 101 | $grep foo* myFile yourFile ourFile 102 | Searching for `foo*` in myFile, yourFile, ourFile, 103 | ``` 104 | 105 | This still isn't ideal, though. None of the fields have names, and there's no flags or options! 106 | Clearly, tuple structs are limiting us. 107 | 108 | ## Named arguments and flags 109 | 110 | ```rust 111 | use type_cli::CLI; 112 | 113 | #[derive(CLI)] 114 | struct Grep { 115 | pattern: regex::Regex, 116 | 117 | #[named] 118 | file: String, 119 | 120 | #[flag(short = "i")] 121 | ignore_case: bool, 122 | } 123 | 124 | fn main() { 125 | let Grep { pattern, file, ignore_case } = Grep::process(); 126 | eprint!("Searching for `{}` in {}", pattern, file); 127 | if ignore_case { 128 | eprint!(", ignoring case"); 129 | } 130 | eprintln!(); 131 | } 132 | ``` 133 | 134 | Named arguments are annoted with `#[named]`, and that allows them to be passed to the command in any order. 135 | By default, named arguments are still required, but they can also be marked with `#[optional]`. 136 | 137 | ``` 138 | $ grep foo* 139 | Expected an argument named `--file` 140 | 141 | $ grep foo* --file myFile 142 | Searching for `foo*` in myFile 143 | ``` 144 | 145 | Flags are annoted with `#[flag]`, and are completely optional boolean or integer flags. 146 | You can optionally specify a shorter form with `#[flag(short = "a")]` (this form also works for named arguments). 147 | 148 | ``` 149 | $ grep foo* --file myFile --ignore-case 150 | Searching for `foo*` in myFile, ignoring case 151 | 152 | $ grep foo* --file myFile -i 153 | Searching for `foo*` in myFile, ignoring case 154 | ``` 155 | 156 | This seems well and good, but what if I want multiple commands in my application? 157 | 158 | ## Subcommands 159 | 160 | ```rust 161 | use type_cli::CLI; 162 | 163 | #[derive(CLI)] 164 | enum Cargo { 165 | New(String), 166 | Build { 167 | #[named] #[optional] 168 | target: Option, 169 | #[flag] 170 | release: bool, 171 | }, 172 | Clippy { 173 | #[flag] 174 | pedantic: bool, 175 | } 176 | } 177 | 178 | fn main() { 179 | match Cargo::process() { 180 | Cargo::New(name) => eprintln!("Creating new crate `{}`", name), 181 | Cargo::Build { target, release } => { 182 | let target = target.as_deref().unwrap_or("windows"); 183 | if release { 184 | eprintln!("Building for {} in release", target); 185 | } else { 186 | eprintln!("Building for {}", target); 187 | } 188 | } 189 | Cargo::Clippy { pedantic: true } => eprintln!("Annoyingly checking your code."), 190 | Cargo::Clippy { pedantic: false } => eprintln!("Checking your code."), 191 | } 192 | } 193 | ``` 194 | 195 | If you derive `CLI` on an enum, each variant will represent a subcommand. 196 | Each subcommand is parsed with the same syntax as before. 197 | 198 | Rust's pascal case will be automatically converted to the standard for shells: 199 | `SubCommand` -> `sub-command` 200 | 201 | ``` 202 | $ cargo new myCrate 203 | Creating new crate `myCrate` 204 | 205 | $ cargo build 206 | Building for windows 207 | 208 | $ cargo build --target linux 209 | Building for linux 210 | 211 | $ cargo build --target linux --release 212 | Building for linux in release 213 | 214 | $ cargo clippy 215 | Checking your code. 216 | 217 | $ cargo clippy --pedantic 218 | Annoyingly checking your code. 219 | ``` 220 | 221 | What about documentation? 222 | 223 | ## --help 224 | 225 | ```rust 226 | use type_cli::CLI; 227 | 228 | #[derive(CLI)] 229 | #[help = "Build manager tool for rust"] 230 | enum Cargo { 231 | New(String), 232 | 233 | #[help = "Build the current crate."] 234 | Build { 235 | #[named] #[optional] 236 | #[help = "the target platform"] 237 | target: Option, 238 | 239 | #[flag] 240 | #[help = "build for release mode"] 241 | release: bool, 242 | }, 243 | 244 | #[help = "Lint your code"] 245 | Clippy { 246 | #[flag] 247 | #[help = "include annoying and subjective lints"] 248 | pedantic: bool, 249 | } 250 | } 251 | ``` 252 | 253 | `type-cli` will automatically generate a help screen for your commands. 254 | If you annote a subcommand or argument with `#[help = ""]`, it will include your short description. 255 | When shown, it will be sent to stderr and the process will exit with a nonzero status. 256 | 257 | ``` 258 | $ cargo 259 | Help - cargo 260 | Build manager tool for rust 261 | 262 | SUBCOMMANDS: 263 | new 264 | build Build the current crate. 265 | clippy Lint your code 266 | ``` 267 | 268 | For enums, this will be shown if the command is called without specifying a subcommand. 269 | 270 | ``` 271 | $ cargo build --help 272 | Help - build 273 | Build the current crate. 274 | 275 | ARGUMENTS: 276 | --target the target platform [optional] 277 | 278 | FLAGS: 279 | --release build for release mode 280 | 281 | 282 | $ cargo clippy -h 283 | Help - clippy 284 | Lint your code 285 | 286 | FLAGS: 287 | --pedantic include annoying and subjective lints 288 | ``` 289 | 290 | For structs or subcommands, this will be called if the flag `--help` or `-h` is passed. 291 | Help messages are not currently supported for tuple structs. -------------------------------------------------------------------------------- /type-cli-derive/src/struct_cmd/named.rs: -------------------------------------------------------------------------------- 1 | use crate::to_snake; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::{format_ident, quote}; 4 | use syn::{self, Ident, Type}; 5 | 6 | struct Arg { 7 | ident: Ident, 8 | l_ident: Ident, 9 | arg_name: String, // The cli-name of the argument. `--arg` 10 | short: Option, // short name of the argument. `-a` 11 | name: String, // The cli-name sans `--` 12 | help: Option, 13 | ty: Type, 14 | required: bool, 15 | variadic: bool, 16 | } 17 | impl Arg { 18 | pub fn new( 19 | ident: Ident, 20 | short: Option, 21 | help: Option, 22 | ty: Type, 23 | required: bool, 24 | variadic: bool, 25 | ) -> Self { 26 | let name = to_snake(&ident); 27 | Self { 28 | ident, 29 | l_ident: format_ident!("{}", name), 30 | arg_name: format!("--{}", name.replace("_", "-")), 31 | name, 32 | short: short.map(|s| format!("-{}", s)), 33 | help, 34 | ty, 35 | required, 36 | variadic, 37 | } 38 | } 39 | } 40 | 41 | pub(super) struct Parser { 42 | cmd_ident: Ident, 43 | pos_args: Vec, 44 | named_args: Vec, 45 | flags: Vec, 46 | } 47 | impl Parser { 48 | /// 49 | /// Process the fields of the struct into a form relevant to CLI. 50 | pub fn collect_args(cmd_ident: Ident, fields: syn::FieldsNamed) -> syn::Result { 51 | let short_reg = regex::Regex::new(r#"short\s*=\s*"(.*)""#).unwrap(); 52 | 53 | let mut pos_args: Vec = Vec::new(); 54 | let mut named_args: Vec = Vec::new(); 55 | let mut flags: Vec = Vec::new(); 56 | let mut any_variadic = false; 57 | for syn::Field { 58 | ident, attrs, ty, .. 59 | } in fields.named 60 | { 61 | let ident = ident.expect("field has an identifier"); 62 | 63 | let required = !attrs.iter().any(|a| a.path.is_ident("optional")); 64 | let variadic = attrs.iter().any(|a| a.path.is_ident("variadic")); 65 | 66 | //let help = try_help!(attrs.iter()); 67 | let help = attrs 68 | .iter() 69 | .find(|a| a.path.is_ident("help")) 70 | .map(crate::parse_help) 71 | .transpose()?; 72 | 73 | // Named arguments. 74 | if let Some(named) = attrs.iter().find(|a| a.path.is_ident("named")) { 75 | if variadic { 76 | panic!("Named argument `{}` cannot be variadic.", ident.to_string()); 77 | } 78 | let short = short_reg 79 | .captures(&named.tokens.to_string()) 80 | .map(|cap| cap[1].to_string()); 81 | named_args.push(Arg::new(ident, short, help, ty, required, false)); 82 | } 83 | // Flags. 84 | else if let Some(flag) = attrs.iter().find(|a| a.path.is_ident("flag")) { 85 | if variadic { 86 | panic!("Flag `{}` cannot be variadic.", ident.to_string()); 87 | } 88 | let short = short_reg 89 | .captures(&flag.tokens.to_string()) 90 | .map(|cap| cap[1].to_string()); 91 | flags.push(Arg::new(ident, short, help, ty, required, false)); 92 | } 93 | // Positional arguments. 94 | else { 95 | if required && pos_args.last().map_or(false, |a| !a.required) { 96 | panic!("Required positional argument `{}` must come before any optional arguments.", ident.to_string()); 97 | } 98 | if any_variadic { 99 | panic!( 100 | "Positional argument `{}` must come before the variadic argument.", 101 | ident.to_string() 102 | ); 103 | } 104 | any_variadic = any_variadic || variadic; 105 | pos_args.push(Arg::new(ident, None, help, ty, required, variadic)); 106 | } 107 | } 108 | 109 | Ok(Self { 110 | cmd_ident, 111 | pos_args, 112 | named_args, 113 | flags, 114 | }) 115 | } 116 | /// 117 | /// Build help info about this command's arguments. 118 | pub fn build_help(&self, helpmsg: &mut String) { 119 | // Help info for arguments. 120 | let args_empty = self.pos_args.is_empty() && self.named_args.is_empty(); 121 | if !args_empty { 122 | helpmsg.push_str("ARGUMENTS:\n"); 123 | } 124 | for arg in &self.pos_args { 125 | helpmsg.push_str(" "); 126 | helpmsg.push_str(&arg.name); 127 | if let Some(help) = &arg.help { 128 | helpmsg.push('\t'); 129 | helpmsg.push_str(help); 130 | } 131 | if arg.variadic { 132 | helpmsg.push('\t'); 133 | helpmsg.push_str("[variadic]"); 134 | } 135 | if !arg.required { 136 | helpmsg.push('\t'); 137 | helpmsg.push_str("[optional]"); 138 | } 139 | helpmsg.push('\n'); 140 | } 141 | for arg in &self.named_args { 142 | helpmsg.push_str(" "); 143 | if let Some(short) = &arg.short { 144 | helpmsg.push_str(short); 145 | helpmsg.push_str(", "); 146 | } 147 | helpmsg.push_str(&arg.arg_name); 148 | if let Some(help) = &arg.help { 149 | helpmsg.push('\t'); 150 | helpmsg.push_str(help); 151 | } 152 | if !arg.required { 153 | helpmsg.push('\t'); 154 | helpmsg.push_str("[optional]"); 155 | } 156 | helpmsg.push('\n'); 157 | } 158 | if !args_empty { 159 | helpmsg.push('\n'); 160 | } 161 | // Help info for flags. 162 | if !self.flags.is_empty() { 163 | helpmsg.push_str("FLAGS:\n"); 164 | } 165 | for flag in &self.flags { 166 | helpmsg.push_str(" "); 167 | if let Some(short) = &flag.short { 168 | helpmsg.push_str(short); 169 | helpmsg.push_str(", "); 170 | } 171 | helpmsg.push_str(&flag.arg_name); 172 | if let Some(help) = &flag.help { 173 | helpmsg.push('\t'); 174 | helpmsg.push_str(help); 175 | } 176 | helpmsg.push('\n'); 177 | } 178 | } 179 | /// 180 | /// 181 | pub fn into_ctor(self, iter: &Ident, help_ident: &Ident) -> TokenStream2 { 182 | let arg_ty = crate_path!(Argument); 183 | let opt_ty = crate_path!(OptionalArg); 184 | let parse_ty = crate_path!(Parse); 185 | let help_ty = crate_path!(HelpInfo); 186 | let err_ty = crate_path!(Error); 187 | let argref_ty = crate_path!(ArgRef); 188 | 189 | let Self { 190 | cmd_ident, 191 | pos_args, 192 | named_args, 193 | flags, 194 | } = self; 195 | let mut declarations = quote! { 196 | let mut #iter = #iter.peekable(); 197 | }; 198 | // Code snippet to consume named arguments and flags. 199 | let consume_flags = { 200 | let mut match_args = quote! {}; 201 | for Arg { 202 | arg_name, 203 | short, 204 | l_ident, 205 | .. 206 | } in &named_args 207 | { 208 | declarations = quote! { 209 | #declarations 210 | let mut #l_ident: Option = None; 211 | }; 212 | let mut pattern = quote! { Some(#arg_name) }; 213 | if let Some(short) = short { 214 | pattern = quote! { #pattern | Some(#short) }; 215 | } 216 | match_args = quote! { 217 | #match_args 218 | #pattern => #l_ident = Some(#iter.next().ok_or(#err_ty::ExpectedValue(#arg_name))?) , 219 | } 220 | } 221 | let mut match_flags = quote! {}; 222 | let flag_ty = crate_path!(Flag); 223 | for Arg { 224 | arg_name: flag, 225 | short, 226 | l_ident, 227 | ty, 228 | .. 229 | } in flags.iter() 230 | { 231 | declarations = quote! { 232 | #declarations 233 | let mut #l_ident = <#ty>::default(); 234 | }; 235 | let mut pattern = quote! { Some(#flag) }; 236 | if let Some(short) = short { 237 | pattern = quote! { #pattern | Some(#short) }; 238 | } 239 | match_flags = quote! { 240 | #match_flags 241 | #pattern => #flag_ty::increment(&mut #l_ident) , 242 | }; 243 | } 244 | 245 | let match_ = quote! { 246 | match #iter.next().as_deref() { 247 | #match_args 248 | #match_flags 249 | Some("--help") | Some("-h") => return Ok(#parse_ty::Help(#help_ty(#help_ident))) , 250 | Some(fl) => return Err(#err_ty::UnknownFlag(fl.to_string())), 251 | _ => panic!("This shouldn't happen."), 252 | } 253 | }; 254 | quote! { 255 | while #iter.peek().map_or(false, |a| a.starts_with('-')) { 256 | #match_ 257 | } 258 | } 259 | }; 260 | // 261 | // Display the help message if called with no arguments. 262 | // If all of the arguments are optional, don't do this. 263 | let help_on_blank = if pos_args.iter().any(|a| a.required && !a.variadic) { 264 | quote! { 265 | if #iter.peek().is_none() { 266 | return Ok(#parse_ty::Help(#help_ty(#help_ident))); 267 | } 268 | } 269 | } else { 270 | quote! {} 271 | }; 272 | // 273 | // Code to consume positional arguments. 274 | let mut pos = quote! {}; 275 | for (i, arg) in pos_args.iter().enumerate() { 276 | let &Arg { 277 | ref l_ident, 278 | required, 279 | variadic, 280 | .. 281 | } = arg; 282 | let i = i + 1; 283 | // Variadic arguments. 284 | if variadic { 285 | declarations = quote! { 286 | #declarations 287 | let mut #l_ident = Vec::::new(); 288 | }; 289 | pos = quote! { 290 | #pos 291 | while let Some(arg) = #iter.next() { 292 | #l_ident.push(arg); 293 | #consume_flags 294 | } 295 | }; 296 | } 297 | // Required arguments. 298 | else if required { 299 | declarations = quote! { 300 | #declarations 301 | let #l_ident : String; 302 | }; 303 | pos = quote! { 304 | #pos 305 | #l_ident = #iter.next().ok_or(#err_ty::ExpectedPositional(#i))?; 306 | #consume_flags 307 | }; 308 | } 309 | // Optional arguments. 310 | else { 311 | declarations = quote! { 312 | #declarations 313 | let mut #l_ident: Option = None; 314 | }; 315 | pos = quote! { 316 | #pos 317 | if let Some(next) = #iter.next() { 318 | #l_ident = Some(next); 319 | #consume_flags 320 | } 321 | }; 322 | } 323 | } 324 | 325 | // Code to put the arguments in the constructor. 326 | let ctor = { 327 | let mut ctor = quote! {}; 328 | for (i, arg) in pos_args.into_iter().enumerate() { 329 | let Arg { 330 | ident, 331 | l_ident, 332 | required, 333 | variadic, 334 | .. 335 | } = arg; 336 | let i = i + 1; 337 | // Collect args if variadic. 338 | ctor = if variadic { 339 | quote! { 340 | #ctor 341 | #ident : #l_ident.iter() 342 | .enumerate() 343 | .map(|(i, val)| #arg_ty::parse(val, #argref_ty::Positional(#i + i))) 344 | .collect::>()? , 345 | } 346 | } 347 | // Handle errors if required. 348 | else if required { 349 | quote! { 350 | #ctor 351 | #ident : #arg_ty::parse(#l_ident, #argref_ty::Positional(#i))? , 352 | } 353 | } 354 | // Allow defaults if optional. 355 | else { 356 | quote! { 357 | #ctor 358 | #ident: #opt_ty::map_parse(#l_ident, #argref_ty::Positional(#i))? , 359 | } 360 | } 361 | } 362 | for Arg { 363 | arg_name, 364 | ident, 365 | l_ident, 366 | required, 367 | .. 368 | } in named_args 369 | { 370 | let argref = quote! { #argref_ty::Named(#arg_name) }; 371 | // Error handling if it's required. 372 | ctor = if required { 373 | quote! { 374 | #ctor 375 | #ident: #arg_ty::parse(#l_ident.ok_or(#err_ty::ExpectedNamed(#arg_name))?, #argref)? , 376 | } 377 | } 378 | // Defaults if it's optional 379 | else { 380 | quote! { 381 | #ctor 382 | #ident: #opt_ty::map_parse(#l_ident, #argref)? , 383 | } 384 | } 385 | } 386 | for Arg { ident, l_ident, .. } in flags { 387 | ctor = quote! { 388 | #ctor 389 | #ident: #l_ident , 390 | } 391 | } 392 | 393 | quote! { 394 | #cmd_ident { #ctor } 395 | } 396 | }; 397 | 398 | quote! {{ 399 | #declarations 400 | #help_on_blank 401 | #consume_flags 402 | #pos 403 | let val = #ctor; 404 | // Return an error if there's an extra argument at the end. 405 | if let Some(a) = #iter.next() { 406 | return Err(#err_ty::ExtraArg(a)); 407 | } 408 | val 409 | }} 410 | } 411 | } 412 | --------------------------------------------------------------------------------