Macro design
You can use a recursive macro-by-example to build the format!
string argument list by individually processing each argument.
The recursive algorithm for the macro needs to keep of the following:
- A list of arguments yet to be processed
- A list of arguments already processed
In each step, the macro removes the first arguments from the list of arguments yet to be processed, processes it, and adds the processed argument to the end of the list of arguments already processed. It then recursively invokes itself with these modified lists. The base case is called when there is only one unprocessed argument remaining, and the recursive case is called when there are more than one unprocessed arguments remaining.
When processing an argument, we have three cases to consider:
private!(expr)
: Private argument given by expression expr
that must
be replaced with "<private>"
public!(expr)
: Public argument given by expression expr
that must
be printed as-is
expr
: Unmarked argument given by expression expr
that must be
assumed private by default
The macro has one recursive case and one base case for each of the three types of arguments listed above. However, each base and recursive case follows the same pattern - the only difference is how the argument is processed.
Implementation
macro_rules! log {
// Log message with processed arguments
(@call $fmt:literal, $(,)? $($processed_args:expr),*) => {
let log_str = format!($fmt, $($processed_args),*);
println!("{}", log_str);
};
// Base case - single unpexplictly private argument
(@rec $fmt:literal; (private!($arg:expr)); $(,)? $($processed_args:expr),*) => {
log!(@call $fmt, $($processed_args),*, "<private>");
};
// Base case - single explictly public argument
(@rec $fmt:literal; (public!($arg:expr)); $(,)? $($processed_args:expr),*) => {
log!(@call $fmt, $($processed_args),*, $arg);
};
// Base case - single unmarked argument
(@rec $fmt:literal; ($arg:expr); $(,)? $($processed_args:expr),*) => {
// Assume private
log!(@call $fmt, $($processed_args),*, "<private>");
};
// Recursive case - process one explictly private argument
(@rec $fmt:literal; (private!($arg:expr), $($unprocessed_args:tt)*); $($processed_args:tt)*) => {
log!(@rec $fmt; ($($unprocessed_args)*); $($processed_args)*, "<private>");
};
// Recursive case - process one explictly public argument
(@rec $fmt:literal; (public!($arg:expr), $($unprocessed_args:tt)*); $($processed_args:tt)*) => {
log!(@rec $fmt; ($($unprocessed_args)*); $($processed_args)*, $arg);
};
// Recursive case - process one unmarked argument
(@rec $fmt:literal; ($arg:expr, $($unprocessed_args:tt)*); $($processed_args:tt)*) => {
log!(@rec $fmt; ($($unprocessed_args)*); $($processed_args)*, "<private>");
};
// Public API
($fmt:literal, $($processed_args:tt)*) => {
log!(@rec $fmt; ($($processed_args)*); );
};
}
The recursive @rec
rules have the following argument specification:
log!(@rec <format-string>; (<comma-separated-unprocessed-args>); <comma-separated-processed-args>);
The public API rule of the macro simply builds the appropriate argument lists and forwards them to the recursive @rec
rules.
Note that the $(,)
matcher fragment present in both the base case rules and the @call
rule is used to capture trailing commas that are produced by the very first recursive case invocation (when the processed_args
list is empty).
Usage
This macro is used exactly as specified in your question:
log!("{} {} {}", private!(1), public!(2), 3);
Use private!(arg)
to denote private arguments and public!(arg)
to denote public arguments (even though the macros do not technically exist). Given that public!
and private!
don't actually exist as standalone macros, you may choose to opt for a less confusing syntax such as @private arg
and @public arg
instead.
Playground