Back to skills
SkillHub ClubWrite Technical DocsFull StackData / AITech Writer

metaprogramming-rlang

Tidy evaluation and programmatic tidyverse patterns using rlang. Use this skill when writing functions that accept column names as arguments, building tidyverse-compatible APIs, or working with data-masking and injection operators. Covers embracing with {{}}, injection (!! and !!!), dynamic dots, .data/.env pronouns, name injection with glue syntax, bridge patterns between selection and data-masking, and package development with rlang.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
43
Hot score
90
Updated
March 20, 2026
Overall rating
C3.0
Composite score
3.0
Best-practice grade
B70.4

Install command

npx @skill-hub/cli install jeremy-allen-claude-skills-metaprogramming-rlang

Repository

jeremy-allen/claude-skills

Skill path: metaprogramming-rlang

Tidy evaluation and programmatic tidyverse patterns using rlang. Use this skill when writing functions that accept column names as arguments, building tidyverse-compatible APIs, or working with data-masking and injection operators. Covers embracing with {{}}, injection (!! and !!!), dynamic dots, .data/.env pronouns, name injection with glue syntax, bridge patterns between selection and data-masking, and package development with rlang.

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Data / AI, Tech Writer.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: jeremy-allen.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install metaprogramming-rlang into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/jeremy-allen/claude-skills before adding metaprogramming-rlang to shared team environments
  • Use metaprogramming-rlang for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: metaprogramming-rlang
description: |
  Tidy evaluation and programmatic tidyverse patterns using rlang. Use this skill when writing functions that accept column names as arguments, building tidyverse-compatible APIs, or working with data-masking and injection operators. Covers embracing with {{}}, injection (!! and !!!), dynamic dots, .data/.env pronouns, name injection with glue syntax, bridge patterns between selection and data-masking, and package development with rlang.
---

# Metaprogramming rlang

This skill covers modern rlang patterns for data-masking, tidy evaluation, and building programmatic tidyverse functions.

## Core Concepts

**Data-masking** allows R expressions to refer to data frame columns as if they were variables in the environment. rlang provides the metaprogramming framework that powers tidyverse data-masking.

### Key rlang Tools

- **Embracing `{{}}`** - Forward function arguments to data-masking functions
- **Injection `!!`** - Inject single expressions or values
- **Splicing `!!!`** - Inject multiple arguments from a list
- **Dynamic dots** - Programmable `...` with injection support
- **Pronouns `.data`/`.env`** - Explicit disambiguation between data and environment variables

## When to Use Each Operator

| Operator | Use Case | Example |
|----------|----------|---------|
| `{{ }}` | Forward function arguments | `summarise(mean = mean({{ var }}))` |
| `!!` | Inject single expression/value | `summarise(mean = mean(!!sym(var)))` |
| `!!!` | Inject multiple arguments | `group_by(!!!syms(vars))` |
| `.data[[]]` | Access columns by name | `mean(.data[[var]])` |

## Function Argument Patterns

### Forwarding with `{{}}`

Use `{{}}` to forward function arguments to data-masking functions. See [embrace-examples.md](references/embrace-examples.md).

### Forwarding `...`

No special syntax needed for dots forwarding. See [dots-forwarding.md](references/dots-forwarding.md).

### Names Patterns with `.data`

Use `.data` pronoun for programmatic column access. See [data-pronoun-examples.md](references/data-pronoun-examples.md).

## Injection Operators

### Advanced Injection with `!!`

Create symbols from strings, inject values to avoid name collisions. See [injection-examples.md](references/injection-examples.md).

### Splicing with `!!!`

Inject multiple symbols from character vectors, splice lists of arguments. See [splicing-examples.md](references/splicing-examples.md).

## Dynamic Dots Patterns

### Using `list2()` for Dynamic Dots Support

Enables splicing, name injection, and trailing commas. See [dynamic-dots-examples.md](references/dynamic-dots-examples.md).

### Name Injection with Glue Syntax

Use glue syntax for dynamic column naming. See [name-injection-examples.md](references/name-injection-examples.md).

## Pronouns for Disambiguation

### `.data` and `.env` Best Practices

Explicit disambiguation prevents masking issues. See [pronouns-examples.md](references/pronouns-examples.md).

## Programming Patterns

### Bridge Patterns

Converting between data-masking and tidy selection behaviors:
- `across()` as selection-to-data-mask bridge
- `across(all_of())` as names-to-data-mask bridge

See [bridge-patterns.md](references/bridge-patterns.md).

### Transformation Patterns

