We’re excited to share our first tidyverse blog post for Rapp, alongside the 0.3.0 release. Rapp helps you turn R scripts into polished command-line tools, with argument parsing and help generation built in.
Why a command-line interface for R?
A command-line interface (CLI) lets you run programs from a terminal, without opening an IDE or starting an interactive R session. This is useful when you want to:
- automate tasks via cron jobs, scheduled tasks, or CI/CD pipelines
- chain R scripts together with other tools in data pipelines
- let others run your R code without needing to know R
- package reusable tools that feel native to the terminal
- expose specific actions through a clean interface that LLM agents can invoke
There are several established packages for building CLIs in R, including argparse, optparse, and docopt, where you explicitly parse and handle command-line arguments in code. Rapp takes a different approach: it derives the CLI surface from the structure of your R script and injects values at runtime, so you never need to handle CLI arguments manually.
How Rapp works
At its core, Rapp is an alternative front-end to R: a drop-in replacement for Rscript that automatically turns common R expression patterns into command-line options, switches, positional arguments, and subcommands. You write normal R code and Rapp handles the CLI surface.
Rapp also uses special #| comments (similar to Quarto’s YAML-in-comments syntax) to add metadata such as help descriptions and short aliases.
A tiny example
Here’s a complete Rapp script (from the package examples), a coin flipper:
#!/usr/bin/env Rapp
#| name: flip-coin
#| description: |
#| Flip a coin.
#| description: Number of coin flips
#| short: 'n'
flips <- 1L
sep <- " "
wrap <- TRUE
seed <- NA_integer_
if (!is.na(seed)) {
set.seed(seed)
}
cat(sample(c("heads", "tails"), flips, TRUE), sep = sep, fill = wrap)
Let’s break down how Rapp interprets this script:
| R code | Generated CLI option | What it does |
|---|---|---|
flips <- 1L | --flips or -n | Integer option with default of 1 |
sep <- " " | --sep | String option with default of " " |
wrap <- TRUE | --wrap / --no-wrap | Boolean toggle (TRUE/FALSE becomes on/off) |
seed <- NA_integer_ | --seed | Optional integer (NA means “not set”) |
The #| short: 'n' comment adds -n as a short alias for --flips. The #!/usr/bin/env Rapp line (called a “shebang”) lets you run the script directly on macOS and Linux without typing Rapp first.
Running the script
With Rapp installed and flip-coin available on your PATH (see
Get started below), you can run the app from the terminal:
flip-coin -n 3
#> heads tails heads
flip-coin --seed 42 -n 5
#> tails heads tails tails heads
Auto-generated help
Rapp generates --help from your script (and --help-yaml if you want a machine-readable spec):
flip-coin --help
Usage: flip-coin [OPTIONS]
Flip a coin.
Options:
-n, --flips <FLIPS> Number of coin flips [default: 1] [type: integer]
--sep <SEP> [default: " "] [type: string]
--wrap / --no-wrap [default: true] Disable with `--no-wrap`.
--seed <SEED> [default: NA] [type: integer]
Breaking change in 0.3.0: positional arguments are now required by default
If you’re upgrading from an earlier version of Rapp, note that positional arguments are now required unless explicitly marked optional.
# Before 0.3.0: this positional was optional
name <- NULL
# In 0.3.0+: add this comment to keep it optional
#| required: false
name <- NULL
If your scripts use positional arguments with NULL defaults that should remain optional, add #| required: false above them.
Highlights in 0.3.0
Rapp will be new to most readers, so rather than listing every change, here are the main ideas (and what’s improved in 0.3.0).
Options, switches, and repeatable flags from plain R
Rapp recognizes a small set of “declarative” patterns at the top level of your script:
- Scalar literals like
flips <- 1Lbecome options like--flips 10. - Logical defaults like
wrap <- TRUEbecome toggles like--wrap/--no-wrap. #| short: nadds a short alias like-n(new in 0.3.0).c()andlist()defaults declare repeatable options (new in 0.3.0): callers can supply the same flag multiple times and values are appended.
Subcommands with switch()
Rapp can now turn a
switch() block into subcommands (and you can nest
switch() blocks for nested commands). Here’s a small sketch of a todo-style app:
#!/usr/bin/env Rapp
#| name: todo
#| description: Manage a simple todo list.
#| description: Path to the todo list file.
#| short: s
store <- ".todo.yml"
switch(
command <- "",
#| description: Display the todos
list = {
limit <- 30L
# ...
},
#| description: Add a new todo
add = {
task <- NULL
# ...
}
)
Help is scoped to the command you’re asking about, so todo --help lists the commands, and todo list --help shows just the options/arguments for list (plus any parent/global options).
Installable launchers for package CLIs
A big part of sharing CLI tools is making them easy to run after installation. In 0.3.0, install_pkg_cli_apps() installs lightweight launchers for scripts in a package’s exec/ directory that use either #!/usr/bin/env Rapp or #!/usr/bin/env Rscript:
Rapp::install_pkg_cli_apps("mypackage")
(There’s also uninstall_pkg_cli_apps() to remove a package’s launchers.)
Get started
Here’s the quickest path to your first Rapp script:
# 1. Install the package
install.packages("Rapp")
# 2. Install the command-line launcher
Rapp::install_pkg_cli_apps("Rapp")
Then create a script (e.g., hello.R):
#!/usr/bin/env Rapp
#| name: hello
#| description: Say hello
name <- "world"
cat("Hello,", name, "\n")
And run it:
Rapp hello.R --name "R users"
#> Hello, R users
Learn more
To dig deeper into Rapp:
- browse examples in the package:
system.file("examples", package = "Rapp") - read the full documentation: https://github.com/r-lib/Rapp
- note that Rapp requires R ≥ 4.1.0
If you try Rapp, we’d love feedback! We especially want to hear about your experiences with edge cases in argument parsing, help output, and how commands should feel. Issues and ideas are welcome at https://github.com/r-lib/Rapp/issues.