cmd_lib_macros/
lib.rs

1//! cmd_lib_macros - Procedural macros for cmd_lib
2//!
3//! ## Invalid syntax examples that should fail to compile
4//!
5//! This section contains documentation tests that demonstrate invalid macro syntax
6//! which should result in compilation errors. These serve as tests to ensure
7//! the macros properly reject invalid input.
8//!
9//! ### Invalid variable expansion syntax
10//!
11//! Variable names cannot start with numbers:
12//! ```compile_fail
13//! # use cmd_lib::*;
14//! run_cmd!(echo "${msg0}");
15//! ```
16//!
17//! Invalid spacing in variable expansion:
18//! ```compile_fail
19//! # use cmd_lib::*;
20//! run_fun!(echo "${ msg }");
21//! ```
22//!
23//! Unclosed variable expansion:
24//! ```compile_fail
25//! # use cmd_lib::*;
26//! run_fun!(echo "${");
27//! ```
28//!
29//! Unclosed variable name:
30//! ```compile_fail
31//! # use cmd_lib::*;
32//! run_fun!(echo "${msg");
33//! ```
34//!
35//! Variable names cannot be numbers:
36//! ```compile_fail
37//! # use cmd_lib::*;
38//! run_fun!(echo "${0}");
39//! ```
40//!
41//! Variable names cannot have spaces:
42//! ```compile_fail
43//! # use cmd_lib::*;
44//! run_fun!(echo "${ 0 }");
45//! ```
46//!
47//! Variable names cannot start with numbers:
48//! ```compile_fail
49//! # use cmd_lib::*;
50//! run_fun!(echo "${0msg}");
51//! ```
52//!
53//! Variable names cannot contain spaces:
54//! ```compile_fail
55//! # use cmd_lib::*;
56//! run_fun!(echo "${msg 0}");
57//! ```
58//!
59//! ### Invalid redirect syntax
60//!
61//! Invalid redirect operator spacing:
62//! ```compile_fail
63//! # use cmd_lib::*;
64//! run_cmd!(ls > >&1);
65//! ```
66//!
67//! Invalid redirect to stdout:
68//! ```compile_fail
69//! # use cmd_lib::*;
70//! run_cmd!(ls >>&1);
71//! ```
72//!
73//! Invalid redirect to stderr:
74//! ```compile_fail
75//! # use cmd_lib::*;
76//! run_cmd!(ls >>&2);
77//! ```
78//!
79//! ### Double redirect errors
80//!
81//! Triple redirect operator:
82//! ```compile_fail
83//! # use cmd_lib::*;
84//! run_cmd!(ls / /x &>>> /tmp/f);
85//! ```
86//!
87//! Double redirect with space:
88//! ```compile_fail
89//! # use cmd_lib::*;
90//! run_cmd!(ls / /x &> > /tmp/f);
91//! ```
92//!
93//! Double output redirect:
94//! ```compile_fail
95//! # use cmd_lib::*;
96//! run_cmd!(ls / /x > > /tmp/f);
97//! ```
98//!
99//! Append and output redirect:
100//! ```compile_fail
101//! # use cmd_lib::*;
102//! run_cmd!(ls / /x >> > /tmp/f);
103//! ```
104
105use proc_macro_error2::{abort, proc_macro_error};
106use proc_macro2::{TokenStream, TokenTree};
107use quote::quote;
108
109/// Mark main function to log error result by default.
110///
111/// ```no_run
112/// # use cmd_lib::*;
113///
114/// #[cmd_lib::main]
115/// fn main() -> CmdResult {
116///     run_cmd!(bad_cmd)?;
117///     Ok(())
118/// }
119/// // output:
120/// // [ERROR] FATAL: Running ["bad_cmd"] failed: No such file or directory (os error 2)
121/// ```
122#[proc_macro_attribute]
123pub fn main(
124    _args: proc_macro::TokenStream,
125    item: proc_macro::TokenStream,
126) -> proc_macro::TokenStream {
127    let orig_function: syn::ItemFn = syn::parse2(item.into()).unwrap();
128    let orig_main_return_type = orig_function.sig.output;
129    let orig_main_block = orig_function.block;
130
131    quote! (
132        fn main() {
133            fn cmd_lib_main() #orig_main_return_type {
134                #orig_main_block
135            }
136
137            cmd_lib_main().unwrap_or_else(|err| {
138                ::cmd_lib::error!("FATAL: {err}");
139                std::process::exit(1);
140            });
141        }
142
143    )
144    .into()
145}
146
147/// Import user registered custom command.
148/// ```no_run
149/// # use cmd_lib::*;
150/// # use std::io::Write;
151/// fn my_cmd(env: &mut CmdEnv) -> CmdResult {
152///     let msg = format!("msg from foo(), args: {:?}", env.get_args());
153///     writeln!(env.stderr(), "{msg}")?;
154///     writeln!(env.stdout(), "bar")
155/// }
156///
157/// use_custom_cmd!(my_cmd);
158/// run_cmd!(my_cmd)?;
159/// # Ok::<(), std::io::Error>(())
160/// ```
161/// Here we import the previous defined `my_cmd` command, so we can run it like a normal command.
162#[proc_macro]
163#[proc_macro_error]
164pub fn use_custom_cmd(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
165    let item: proc_macro2::TokenStream = item.into();
166    let mut cmd_fns = vec![];
167    for t in item {
168        if let TokenTree::Punct(ref ch) = t {
169            if ch.as_char() != ',' {
170                abort!(t, "only comma is allowed");
171            }
172        } else if let TokenTree::Ident(cmd) = t {
173            let cmd_name = cmd.to_string();
174            cmd_fns.push(quote!(&#cmd_name, #cmd));
175        } else {
176            abort!(t, "expect a list of comma separated commands");
177        }
178    }
179
180    quote! (
181        #(::cmd_lib::register_cmd(#cmd_fns);)*
182    )
183    .into()
184}
185
186/// Run commands, returning [`CmdResult`](../cmd_lib/type.CmdResult.html) to check status.
187/// ```no_run
188/// # use cmd_lib::run_cmd;
189/// let msg = "I love rust";
190/// run_cmd!(echo $msg)?;
191/// run_cmd!(echo "This is the message: $msg")?;
192///
193/// // pipe commands are also supported
194/// run_cmd!(du -ah . | sort -hr | head -n 10)?;
195///
196/// // or a group of commands
197/// // if any command fails, just return Err(...)
198/// let file = "/tmp/f";
199/// let keyword = "rust";
200/// if run_cmd! {
201///     cat ${file} | grep ${keyword};
202///     echo "bad cmd" >&2;
203///     ignore ls /nofile;
204///     date;
205///     ls oops;
206///     cat oops;
207/// }.is_err() {
208///     // your error handling code
209/// }
210/// # Ok::<(), std::io::Error>(())
211/// ```
212#[proc_macro]
213#[proc_macro_error]
214pub fn run_cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
215    let cmds = lexer::Lexer::new(input.into()).scan().parse(false);
216    quote! ({
217        use ::cmd_lib::AsOsStr;
218        #cmds.run_cmd()
219    })
220    .into()
221}
222
223/// Run commands, returning [`FunResult`](../cmd_lib/type.FunResult.html) to capture output and to check status.
224/// ```no_run
225/// # use cmd_lib::run_fun;
226/// let version = run_fun!(rustc --version)?;
227/// println!("Your rust version is {}", version);
228///
229/// // with pipes
230/// let n = run_fun!(echo "the quick brown fox jumped over the lazy dog" | wc -w)?;
231/// println!("There are {} words in above sentence", n);
232/// # Ok::<(), std::io::Error>(())
233/// ```
234#[proc_macro]
235#[proc_macro_error]
236pub fn run_fun(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
237    let cmds = lexer::Lexer::new(input.into()).scan().parse(false);
238    quote! ({
239        use ::cmd_lib::AsOsStr;
240        #cmds.run_fun()
241    })
242    .into()
243}
244
245/// Run commands with/without pipes as a child process, returning [`CmdChildren`](../cmd_lib/struct.CmdChildren.html) result.
246/// ```no_run
247/// # use cmd_lib::*;
248///
249/// let mut handle = spawn!(ping -c 10 192.168.0.1)?;
250/// // ...
251/// if handle.wait().is_err() {
252///     // ...
253/// }
254/// # Ok::<(), std::io::Error>(())
255#[proc_macro]
256#[proc_macro_error]
257pub fn spawn(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
258    let cmds = lexer::Lexer::new(input.into()).scan().parse(true);
259    quote! ({
260        use ::cmd_lib::AsOsStr;
261        #cmds.spawn(false)
262    })
263    .into()
264}
265
266/// Run commands with/without pipes as a child process, returning [`FunChildren`](../cmd_lib/struct.FunChildren.html) result.
267/// ```no_run
268/// # use cmd_lib::*;
269/// let mut procs = vec![];
270/// for _ in 0..4 {
271///     let proc = spawn_with_output!(
272///         sudo bash -c "dd if=/dev/nvmen0 of=/dev/null bs=4096 skip=0 count=1024 2>&1"
273///         | awk r#"/copied/{print $(NF-1) " " $NF}"#
274///     )?;
275///     procs.push(proc);
276/// }
277///
278/// for (i, mut proc) in procs.into_iter().enumerate() {
279///     let bandwidth = proc.wait_with_output()?;
280///     info!("thread {i} bandwidth: {bandwidth} MB/s");
281/// }
282/// # Ok::<(), std::io::Error>(())
283/// ```
284#[proc_macro]
285#[proc_macro_error]
286pub fn spawn_with_output(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
287    let cmds = lexer::Lexer::new(input.into()).scan().parse(true);
288    quote! ({
289        use ::cmd_lib::AsOsStr;
290        #cmds.spawn_with_output()
291    })
292    .into()
293}
294
295#[proc_macro]
296#[proc_macro_error]
297/// Log a fatal message at the error level, and exit process.
298///
299/// e.g:
300/// ```no_run
301/// # use cmd_lib::*;
302/// let file = "bad_file";
303/// cmd_die!("could not open file: $file");
304/// // output:
305/// // [ERROR] FATAL: could not open file: bad_file
306/// ```
307/// format should be string literals, and variable interpolation is supported.
308/// Note that this macro is just for convenience. The process will exit with 1 and print
309/// "FATAL: ..." messages to error console. If you want to exit with other code, you
310/// should probably define your own macro or functions.
311pub fn cmd_die(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
312    let msg = parse_msg(input.into());
313    quote!({
314        ::cmd_lib::error!("FATAL: {} at {}:{}", #msg, file!(), line!());
315        std::process::exit(1)
316    })
317    .into()
318}
319
320fn parse_msg(input: TokenStream) -> TokenStream {
321    let mut iter = input.into_iter();
322    let mut output = TokenStream::new();
323    let mut valid = false;
324    if let Some(ref tt) = iter.next() {
325        if let TokenTree::Literal(lit) = tt {
326            let s = lit.to_string();
327            if s.starts_with('\"') || s.starts_with('r') {
328                let str_lit = lexer::scan_str_lit(lit);
329                output.extend(quote!(#str_lit));
330                valid = true;
331            }
332        }
333        if !valid {
334            abort!(tt, "invalid format: expect string literal");
335        }
336        if let Some(tt) = iter.next() {
337            abort!(
338                tt,
339                "expect string literal only, found extra {}",
340                tt.to_string()
341            );
342        }
343    }
344    output
345}
346
347mod lexer;
348mod parser;