Transform single arguments by wrapping, transform dots with `across()`. See [transformation-patterns.md](references/transformation-patterns.md).

## Error-Prone Patterns to Avoid

### Deprecated/Dangerous Patterns

- String parsing with `eval(parse(text = ...))` - Security risk
- `get()` in data mask - Name collision prone

See [avoid-patterns.md](references/avoid-patterns.md).

### Common Mistakes

- Don't use `{{ }}` on non-arguments
- Don't mix injection styles unnecessarily

## Package Development with rlang

### Import Strategy

```r
# In DESCRIPTION:
Imports: rlang

# In NAMESPACE, import specific functions:
importFrom(rlang, enquo, enquos, expr, !!!, :=)
```

### Documentation Tags

```r
#' @param var <[`data-masked`][dplyr::dplyr_data_masking]> Column to summarize
#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Additional grouping variables
#' @param cols <[`tidy-select`][dplyr::dplyr_tidy_select]> Columns to select
```

### Testing rlang Functions

See [testing-examples.md](references/testing-examples.md) for testing data-masking and injection behavior.

source: Sarah Johnson's gist https://gist.github.com/sj-io/3828d64d0969f2a0f05297e59e6c15ad


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/embrace-examples.md

```markdown
# Forwarding with {{}} (Embrace)

Use {{}} to forward function arguments to data-masking functions.

### Single argument forwarding

```r
my_summarise <- function(data, var) {
  data |> dplyr::summarise(mean = mean({{ var }}))
}
```

### Works with any data-masking expression

```r
mtcars |> my_summarise(cyl)
mtcars |> my_summarise(cyl * am)
mtcars |> my_summarise(.data$cyl)  # pronoun syntax supported
```

```

### references/dots-forwarding.md

```markdown
# Forwarding ... (No Special Syntax Needed)

### Simple dots forwarding

```r
my_group_by <- function(.data, ...) {
  .data |> dplyr::group_by(...)
}
```

### Works with tidy selections too

```r
my_select <- function(.data, ...) {
  .data |> dplyr::select(...)
}
```

### For single-argument tidy selections, wrap in c()

```r
my_pivot_longer <- function(.data, ...) {
  .data |> tidyr::pivot_longer(c(...))
}
```

```

### references/data-pronoun-examples.md

```markdown
# Names Patterns with .data

Use .data pronoun for programmatic column access.

### Single column by name

```r
my_mean <- function(data, var) {
  data |> dplyr::summarise(mean = mean(.data[[var]]))
}
```

### Usage - completely insulated from data-masking

```r
mtcars |> my_mean("cyl")  # No ambiguity, works like regular function
```

### Multiple columns with all_of()

```r
my_select_vars <- function(data, vars) {
  data |> dplyr::select(all_of(vars))
}

mtcars |> my_select_vars(c("cyl", "am"))
```

```

### references/injection-examples.md

```markdown
# Advanced Injection with !!

### Create symbols from strings

```r
var <- "cyl"
mtcars |> dplyr::summarise(mean = mean(!!sym(var)))
```

### Inject values to avoid name collisions

```r
df <- data.frame(x = 1:3)
x <- 100
df |> dplyr::mutate(scaled = x / !!x)  # Uses both data and env x
```

### Use data_sym() for tidyeval contexts (more robust)

```r
mtcars |> dplyr::summarise(mean = mean(!!data_sym(var)))
```

```

### references/splicing-examples.md

```markdown
# Splicing with !!!

### Multiple symbols from character vector

```r
vars <- c("cyl", "am")
mtcars |> dplyr::group_by(!!!syms(vars))
```

### Or use data_syms() for tidy contexts

```r
mtcars |> dplyr::group_by(!!!data_syms(vars))
```

### Splice lists of arguments

```r
args <- list(na.rm = TRUE, trim = 0.1)
mtcars |> dplyr::summarise(mean = mean(cyl, !!!args))
```

```

### references/dynamic-dots-examples.md

```markdown
# Using list2() for Dynamic Dots Support

```r
my_function <- function(...) {
  # Collect with list2() instead of list() for dynamic features
  dots <- list2(...)
  # Process dots...
}
```

## Enables these features

```r
my_function(a = 1, b = 2)           # Normal usage
my_function(!!!list(a = 1, b = 2))  # Splice a list
my_function("{name}" := value)      # Name injection
my_function(a = 1, )                # Trailing commas OK
```

```

### references/name-injection-examples.md

