# `string_magic`: Magical String Interpolation

#### Laurent R. Bergé

#### 2025-04-18

String interpolation is a common feature of many programming languages and is used to insert the value of variables directly into character strings. This is useful to compose dynamic messages in a natural way. For example `"Hello {x}"` will be interpolated as “Hello John” if `x = "John"`.

What if you want to apply some modifications to the variable before insertion? Like normalizing the case, trimming at 40 characters, or concatenating? In general, you can’t. Well, actually you can but have to use the language’s own native tools. And it can be surprising how performing a few basic operations can quickly amount to lot of typing.

The aim of the package `stringmagic` is to simplify and empower string interpolation. With the `string_magic` function, you can apply 50+ basic operations to interpolated variables, interpolations can be nested, there is advanced support for pluralization, and much more.

# 1 Basic interpolation

`string_magic` behaves in a similar way to the well known function glue. Almost anything `glue` can do, `string_magic` can do. Use curly brackets to interpolate variables: i.e to insert their value directly in the string:

    library(stringmagic)
    x = "John" ; y = "Mary"
    string_magic("Hi {x}! How's {y} doing?")
    #> [1] "Hi John! How's Mary doing?"

You can either interpolate a variable that already exists, as above, or provide the variable as an argument:

    string_magic("Hi {x}! How's {y} doing?", y = "Jane")
    #> [1] "Hi John! How's Jane doing?"

So far, so good. Now let’s come to `string_magic`’s originality: the ability to add operations to interpolations.

# 2 How to add operations to interpolations

In `string_magic` you can apply any arbitrary chain of operations to the interpolated variables. The syntax is as follows:

The `operations` are a comma separated sequence of keywords, each keyword being bound to a specific function. Here is an example in which we apply two operations:

    lovers = c("romeo", "juliet")
    string_magic("Famous lovers: {title, enum ? lovers}.")
    #> [1] "Famous lovers: Romeo and Juliet."

As shown, `title` puts to title case and `enum` creates an enumeration (i.e. turns the elements of a vector into a single character string). These two operations are applied sequentially from left to right to the variable `lovers`. You can stack as many operations as you wish.

Here is a short list of common operations: `enum` to enumerate; `lower`, `upper` and `title` to change the case; `unik` to create the vector of unique elements; `n`, `N` to format integers; `split` to break the string with respect to a pattern; `replace` to make pattern replacements; `collapse` to concatenate the strings; `first` to select the first elements; `q`, `Q` and `bq` for quoting; `align` to align the elements.

The question mark defines the separation between the operations and the expression. After the question mark, you can use any valid R expression. For example:

    string_magic("The max of each variable is: {enum ? sapply(iris[, 1:4], max)}.")
    #> [1] "The max of each variable is: 7.9, 4.4, 6.9 and 2.5."

## 2.1 Adding arguments to operations

Many operations use arguments. Arguments are passed using quoted text just before the operation. The syntax is:

Let’s take the example of splitting an email address and keeping the text before the domain:

    email = "John@Doe.com"
    string_magic("This message comes from {'@'split, first ? email}.")
    #> [1] "This message comes from John."

In this example, `split` breaks `"John@Doe.com"` with respect to `"@"`, then `first` keeps only the first element. Almost all operations have default values. For `split`, the default value is a single space. The operation `first` also accepts arguments and its default is 1. Let’s change it to keep the first two values:

    string_magic("The first two species are: {unik, '2'first, q, enum ? iris$Species}.")
    #> [1] "The first two species are: 'setosa' and 'versicolor'."

### 2.1.1 Alternative ways to pass arguments

There are three ways to pass arguments to operations:

The first, canonical way, is using quotes like in the examples of the previous section.

The second way uses a space to separate the argument from the operation and hence forbids any space in the argument.

    string_magic("The first five sepal lengths are: {5 first, enum ? iris$Sepal.Length}.")
    #> [1] "The first five sepal lengths are: 5.1, 4.9, 4.7, 4.6 and 5."

The third way uses backticks. Passing arguments with backticks requests an evaluation of the argument, using the variables defined in the calling environment. Let’s redo the previous example with argument evaluation:

    n = 5
    string_magic("The first {N?n} sepal lengths are: {`n`first, enum ? iris$Sepal.Length}.")
    #> [1] "The first five sepal lengths are: 5.1, 4.9, 4.7, 4.6 and 5."

