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;