commander_macros/
lib.rs

1#![recursion_limit = "256"]
2
3mod tokens;
4mod errors;
5mod tools;
6
7extern crate proc_macro;
8
9use proc_macro::{ TokenStream };
10use proc_macro2::{ TokenStream as TokenStream2, Span as Span2 };
11use std::collections::HashMap;
12
13use lazy_static::lazy_static;
14use quote::quote;
15use syn::{ Ident, ItemFn, parse_macro_input };
16use tokens::{ CommandToken, OptionsToken};
17
18use crate::errors::{DON_NOT_MATCH, ENTRY_ONLY_MAIN, NO_SUB_CMD_NAMES_MAIN, OPT_DUPLICATE_DEFINITION, compile_error_info, DIRECT_ONLY_ONCE};
19use crate::tools::generate_call_fn;
20use crate::tokens::{PureArguments, check_arguments};
21use syn::spanned::Spanned;
22
23macro_rules! prefix {
24    ($($i: tt),*) => {
25        {
26            let mut prefix_str = String::new();
27
28            prefix_str.push_str("_commander_rust");
29            $(
30                prefix_str.push_str(&format!("_{}", $i));
31            )*
32            prefix_str
33        }
34    };
35
36    ($e: expr) => {
37        {
38            format!("_commander_rust_{}", $e)
39        }
40    }
41}
42
43macro_rules! import {
44    ($o: ident as $r: ident) => {
45        quote! {
46            use commander_rust::{ $o as $r };
47        }
48    };
49    ($o: ident as $r: ident from $f: path) => {
50        quote! {
51            use $f::{ $o as $r };
52        }
53    }
54}
55
56lazy_static! {
57    static ref COMMAND_OPTIONS: std::sync::Mutex<HashMap<String, Vec<String>>> = std::sync::Mutex::new(HashMap::new());
58    static ref OPTIONS: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(vec![]);
59    static ref GET_FN_NAMES: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(vec![]);
60    static ref DIRECT_NAME: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
61}
62
63
64/// Define command.
65///
66/// # Format
67///
68/// `#[command(sub_cmd $(<rs>|[os]|<rm...>|[om...]),* , desc)]`, explain:
69/// - `sub_name`: the name of sub-command you defined, it should be same as the name of command processing function
70/// - `$(<rs>|[os]|<rm...>|[om...]),*`: arguments list, divided by comma. Only four types of arguments: <RequiredSingle>,[OptionalSingle],<RequiredMultiple>,[OptionalMultiple]
71/// - `desc`: description of this sub-command, it's using for display help information.
72///
73/// # Note
74///
75/// `#[command]` should be placed after all `#[option]`
76#[proc_macro_attribute]
77pub fn command(cmd: TokenStream, method: TokenStream) -> TokenStream {
78    let method: ItemFn = parse_macro_input!(method as ItemFn);
79    let ItemFn {
80        ident,
81        decl,
82        ..
83    } = &method;
84    let name = format!("{}", ident);
85    let get_fn = Ident::new(&prefix!(name), ident.span());
86    let cmd_token = Ident::new(&prefix!("Command"), ident.span());
87    let opts = COMMAND_OPTIONS.lock().unwrap();
88    let mut get_fn_names = GET_FN_NAMES.lock().unwrap();
89    let mut get_fns = vec![];
90
91    // clear all options in OPTIONS
92    OPTIONS.lock().unwrap().clear();
93
94    if let Some(v) = opts.get(&name) {
95        for i in v {
96            get_fns.push(Ident::new(&prefix!(name, i), ident.span()));
97        }
98    }
99
100    if !get_fn_names.contains(&name) {
101        get_fn_names.push(name.clone());
102    }
103
104    let command: CommandToken = parse_macro_input!(cmd as CommandToken);
105    // generating call function, because we can't call unstable (uncertain quantity parameters) function
106    let call_fn_name = Ident::new(&prefix!(name, "call"), ident.span());
107    let call_fn = generate_call_fn(&decl.inputs, &call_fn_name, &ident);
108    let mut error_info = check_arguments(&command.args);
109
110    if format!("{}", command.name) != "main" && format!("{}", command.name) != name {
111        error_info = compile_error_info(command.name.span(), DON_NOT_MATCH);
112    }
113
114    if name == "main" {
115        error_info = compile_error_info(ident.span(), NO_SUB_CMD_NAMES_MAIN)
116    }
117
118    TokenStream::from(quote! {
119        #error_info
120
121        fn #get_fn() -> #cmd_token {
122            let mut command = #command;
123            let mut fns = CALL_FNS.lock().unwrap();
124
125            if !fns.contains_key(#name) {
126                fns.insert(String::from(#name), #call_fn_name);
127            }
128
129            command.opts = {
130                let mut v = vec![];
131                #(
132                    v.push(#get_fns());
133                )*
134                v
135            };
136            command
137        }
138
139        #call_fn
140
141        #method
142    })
143}
144
145
146/// Define option of command or public.
147///
148/// # Format
149///
150/// it's similar with `command`. The only difference between them is the length of arguments.
151///
152/// `#[command(sub_cmd <rs>|[os]|<rm...>|[om...] , desc)]`, explain:
153/// - `sub_name`: the name of sub-command you defined, it should be same as the name of command processing function
154/// - `<rs>|[os]|<rm...>|[om...]`: only one. Only four types of arguments: <RequiredSingle>,[OptionalSingle],<RequiredMultiple>,[OptionalMultiple]
155/// - `desc`: description of this sub-command, it's using for display help information.
156///
157/// # Note
158///
159/// all `#[option]` should be placed before `#[command]`
160///
161#[proc_macro_attribute]
162pub fn option(opt: TokenStream, method: TokenStream) -> TokenStream {
163    let option: OptionsToken = parse_macro_input!(opt as OptionsToken);
164    let method: ItemFn = parse_macro_input!(method as ItemFn);
165    let ItemFn {
166        ident,
167        ..
168    } = &method;
169    let name = format!("{}", ident);
170    let opt_name = format!("{}", option.long);
171    let fn_name = prefix!(name, opt_name);
172    let get_fn = Ident::new(&fn_name, option.long.span());
173    let opt_token = Ident::new(&prefix!("Options"), ident.span());
174    let mut opts = COMMAND_OPTIONS.lock().unwrap();
175    let mut error_info = TokenStream2::new();
176    let mut all_opts = OPTIONS.lock().unwrap();
177
178
179    // check if options are duplicate definition
180    if all_opts.contains(&format!("{}", option.short)) {
181        error_info = compile_error_info(option.short.span(), OPT_DUPLICATE_DEFINITION);
182    } else if all_opts.contains(&format!("{}", option.long)) {
183        error_info = compile_error_info(option.long.span(), OPT_DUPLICATE_DEFINITION);
184    } else {
185        all_opts.push(format!("{}", option.short));
186        all_opts.push(format!("{}", option.long));
187
188        if opts.contains_key(&name) {
189            if let Some(v) = opts.get_mut(&name) {
190                v.push(opt_name);
191            }
192        } else {
193            opts.insert(name, vec![opt_name]);
194        }
195    }
196
197    if error_info.is_empty() {
198        TokenStream::from(quote! {
199            #error_info
200
201            fn #get_fn() -> #opt_token {
202                #option
203            }
204
205            #method
206        })
207    } else {
208        TokenStream::from(quote! {
209            #error_info
210
211            #method
212        })
213    }
214
215}
216
217/// Define direct function of CLI
218///
219/// It allows users that don't use sub-command. It can be used directly.
220///
221/// # example
222///
223/// ```ignore
224/// #[direct(<a> <b> [c] [d])]
225///  fn direct(a: String, b: String) {
226///      println!("hello! {} {}", a, b);
227///  }
228/// ```
229/// When you input `[pkg-name] 1 2 3`, cli will print "hello! 1 2".
230/// So it allows you that don't need to define a sub-command anymore in some simple situations.
231///
232/// # Format
233///
234/// Only accept arguments, such as `#[direct(<a> <b> <c>)]`.
235
236#[proc_macro_attribute]
237pub fn direct(pure_args: TokenStream, func: TokenStream) -> TokenStream {
238    let func: ItemFn = parse_macro_input!(func as ItemFn);
239    let ItemFn {
240        ident,
241        decl,
242        ..
243    } = &func;
244    let name = format!("{}", ident);
245    let pure_args: PureArguments = parse_macro_input!(pure_args as PureArguments);
246    let direct_fn: &mut Option<String> = &mut (*DIRECT_NAME.lock().unwrap());
247    let direct_get_fn = Ident::new(&prefix!(name), ident.span());
248    let argument_ident = Ident::new(&prefix!("Argument"), ident.span());
249    let call_fn_name = Ident::new(&prefix!(name, "call"), ident.span());
250    let call_fn = generate_call_fn(&decl.inputs, &call_fn_name, &ident);
251    let mut error_info: TokenStream2 = check_arguments(&pure_args.0);
252
253
254    if let Some(_) = direct_fn {
255        error_info = compile_error_info(pure_args.span(), DIRECT_ONLY_ONCE);
256    } else {
257        *direct_fn = Some(format!("{}", ident));
258    }
259
260    TokenStream::from(quote! {
261        #error_info
262
263        #func
264
265        fn #direct_get_fn() -> Vec<#argument_ident> {
266            let direct_fn: &mut Option<fn(raws: &Vec<_commander_rust_Raw>, app: _commander_rust_Cli)> = &mut (*DIRECT_FN.lock().unwrap());
267
268            *direct_fn.borrow_mut() = Some(#call_fn_name);
269            #pure_args
270        }
271
272        #call_fn
273    })
274}
275
276/// Define entry of CLI.
277///
278/// Only using for `main` function. No parameter needed.
279///
280/// # Note
281///
282/// If you want to define public options, put all options before `#[entry]`.
283/// In other word, you can regard `#[entry]` as `#[command]` without parameters.
284///
285#[proc_macro_attribute]
286pub fn entry(pure_arguments: TokenStream, main: TokenStream) -> TokenStream {
287    let pure_args: PureArguments = parse_macro_input!(pure_arguments as PureArguments);
288    let main: ItemFn = parse_macro_input!(main as ItemFn);
289    let ItemFn {
290        ident,
291        ..
292    } = &main;
293    let target = format!("{}", ident);
294    let opts = COMMAND_OPTIONS.lock().unwrap();
295    let imports = vec![
296        import!(Argument as _commander_rust_Argument),
297        import!(ArgumentType as _commander_rust_ArgumentType),
298        import!(Command as _commander_rust_Command),
299        import!(Options as _commander_rust_Options),
300        import!(Raw as _commander_rust_Raw),
301        import!(normalize as _commander_rust_normalize),
302        import!(Instance as _commander_rust_Instance),
303        import!(ls as _commander_rust_ls),
304        import!(Application as _commander_rust_Application),
305        import!(Cli as _commander_rust_Cli),
306    ];
307    let get_fn = Ident::new(&prefix!("main"), ident.span());
308    let app_token = Ident::new(&prefix!("Application"), ident.span());
309    let get_fn_names = GET_FN_NAMES.lock().unwrap();
310    let direct_fn = &(*DIRECT_NAME.lock().unwrap());
311    let mut get_cmds_fns = vec![];
312    let mut get_opts_fns = vec![];
313    let mut error_info = check_arguments(&pure_args.0);
314
315    // init can be used with fn main only
316    if target != String::from("main") {
317        error_info = compile_error_info(ident.span(), ENTRY_ONLY_MAIN);
318    }
319
320    if let Some(v) = opts.get("main") {
321        for i in v {
322            get_opts_fns.push(Ident::new(&prefix!("main", i), ident.span()));
323        }
324    }
325
326    for i in get_fn_names.iter() {
327        get_cmds_fns.push(Ident::new(&prefix!(i), ident.span()));
328    }
329
330    let needed = quote! {
331        #error_info
332        mod _commander_rust_Inner {
333            use crate::_commander_rust_ls;
334            use crate::_commander_rust_Raw;
335            use crate::_commander_rust_Cli;
336
337            type Raw = _commander_rust_Raw;
338            type Map = std::collections::HashMap<String, fn(raws: &Vec<Raw>, app: _commander_rust_Cli)>;
339            type Mutex = std::sync::Mutex<Map>;
340
341            _commander_rust_ls! {
342               pub static ref CALL_FNS: Mutex = Mutex::new(Map::new());
343               pub static ref DIRECT_FN: std::sync::Mutex<Option<fn(raws: &Vec<Raw>, app: _commander_rust_Cli)>> = std::sync::Mutex::new(None);
344            }
345
346            pub const APP_NAME: &'static str = env!("CARGO_PKG_NAME");
347            pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
348            pub const DESCRIPTION: &'static str = env!("CARGO_PKG_DESCRIPTION");
349        }
350
351        use _commander_rust_Inner::{ CALL_FNS, DIRECT_FN, VERSION, DESCRIPTION, APP_NAME };
352        #(#imports)*
353
354        #main
355    };
356
357    // inject direct-functions' arguments or not
358    if let Some(df) = direct_fn {
359        let direct_get_fn = Ident::new(&prefix!(df), Span2::call_site());
360
361        TokenStream::from(quote! {
362            #needed
363
364            fn #get_fn() -> #app_token {
365                let mut application = #app_token {
366                    name: String::from(APP_NAME),
367                    desc: String::from(DESCRIPTION),
368                    opts: vec![],
369                    cmds: vec![],
370                    direct_args: vec![],
371                };
372
373                application.opts = {
374                    let mut v = vec![];
375                    #(
376                        v.push(#get_opts_fns());
377                    )*
378                    v
379                };
380                application.cmds = {
381                    let mut v = vec![];
382                    #(
383                        v.push(#get_cmds_fns());
384                    )*
385                    v
386                };
387                // inject direct-fns
388                application.direct_args = #direct_get_fn();
389
390                application
391            }
392        })
393    } else {
394        TokenStream::from(quote!{
395            #needed
396
397            fn #get_fn() -> #app_token {
398                let mut application = #app_token {
399                    name: String::from(APP_NAME),
400                    desc: String::from(DESCRIPTION),
401                    opts: vec![],
402                    cmds: vec![],
403                    direct_args: vec![],
404                };
405
406                application.opts = {
407                    let mut v = vec![];
408                    #(
409                        v.push(#get_opts_fns());
410                    )*
411                    v
412                };
413
414                application.cmds = {
415                    let mut v = vec![];
416                    #(
417                        v.push(#get_cmds_fns());
418                    )*
419                    v
420                };
421                application
422            }
423        })
424    }
425}
426
427/// Run cli now.
428///
429/// `run!()` instead of `run()`. You can use this macro to get application of cli.
430/// See `Application` for more details.
431///
432#[proc_macro]
433pub fn run(_: TokenStream) -> TokenStream {
434    TokenStream::from(quote! {
435        {
436            // _commander_rust_main is generated by `entry`
437            //
438            let mut app = _commander_rust_main();
439            let ins;
440
441            app.derive();
442            ins = _commander_rust_normalize(std::env::args().into_iter().collect::<Vec<String>>(), &app);
443
444            let cli = _commander_rust_Cli::from(&ins, &app);
445            let fns = CALL_FNS.lock().unwrap();
446
447            if let Some(cli) = cli {
448                if cli.has("help") || cli.has("h") {
449                    // display sub-command usage
450                    if cli.cmd.is_some() {
451                        for cmd in &app.cmds {
452                            if cmd.name == cli.get_name() {
453                                println!("{:#?}", cmd);
454                                break;
455                            }
456                        }
457                    } else {
458                        // display cli usage
459                        println!("{:#?}", app);
460                    }
461                } else if cli.has("version") || cli.has("V") {
462                    println!("version: {}", VERSION);
463                } else {
464                    if let Some(callback) = fns.get(&cli.get_name()) {
465                        callback(&cli.get_raws(), cli);
466                    } else if !cli.direct_args.is_empty() {
467                        let df = *DIRECT_FN.lock().unwrap();
468
469                        if let Some(f) = &df {
470                            f(&cli.direct_args.clone(), cli)
471                        } else {
472                            println!("ERRRRR");
473                        }
474                    } else {
475                        eprintln!("Unknown usage. Using `{} --help` for more help information.\n", APP_NAME);
476                    }
477                }
478            } else {
479                println!("Using `{} --help` for more help information.", APP_NAME);
480            }
481
482            app
483        }
484    })
485}