Search code examples
rustrust-macros

Is there a way to make a macro replace things in strings?


This macro should be able to replace entries in a string via an argument. For example, this would work:

let string = "Hello, world!";
replace_macro!(string, "world", "Rust"); // Hello, Rust!

I'm not sure how to do this, as all my previous attempts of just writing a regular function and calling that don't work inside macros. If possible, I'd like to be using macro_rules as opposed to a proc macro.


Solution

  • It is not possible. Macros cannot inspect and/or change the value of variables.

    It is possible if the literal is embedded in the call (replace_macro!("Hello, world!", "world", "Rust");) but requires a proc-macro: macro_rules! macros cannot inspect and/or change literals.

    It's a rather simple with a proc macro:

    use quote::ToTokens;
    use syn::parse::Parser;
    use syn::spanned::Spanned;
    
    type Args = syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]>;
    
    #[proc_macro]
    pub fn replace_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
        let input_span = input.span();
    
        let args = match Args::parse_terminated.parse(input) {
            Ok(args) => Vec::from_iter(args),
            Err(err) => return err.into_compile_error().into(),
        };
        let (original, text, replacement) = match args.as_slice() {
            [original, text, replacement] => (original.value(), text.value(), replacement.value()),
            _ => {
                return syn::Error::new(
                    input_span,
                    r#"expected `"<original>", "<text>", "<replacement>"`"#,
                )
                .into_compile_error()
                .into()
            }
        };
    
        original
            .replace(&text, &replacement)
            .into_token_stream()
            .into()
    }
    

    It parses a list of three string literals, punctated by commas, then calls str::replace() to do the real work.