```markdown
# Name Injection with Glue Syntax

### Basic name injection

```r
name <- "result"
list2("{name}" := 1)  # Creates list(result = 1)
```

### In function arguments with {{

```r
my_mean <- function(data, var) {
  data |> dplyr::summarise("mean_{{ var }}" := mean({{ var }}))
}

mtcars |> my_mean(cyl)        # Creates column "mean_cyl"
mtcars |> my_mean(cyl * am)   # Creates column "mean_cyl * am"
```

### Allow custom names with englue()

```r
my_mean <- function(data, var, name = englue("mean_{{ var }}")) {
  data |> dplyr::summarise("{name}" := mean({{ var }}))
}

# User can override default
mtcars |> my_mean(cyl, name = "cylinder_mean")
```

```

### references/pronouns-examples.md

```markdown
# .data and .env Best Practices

Explicit disambiguation prevents masking issues.

```r
cyl <- 1000  # Environment variable

mtcars |> dplyr::summarise(
  data_cyl = mean(.data$cyl),    # Data frame column
  env_cyl = mean(.env$cyl),      # Environment variable
  ambiguous = mean(cyl)          # Could be either (usually data wins)
)
```

### Use in loops and programmatic contexts

```r
vars <- c("cyl", "am")
for (var in vars) {
  result <- mtcars |> dplyr::summarise(mean = mean(.data[[var]]))
  print(result)
}
```

```

### references/bridge-patterns.md

```markdown
# Bridge Patterns

Converting between data-masking and tidy selection behaviors.

### across() as selection-to-data-mask bridge

```r
my_group_by <- function(data, vars) {
  data |> dplyr::group_by(across({{ vars }}))
}

# Works with tidy selection
mtcars |> my_group_by(starts_with("c"))
```

### across(all_of()) as names-to-data-mask bridge

```r
my_group_by <- function(data, vars) {
  data |> dplyr::group_by(across(all_of(vars)))
}

mtcars |> my_group_by(c("cyl", "am"))
```

```

### references/transformation-patterns.md

```markdown
# Transformation Patterns

### Transform single arguments by wrapping

```r
my_mean <- function(data, var) {
  data |> dplyr::summarise(mean = mean({{ var }}, na.rm = TRUE))
}
```

### Transform dots with across()

```r
my_means <- function(data, ...) {
  data |> dplyr::summarise(across(c(...), ~ mean(.x, na.rm = TRUE)))
}
```

### Manual transformation (advanced)

```r
my_means_manual <- function(.data, ...) {
  vars <- enquos(..., .named = TRUE)
  vars <- purrr::map(vars, ~ expr(mean(!!.x, na.rm = TRUE)))
  .data |> dplyr::summarise(!!!vars)
}
```

```

### references/avoid-patterns.md

```markdown
# Error-Prone Patterns to Avoid

## Deprecated/Dangerous Patterns

### Avoid - String parsing and eval (security risk)

```r
var <- "cyl"
code <- paste("mean(", var, ")")
eval(parse(text = code))  # Dangerous!
```

### Good - Symbol creation and injection

```r
!!sym(var)  # Safe symbol injection
```

### Avoid - get() in data mask (name collisions)

```r
with(mtcars, mean(get(var)))  # Collision-prone
```

### Good - Explicit injection or .data

```r
with(mtcars, mean(!!sym(var)))  # Safe
# or
mtcars |> summarise(mean(.data[[var]]))  # Even safer
```

## Common Mistakes

### Don't use {{ }} on non-arguments

```r
my_func <- function(x) {
  x <- force(x)  # x is now a value, not an argument
  quo(mean({{ x }}))  # Wrong! Captures value, not expression
}
```

### Don't mix injection styles unnecessarily

Pick one approach and stick with it:

### Either: embrace pattern

```r
my_func <- function(data, var) data |> summarise(mean = mean({{ var }}))
```

### Or: defuse-and-inject pattern

```r
my_func <- function(data, var) {
  var <- enquo(var)
  data |> summarise(mean = mean(!!var))
}
```

```

### references/testing-examples.md

```markdown
# Testing rlang Functions

### Test data-masking behavior

```r
test_that("function supports data masking", {
  result <- my_function(mtcars, cyl)
  expect_equal(names(result), "mean_cyl")

  # Test with expressions
  result2 <- my_function(mtcars, cyl * 2)
  expect_true("mean_cyl * 2" %in% names(result2))
})
```

### Test injection behavior

```r
test_that("function supports injection", {
  var <- "cyl"
  result <- my_function(mtcars, !!sym(var))
  expect_true(nrow(result) > 0)
})
```

```