## 2.2 Adding options to operations

Options are keywords working like flags (i.e. things that can be turned on) and change the behavior of the current operation. Many operations acccept options. Add options using a dot separated sequence of keywords attached to the operation:

We have seen the `enum` operation in an earlier example, let’s add a couple of options to it.

    fields = c("maths", "physics")
    string_magic("This position requires a PhD in either: {enum.i.or ? fields}.")
    #> [1] "This position requires a PhD in either: i) maths, or ii) physics."

As just seen, the option `i` triggers an enumeration with roman lower case numbers, and option `or` finishes the enumeration with an `or` instead of an `and`. The order of the options does not matter. Note that there is partial matching on the options, so that long option names can be shortened.

Let’s have another example where we wish to add upper cases but only for the first letter of each sentence. To do this, we use the option `sentence` of the operation `upper`:

    oversight = "hey, you forgot. forgot what? forgot the capital letters!"
    string_magic("{upper.sentence ? oversight}")
    #> [1] "Hey, you forgot. Forgot what? Forgot the capital letters!"

Wrapping up. With `string_magic` you can perform string interpolations à la `glue` and also empower these interpolations by adding operations on-the-fly (see the operations reference). This can take us a long way already. We are presenting next how you can nest interpolations and follow with an introduction to pluralization.

# 3 Nesting interpolations

In `string_magic`, you have two types of interpolations: regular interpolations (the ones we have covered so far), and verbatim evaluation. The syntax of vertatim interpolations is as follows:

Anything after the exclamation mark is treated as verbatim: i.e. anything coming before the closing bracket is treated as a simple character string. Let’s give an example:

    string_magic("Hello {upper ! world}.")
    #> [1] "Hello WORLD."

In that case, since `world` is placed after the excalamation mark (and not a question mark!), the character string `"world"` is passed to the operation `upper`, leading to the result.

The main interest of verbatim evaluations is nesting. Indeed, the function `string_magic` is always applied to the verbatim character string before any operation is applied. This means that interpolations within the verbatim text will be resolved. For example, let’s create the sum from `x1` to `x3`:

    string_magic("y = {' + 'collapse ! x{1:3}}")
    #> [1] "y = x1 + x2 + x3"

Now let’s tackle a more challenging example: write a polynomial of order `n`. It should be of the form `a + bx + cx^2 + etc`.

    n = 2
    string_magic("poly({n}): {' + 'collapse ! {letters[1 + 0:n]}x^{0:n}}")
    #> [1] "poly(2): ax^0 + bx^1 + cx^2"

Let’s break it down. The verbatim expression (the one following the exclamation mark), here `"{letters[1 + 0:n]}x^{0:n}"`, is evaluated with `string_magic`. `string_magic("{letters[1 + 0:n]}x^{0:n}")` leads to the vector `c("ax^0", "bx^1", "cx^2")`.

The operation `' + 'collapse` then concatenates that vector with `' + '`. This value is then appended to the previous string.

We can refine by adding a cleaning operation in which we replace `"x^0"` and `"^1"` by the empty string. Let’s do it:

    n = 4
    string_magic("poly({n}): {' + 'c, 'f/x^0, ^1'clean ! {letters[1 + 0:n]}x^{0:n}}")
    #> [1] "poly(4): a + bx + cx^2 + dx^3 + ex^4"

To achieve our goal, we used the operation `clean` which replaces comma separated regular expression patterns (of the form `'flags/pat1, pat2, etc'`) with the empty string. We also used the flag `f` (standing for ‘fixed’) to suppress the regular expression meaning of the patterns (because `'^'` has a specific meaning in regexes) – see the vignette on flags for more information. Finally, you can notice that the operation `collapse` has a single letter alias.

# 4 Pluralization: A brief introduction

Pluralization is about the automatic writing of a message in singular or plural form, depending on context. This feature is especially important for developers who often need to write human readable messages to inform the user. `string_magic` has an extensive support for pluralization, which we briefly introduce here.

