aopt_macro/
lib.rs

1extern crate syn;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::ext::IdentExt;
6use syn::parse::Parse;
7use syn::parse::ParseStream;
8use syn::punctuated::Punctuated;
9use syn::token::Comma;
10use syn::{parse_macro_input, Expr};
11
12/// Parsing arguments using for `getopt!` or `getoptd!`.
13///
14/// Arguments should be like: `$iter`(getopt input arguments), `$($parser),+`(one or more parser)
15#[derive(Debug)]
16struct GetoptArgs {
17    iter: Expr,
18    parsers: Punctuated<Expr, Comma>,
19}
20
21impl Parse for GetoptArgs {
22    fn parse(input: ParseStream) -> syn::Result<Self> {
23        let mut parsers = Punctuated::new();
24        let iter: Expr = input.parse()?;
25        let _: Comma = input.parse()?;
26        let first: Expr = input.parse()?;
27
28        parsers.push(first);
29        while input.peek(Comma) {
30            parsers.push_punct(input.parse()?);
31            if input.is_empty() {
32                break;
33            } else {
34                parsers.push(input.parse()?);
35            }
36        }
37
38        Ok(Self { iter, parsers })
39    }
40}
41
42#[proc_macro]
43pub fn getoptd(input: TokenStream) -> TokenStream {
44    let getopt_args = parse_macro_input!(input as GetoptArgs);
45    let iterator = &getopt_args.iter;
46    let iterator = quote! { #iterator };
47
48    let mut getopt_init = quote! {
49        let mut parsers = vec![];
50    };
51
52    getopt_init.extend(getopt_args.parsers.iter().map(|p| match p {
53        Expr::Path(path) => {
54            quote! {
55                parsers.push(&mut #path);
56            }
57        }
58        Expr::Reference(reference) => {
59            if reference.mutability.is_some() {
60                quote! {
61                    quote! {
62                        parsers.push(#reference);
63                    }
64                }
65            } else {
66                syn::Error::new_spanned(reference, "need an instance or a mutable reference")
67                    .to_compile_error()
68            }
69        }
70        expr => {
71            quote! {
72                parsers.push(#expr);
73            }
74        }
75    }));
76
77    let ret = quote! {{
78        #getopt_init
79        getopt_dynparser(#iterator, parsers)
80    }};
81    ret.into()
82}
83
84#[proc_macro]
85pub fn getopt(input: TokenStream) -> TokenStream {
86    let getopt_args = parse_macro_input!(input as GetoptArgs);
87    let iterator = &getopt_args.iter;
88    let iterator = quote! { #iterator };
89
90    let mut getopt_init = quote! {
91        let mut parsers = vec![];
92    };
93
94    getopt_init.extend(getopt_args.parsers.iter().map(|p| match p {
95        Expr::Path(path) => {
96            quote! {
97                parsers.push(&mut #path);
98            }
99        }
100        Expr::Reference(reference) => {
101            if reference.mutability.is_some() {
102                quote! {
103                    parsers.push(#reference);
104                }
105            } else {
106                syn::Error::new_spanned(reference, "need an instance or a mutable reference")
107                    .to_compile_error()
108            }
109        }
110        expr => {
111            quote! {
112                parsers.push(#expr);
113            }
114        }
115    }));
116
117    let ret = quote! {{
118        #getopt_init
119        getopt_parser(#iterator, parsers)
120    }};
121    ret.into()
122}
123
124/// Parsing arguments using for `getopt_help!`.
125///
126/// Arguments should be like: `$set`(option set), `$($cmd_name),*`(zero or more cmd name)
127#[derive(Debug)]
128struct HelpArgs {
129    set: Expr,
130    cmds: Punctuated<Expr, Comma>,
131}
132
133impl Parse for HelpArgs {
134    fn parse(input: ParseStream) -> syn::Result<Self> {
135        let mut cmds = Punctuated::new();
136        let set: Expr = input.parse()?;
137
138        if input.peek(Comma) && input.parse::<Comma>().is_ok() {
139            while !input.is_empty() {
140                cmds.push(input.parse()?);
141                if input.peek(Comma) {
142                    cmds.push_punct(input.parse()?);
143                } else {
144                    break;
145                }
146            }
147        }
148
149        Ok(Self { set, cmds })
150    }
151}
152
153#[proc_macro]
154pub fn getopt_help(input: TokenStream) -> TokenStream {
155    let help_args = parse_macro_input!(input as HelpArgs);
156    let set = help_args.set;
157    let cmds = help_args.cmds;
158
159    let mut help_code = quote! {};
160
161    if cmds.is_empty() {
162        help_code.extend(quote! {{
163            let mut help = AppHelp::<std::io::Stdout, DefaultFormat>::default();
164            help.set_name(gstr(env!("CARGO_PKG_NAME")));
165            let global = help.store.get_global_mut();
166            for opt in #set.opt_iter() {
167                if opt.match_style(aopt::opt::Style::Pos) {
168                    global.add_pos(PosStore::new(
169                        opt.get_name(),
170                        opt.get_hint(),
171                        opt.get_help(),
172                        opt.get_index().unwrap().to_string().into(),
173                        opt.get_optional(),
174                    ));
175                } else if opt.match_style(aopt::opt::Style::Argument)
176                    || opt.match_style(aopt::opt::Style::Boolean)
177                    || opt.match_style(aopt::opt::Style::Multiple)
178                {
179                    global.add_opt(OptStore::new(
180                        opt.get_name(),
181                        opt.get_hint(),
182                        opt.get_help(),
183                        opt.get_type_name(),
184                        opt.get_optional(),
185                    ));
186                }
187            }
188            global.set_header(gstr(env!("CARGO_PKG_DESCRIPTION")));
189            global.set_footer(gstr(&format!(
190                "Create by {} v{}",
191                env!("CARGO_PKG_AUTHORS"),
192                env!("CARGO_PKG_VERSION")
193            )));
194            help
195        }});
196    } else {
197        help_code.extend(quote! {
198            let mut cmds_arg: Vec<&str> = vec![];
199        });
200        help_code.extend(cmds.iter().map(|p| {
201            quote! {
202                cmds_arg.push(#p);
203            }
204        }));
205        help_code.extend(quote! {{
206            let mut help = AppHelp::<std::io::Stdout, DefaultFormat>::default();
207            help.set_name(gstr(env!("CARGO_PKG_NAME")));
208            let version = gstr(&format!(
209                "Create by {} v{}",
210                env!("CARGO_PKG_AUTHORS"),
211                env!("CARGO_PKG_VERSION")
212            ));
213            help.store.new_sec(gstr("default")).set_help(gstr("Commands:")).commit();
214            let global = help.store.get_global_mut();
215            global.set_header(gstr(env!("CARGO_PKG_DESCRIPTION")));
216            global.set_footer(version.clone());
217            for cmd_name in cmds_arg {
218                if let Ok(Some(opt)) = #set.find(cmd_name) {
219                    let mut search_cmd = help.store.new_cmd(gstr(cmd_name));
220                    search_cmd
221                        .set_footer(version.clone())
222                        .set_hint(opt.get_hint())
223                        .set_help(opt.get_help());
224                    for opt in #set.opt_iter() {
225                        if opt.match_style(aopt::opt::Style::Pos) {
226                            search_cmd.add_pos(PosStore::new(
227                                opt.get_name(),
228                                opt.get_hint(),
229                                opt.get_help(),
230                                opt.get_index().unwrap().to_string().into(),
231                                opt.get_optional(),
232                            ));
233                        } else if opt.match_style(aopt::opt::Style::Argument)
234                            || opt.match_style(aopt::opt::Style::Boolean)
235                            || opt.match_style(aopt::opt::Style::Multiple)
236                        {
237                            search_cmd.add_opt(OptStore::new(
238                                opt.get_name(),
239                                opt.get_hint(),
240                                opt.get_help(),
241                                opt.get_type_name(),
242                                opt.get_optional(),
243                            ));
244                        }
245                    }
246                    search_cmd.commit();
247                    help.store.attach_cmd(gstr("default"), gstr(cmd_name));
248                }
249            }
250            help
251        }});
252    }
253    let ret = quote! {{ #help_code }};
254
255    ret.into()
256}
257
258#[derive(Debug)]
259struct ParameterOrNormalArgs {
260    name: Option<syn::Ident>,
261    value: Expr,
262}
263
264impl Parse for ParameterOrNormalArgs {
265    fn parse(input: ParseStream) -> syn::Result<Self> {
266        let mut name = None;
267        let value;
268
269        if input.peek2(syn::token::Eq) {
270            if input.peek(syn::Ident::peek_any) {
271                name = Some(input.parse()?);
272                input.parse::<syn::Token![=]>()?;
273                value = input.parse()?;
274            } else if input.peek(syn::Token![default]) {
275                name = Some(syn::Ident::new("default", proc_macro2::Span::call_site()));
276                input.parse::<syn::Token![=]>()?;
277                value = input.parse()?;
278            } else {
279                value = input.parse()?;
280            }
281        } else {
282            value = input.parse()?;
283        }
284        Ok(Self { name, value })
285    }
286}
287
288/// Parsing arguments using for `opt_create!`.
289///
290/// Arguments should be like: `$init`(option create string), `$help?`(help message),
291/// `$callback?`(callback), `$($ident = $value),*`(zero or more setting)
292#[derive(Debug)]
293struct CreateArgs {
294    parser: Expr,
295    init: Expr,
296    parameter_args: Punctuated<ParameterOrNormalArgs, Comma>,
297}
298
299impl Parse for CreateArgs {
300    fn parse(input: ParseStream) -> syn::Result<Self> {
301        let mut parameter_args = Punctuated::new();
302        let parser: Expr = input.parse()?;
303        let _: Comma = input.parse()?;
304        let init: Expr = input.parse()?;
305
306        if input.peek(Comma) && input.parse::<Comma>().is_ok() {
307            while !input.is_empty() {
308                let arg: ParameterOrNormalArgs = input.parse()?;
309
310                parameter_args.push(arg);
311                if input.peek(Comma) {
312                    parameter_args.push_punct(input.parse()?);
313                } else {
314                    break;
315                }
316            }
317        }
318
319        Ok(Self {
320            parser,
321            init,
322            parameter_args,
323        })
324    }
325}
326
327#[proc_macro]
328pub fn getopt_add(input: TokenStream) -> TokenStream {
329    let getopt_args = parse_macro_input!(input as CreateArgs);
330    let init = getopt_args.init;
331    let parser = getopt_args.parser;
332    let mut found_help = false;
333    let mut callback = None;
334    let mut output = quote! {
335        let init_string = #init.into();
336        let mut create_info = CreateInfo::parse(init_string, #parser.get_prefix());
337    };
338
339    output.extend(getopt_args.parameter_args.iter().map(|v| {
340        let name = &v.name;
341        let expr = &v.value;
342
343        if let Some(name) = name {
344            match name.to_string().as_str() {
345                "help" => {
346                    quote! {{
347                        let value = #expr.into();
348                        create_info = create_info.and_then(|mut ci| { ci.set_help(value); Ok(ci) });
349                    }}
350                }
351                "name" => {
352                    quote! {{
353                        let value = #expr.into();
354                        create_info = create_info.and_then(|mut ci| { ci.set_name(value); Ok(ci) });
355                    }}
356                }
357                "prefix" => {
358                    quote! {{
359                        let value = #expr.into();
360                        create_info = create_info.and_then(|mut ci| { ci.set_prefix(value); Ok(ci) });
361                    }}
362                }
363                "index" => {
364                    quote! {{
365                        let value = #expr.into();
366                        create_info = create_info.and_then(|mut ci| { ci.set_index(value); Ok(ci) });
367                    }}
368                }
369                "default" => {
370                    quote! {{
371                        let value = #expr;
372                        create_info = create_info.and_then(|mut ci| { ci.set_default_value(value); Ok(ci) });
373                    }}
374                }
375                "hint" => {
376                    quote! {{
377                        let value = #expr.into();
378                        create_info = create_info.and_then(|mut ci| { ci.set_hint(value); Ok(ci) });
379                    }}
380                }
381                "alias" => {
382                    quote! {{
383                        let value = #expr.into();
384                        create_info = create_info.and_then(|mut ci| { ci.add_alias(value)?; Ok(ci) });
385                    }}
386                }
387                "callback" => {
388                    callback = Some(expr.clone());
389                    quote! {}
390                }
391                _ => syn::Error::new_spanned(name, "Not support option field name").to_compile_error(),
392            }
393        }
394        else if !found_help {
395            found_help = true;
396            quote! {{
397                let value = #expr.into();
398                create_info = create_info.and_then(|mut ci| { ci.set_help(value); Ok(ci) });
399            }}
400        }
401        else if callback.is_none() {
402            callback = Some(expr.clone());
403            quote! { }
404        }
405        else {
406            syn::Error::new_spanned(syn::Ident::new("default", proc_macro2::Span::call_site()), "Not support more than three position arguments").to_compile_error()
407        }
408    }));
409
410    output.extend(quote! {
411        let uid = create_info.and_then(|mut ci| #parser.add_opt_ci(ci));
412    });
413
414    if let Some(callback) = callback {
415        output.extend(quote! {
416            let uid = uid.and_then(|uid| {
417                #parser.add_callback(uid, #callback);
418                Ok(uid)
419            });
420        });
421    }
422
423    let ret = quote! {{
424        #output
425        uid
426    }};
427
428    ret.into()
429}