cmd_lib_macros/
lib.rs

1use proc_macro2::{TokenStream, TokenTree};
2use proc_macro_error2::{abort, proc_macro_error};
3use quote::quote;
4
5/// Mark main function to log error result by default.
6///
7/// ```no_run
8/// # use cmd_lib::*;
9///
10/// #[cmd_lib::main]
11/// fn main() -> CmdResult {
12///     run_cmd!(bad_cmd)?;
13///     Ok(())
14/// }
15/// // output:
16/// // [ERROR] FATAL: Running ["bad_cmd"] failed: No such file or directory (os error 2)
17/// ```
18#[proc_macro_attribute]
19pub fn main(
20    _args: proc_macro::TokenStream,
21    item: proc_macro::TokenStream,
22) -> proc_macro::TokenStream {
23    let orig_function: syn::ItemFn = syn::parse2(item.into()).unwrap();
24    let orig_main_return_type = orig_function.sig.output;
25    let orig_main_block = orig_function.block;
26
27    quote! (
28        fn main() {
29            fn cmd_lib_main() #orig_main_return_type {
30                #orig_main_block
31            }
32
33            cmd_lib_main().unwrap_or_else(|err| {
34                ::cmd_lib::error!("FATAL: {err}");
35                std::process::exit(1);
36            });
37        }
38
39    )
40    .into()
41}
42
43/// Import user registered custom command.
44/// ```no_run
45/// # use cmd_lib::*;
46/// # use std::io::Write;
47/// fn my_cmd(env: &mut CmdEnv) -> CmdResult {
48///     let msg = format!("msg from foo(), args: {:?}", env.get_args());
49///     writeln!(env.stderr(), "{msg}")?;
50///     writeln!(env.stdout(), "bar")
51/// }
52///
53/// use_custom_cmd!(my_cmd);
54/// run_cmd!(my_cmd)?;
55/// # Ok::<(), std::io::Error>(())
56/// ```
57/// Here we import the previous defined `my_cmd` command, so we can run it like a normal command.
58#[proc_macro]
59#[proc_macro_error]
60pub fn use_custom_cmd(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
61    let item: proc_macro2::TokenStream = item.into();
62    let mut cmd_fns = vec![];
63    for t in item {
64        if let TokenTree::Punct(ref ch) = t {
65            if ch.as_char() != ',' {
66                abort!(t, "only comma is allowed");
67            }
68        } else if let TokenTree::Ident(cmd) = t {
69            let cmd_name = cmd.to_string();
70            cmd_fns.push(quote!(&#cmd_name, #cmd));
71        } else {
72            abort!(t, "expect a list of comma separated commands");
73        }
74    }
75
76    quote! (
77        #(::cmd_lib::register_cmd(#cmd_fns);)*
78    )
79    .into()
80}
81
82/// Run commands, returning [`CmdResult`](../cmd_lib/type.CmdResult.html) to check status.
83/// ```no_run
84/// # use cmd_lib::run_cmd;
85/// let msg = "I love rust";
86/// run_cmd!(echo $msg)?;
87/// run_cmd!(echo "This is the message: $msg")?;
88///
89/// // pipe commands are also supported
90/// run_cmd!(du -ah . | sort -hr | head -n 10)?;
91///
92/// // or a group of commands
93/// // if any command fails, just return Err(...)
94/// let file = "/tmp/f";
95/// let keyword = "rust";
96/// if run_cmd! {
97///     cat ${file} | grep ${keyword};
98///     echo "bad cmd" >&2;
99///     ignore ls /nofile;
100///     date;
101///     ls oops;
102///     cat oops;
103/// }.is_err() {
104///     // your error handling code
105/// }
106/// # Ok::<(), std::io::Error>(())
107/// ```
108#[proc_macro]
109#[proc_macro_error]
110pub fn run_cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
111    let cmds = lexer::Lexer::new(input.into()).scan().parse(false);
112    quote! ({
113        use ::cmd_lib::AsOsStr;
114        #cmds.run_cmd()
115    })
116    .into()
117}
118
119/// Run commands, returning [`FunResult`](../cmd_lib/type.FunResult.html) to capture output and to check status.
120/// ```no_run
121/// # use cmd_lib::run_fun;
122/// let version = run_fun!(rustc --version)?;
123/// println!("Your rust version is {}", version);
124///
125/// // with pipes
126/// let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?;
127/// println!("There are {} words in above sentence", n);
128/// # Ok::<(), std::io::Error>(())
129/// ```
130#[proc_macro]
131#[proc_macro_error]
132pub fn run_fun(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
133    let cmds = lexer::Lexer::new(input.into()).scan().parse(false);
134    quote! ({
135        use ::cmd_lib::AsOsStr;
136        #cmds.run_fun()
137    })
138    .into()
139}
140
141/// Run commands with/without pipes as a child process, returning [`CmdChildren`](../cmd_lib/struct.CmdChildren.html) result.
142/// ```no_run
143/// # use cmd_lib::*;
144///
145/// let mut handle = spawn!(ping -c 10 192.168.0.1)?;
146/// // ...
147/// if handle.wait().is_err() {
148///     // ...
149/// }
150/// # Ok::<(), std::io::Error>(())
151#[proc_macro]
152#[proc_macro_error]
153pub fn spawn(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
154    let cmds = lexer::Lexer::new(input.into()).scan().parse(true);
155    quote! ({
156        use ::cmd_lib::AsOsStr;
157        #cmds.spawn(false)
158    })
159    .into()
160}
161
162/// Run commands with/without pipes as a child process, returning [`FunChildren`](../cmd_lib/struct.FunChildren.html) result.
163/// ```no_run
164/// # use cmd_lib::*;
165/// let mut procs = vec![];
166/// for _ in 0..4 {
167///     let proc = spawn_with_output!(
168///         sudo bash -c "dd if=/dev/nvmen0 of=/dev/null bs=4096 skip=0 count=1024 2>&1"
169///         | awk r#"/copied/{print $(NF-1) " " $NF}"#
170///     )?;
171///     procs.push(proc);
172/// }
173///
174/// for (i, mut proc) in procs.into_iter().enumerate() {
175///     let bandwidth = proc.wait_with_output()?;
176///     info!("thread {i} bandwidth: {bandwidth} MB/s");
177/// }
178/// # Ok::<(), std::io::Error>(())
179/// ```
180#[proc_macro]
181#[proc_macro_error]
182pub fn spawn_with_output(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
183    let cmds = lexer::Lexer::new(input.into()).scan().parse(true);
184    quote! ({
185        use ::cmd_lib::AsOsStr;
186        #cmds.spawn_with_output()
187    })
188    .into()
189}
190
191#[proc_macro]
192#[proc_macro_error]
193/// Log a fatal message at the error level, and exit process.
194///
195/// e.g:
196/// ```no_run
197/// # use cmd_lib::*;
198/// let file = "bad_file";
199/// cmd_die!("could not open file: $file");
200/// // output:
201/// // [ERROR] FATAL: could not open file: bad_file
202/// ```
203/// format should be string literals, and variable interpolation is supported.
204/// Note that this macro is just for convenience. The process will exit with 1 and print
205/// "FATAL: ..." messages to error console. If you want to exit with other code, you
206/// should probably define your own macro or functions.
207pub fn cmd_die(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
208    let msg = parse_msg(input.into());
209    quote!({
210        ::cmd_lib::error!("FATAL: {} at {}:{}", #msg, file!(), line!());
211        std::process::exit(1)
212    })
213    .into()
214}
215
216fn parse_msg(input: TokenStream) -> TokenStream {
217    let mut iter = input.into_iter();
218    let mut output = TokenStream::new();
219    let mut valid = false;
220    if let Some(ref tt) = iter.next() {
221        if let TokenTree::Literal(lit) = tt {
222            let s = lit.to_string();
223            if s.starts_with('\"') || s.starts_with('r') {
224                let str_lit = lexer::scan_str_lit(lit);
225                output.extend(quote!(#str_lit));
226                valid = true;
227            }
228        }
229        if !valid {
230            abort!(tt, "invalid format: expect string literal");
231        }
232        if let Some(tt) = iter.next() {
233            abort!(
234                tt,
235                "expect string literal only, found extra {}",
236                tt.to_string()
237            );
238        }
239    }
240    output
241}
242
243mod lexer;
244mod parser;