The syntax for pluralization is as follows:

After the opening bracket, to trigger pluralization use an hashtag (`#`) or a dollar sign (`$`) followed by plural operations. There is two kinds of pluralization: either on the value of the expresison (the `#`), either on its length (the `$`). Here is an example of pluralization on length with three operations:

    friends = c("Piglet", "Eeyore")
    string_magic("My best friend{$s, are, enum ? friends}. Who am I?")
    #> [1] "My best friends are Piglet and Eeyore. Who am I?"

    friends = "Mercutio"
    string_magic("My best friend{$s, are, enum ? friends}. Who am I?")
    #> [1] "My best friend is Mercutio. Who am I?"

As you can see, the name of the operations is intuitive: `s` to add an “s” when plural, `are` to write `is` when singular and `are` when plural (the operation `is` is identical), and `enum` to create an enumeration.

Note that in plural interpolations the expression over which to pluralize is optional. It is automaically deduced from the previous interpolations. Now let’s illustrate this behavior with a pluralization on value:

    nFiles = 6
    string_magic("Warning: {N?nFiles} file{#s, is} corrupted.")
    #> [1] "Warning: six files are corrupted."

    nFiles = 1
    string_magic("Warning: {N?nFiles} file{#s, is} corrupted.")
    #> [1] "Warning: one file is corrupted."

The first interpolation is a regular one. The second one, `{#s, is}`, deduces the expression over which to pluralize from this previous interpolation.

This was only an introduction. To have more information on pluralization, please see the dedicated section.

# 5 Escaping and special cases

We end this introduction with a few special cases.

### 5.0.1 Escape brackets

The opening and closing brakets, `{}`, are special characters and cannot be used as regular text. To bypass their special meaning, you need to escape them with a double backslash.

You only need to escape the special delimiters which the algorithm is currently looking for. For example, if we escape the opening delimiter, we don’t need to escape the closing one:

    # Here opening delimiter is escaped: the closing delimiter has no special meaning
    string_magic("open = \\{, close = }")
    #> [1] "open = {, close = }"

You need to escape the closing delimiter only when the algorithm is looking for it. In the following example, we need to escape the closing curly bracket to treat it as a regular character:

    string_magic("Here are closing brackets: {5 times.c ! \\}}")
    #> [1] "Here are closing brackets: }}}}}"

Alternatively, use the argument `.delim` to change the delimiters:

    string_magic("Here I {interpolate} with .[this] ", .delim = ".[ ]", this = ".[]")
    #> [1] "Here I {interpolate} with .[] "

When using delimiters composed of two or more characters, like in the previous example, to escape it you need to add a double backslash before the first character (i.e. right before the delimiter):

    string_magic("Here I {interpolate} with \\.[] ", .delim = ".[ ]")
    #> [1] "Here I {interpolate} with .[] "

    # another example with {{ }}  as delimiter
    string_magic("I {{what}} with \\{{ }} ", .delim = "{{ }}", what = "interpolate")
    #> [1] "I interpolate with {{ }} "

Another way is to pass a character string in an expression. Indeed, since `{expr}` evaluates `expr`, if `expr` is a character string, it will stay untouched.

    string_magic("I {'can {write} {{what}} I want'}")
    #> [1] "I can {write} {{what}} I want"

### 5.0.2 Other escapes

In the few operations expecting a semi-colon (if-else and certain operations in pluralization), it can also be escaped with a double backslash.

    # if-else statement with semi-colon
    is_c = TRUE
    string_magic("{&is_c ; int i = 1\\; ; i = 1}")
    #> [1] "int i = 1;"

    is_c = FALSE
    string_magic("{&is_c ; int i = 1\\; ; i = 1}")
    #> [1] "i = 1"

### 5.0.3 How to use a regular `!` in interpolations?

In interpolations, the exclamation mark (`!`) signals a verbatim expression. But what if you use it to mean the logical operation not in an operation-free interpolation? In that case, you need a hack: use a question mark (`?`) first to indicate to the algorithm that you want to evaluate the expression.

Ex.4: using the `?` hack to use `!` as a logical operation.

    string_magic("{!TRUE} is {?!TRUE}")
    #> [1] "TRUE is FALSE"