r-lib/r-cli-app/SKILL.md
Build command-line apps in R using the Rapp package. Use when creating a CLI tool in R, adding argument parsing to an R script, turning an R script into a command-line app, shipping CLIs in an R package, or using Rapp (the alternative Rscript front-end). Also use for shebang scripts, exec/ directory in R packages, or subcommand-based R tools.
npx skillsauth add posit-dev/skills r-cli-appInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Rapp (v0.3.0) is an R package that provides a drop-in replacement for Rscript
that automatically parses command-line arguments into R values. It turns simple
R scripts into polished CLI apps with argument parsing, help text, and subcommand
support — with zero boilerplate.
R ≥ 4.1.0 | CRAN: install.packages("Rapp") | GitHub: r-lib/Rapp
After installing, put the Rapp launcher on PATH:
Rapp::install_pkg_cli_apps("Rapp")
This places the Rapp executable in ~/.local/bin (macOS/Linux) or
%LOCALAPPDATA%\Programs\R\Rapp\bin (Windows).
Rapp scans top-level expressions of an R script and converts specific patterns into CLI constructs. This means:
source() and as a CLI tool.Only top-level assignments are recognized. Assignments inside functions, loops, or conditionals are not parsed as CLI arguments.
This table is the heart of Rapp — each R pattern automatically maps to a CLI surface:
| R Top-Level Expression | CLI Surface | Notes |
|---|---|---|
| foo <- "text" | --foo <value> | String option |
| foo <- 1L | --foo <int> | Integer option |
| foo <- 3.14 | --foo <float> | Float option |
| foo <- TRUE / FALSE | --foo / --no-foo | Boolean toggle |
| foo <- NA_integer_ | --foo <int> | Optional integer (NA = not set) |
| foo <- NA_character_ | --foo <str> | Optional string (NA = not set) |
| foo <- NULL | positional arg | Required by default |
| foo... <- NULL | variadic positional | Zero or more values |
| foo <- c() | repeatable --foo | Multiple values as strings |
| foo <- list() | repeatable --foo | Multiple values parsed as YAML/JSON |
| switch("", cmd1={}, cmd2={}) | subcommands | app cmd1, app cmd2 |
| switch(cmd <- "", ...) | subcommands | Same; captures command name in cmd |
n <- 5L means --n 10 gives integer 10L.!is.na(myvar).n_flips → --n-flips.#!/usr/bin/env Rapp
Makes the script directly executable on macOS/Linux after chmod +x.
On Windows, call Rapp myscript.R explicitly.
Hash-pipe comments (#|) before any code set script-level metadata:
#!/usr/bin/env Rapp
#| name: my-app
#| title: My App
#| description: |
#| A short description of what this app does.
#| Can span multiple lines using YAML block scalar `|`.
The name: field sets the app name in help output (defaults to filename).
Place #| comments immediately before the assignment they annotate:
#| description: Number of coin flips
#| short: 'n'
flips <- 1L
Available annotation fields:
| Field | Purpose |
|---|---|
| description: | Help text shown in --help |
| title: | Display title (for subcommands and front matter) |
| short: | Single-letter alias, e.g. 'n' → -n |
| required: | true/false — for positional args only |
| val_type: | Override type: string, integer, float, bool, any |
| arg_type: | Override CLI type: option, switch, positional |
| action: | For repeatable options: replace or append |
Add #| short: for frequently-used options — users expect single-letter
shortcuts for common flags like verbose (-v), output (-o), or count (-n).
Scalar literal assignments become named options:
name <- "world" # --name <value> (string, default "world")
count <- 1L # --count <int> (integer, default 1)
threshold <- 0.5 # --threshold <flt> (float, default 0.5)
seed <- NA_integer_ # --seed <int> (optional, NA if omitted)
output <- NA_character_ # --output <str> (optional, NA if omitted)
For optional arguments, test whether the user supplied them:
seed <- NA_integer_
if (!is.na(seed)) set.seed(seed)
TRUE/FALSE assignments become toggles:
verbose <- FALSE # --verbose or --no-verbose
wrap <- TRUE # --wrap (default) or --no-wrap
Values yes/true/1 set TRUE; no/false/0 set FALSE.
pattern <- c() # --pattern '*.csv' --pattern 'sales-*' → character vector
threshold <- list() # --threshold 5 --threshold '[10,20]' → list of parsed values
Assign NULL for positional args (required by default):
#| description: The input file to process.
input_file <- NULL
Make optional with #| required: false. Test with is.null(myvar).
Use ... suffix to collect multiple positional values:
pkgs... <- c()
# install-pkgs dplyr ggplot2 tidyr → pkgs... = c("dplyr", "ggplot2", "tidyr")
Use switch() with a string first argument to declare subcommands.
Options before the switch() are global; options inside branches are
local to that subcommand.
switch(
command <- "",
#| title: Display the todos
list = {
#| description: Max entries to display (-1 for all).
limit <- 30L
# ... list implementation
},
#| title: Add a new todo
add = {
#| description: Task description to add.
task <- NULL
# ... add implementation
},
#| title: Mark a task as completed
done = {
#| description: Index of the task to complete.
index <- 1L
# ... done implementation
}
)
Help is scoped: myapp --help lists commands; myapp list --help shows
list-specific options plus globals. Subcommands can nest by placing another
switch() inside a branch.
Every Rapp automatically gets --help (human-readable) and --help-yaml
(machine-readable). These work with subcommands too.
Use Rapp::run() to test scripts from an R session:
Rapp::run("path/to/myapp.R", c("--help"))
Rapp::run("path/to/myapp.R", c("--name", "Alice", "--count", "5"))
It returns the evaluation environment (invisibly) for inspection, and
supports browser() for interactive debugging.
Use Rapp::run() with testthat snapshot testing. Test computed values by
accessing the returned environment, and test output with expect_snapshot().
See references/advanced.md for detailed testing patterns, including:
#!/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)
flip-coin # heads
flip-coin -n 3 # heads tails heads
flip-coin --seed 42 -n 5
flip-coin --help
Generated 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]
--seed <SEED> [default: NA] [type: integer]
#!/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 <- "",
list = {
#| description: Max entries to display (-1 for all).
limit <- 30L
tasks <- if (file.exists(store)) yaml::read_yaml(store) else list()
if (!length(tasks)) {
cat("No tasks yet.\n")
} else {
if (limit >= 0L) tasks <- head(tasks, limit)
writeLines(sprintf("%2d. %s\n", seq_along(tasks), tasks))
}
},
add = {
#| description: Task description to add.
task <- NULL
tasks <- if (file.exists(store)) yaml::read_yaml(store) else list()
tasks[[length(tasks) + 1L]] <- task
yaml::write_yaml(tasks, store)
cat("Added:", task, "\n")
},
done = {
#| description: Index of the task to complete.
#| short: i
index <- 1L
tasks <- if (file.exists(store)) yaml::read_yaml(store) else list()
task <- tasks[[as.integer(index)]]
tasks[[as.integer(index)]] <- NULL
yaml::write_yaml(tasks, store)
cat("Completed:", task, "\n")
}
)
todo add "Write quarterly report"
todo list
todo list --limit 5
todo done 1
todo --store /tmp/work.yml list
Place CLI scripts in exec/ and add Rapp to Imports in DESCRIPTION:
mypkg/
├── DESCRIPTION
├── R/
├── exec/
│ ├── myapp # script with #!/usr/bin/env Rapp shebang
│ └── myapp2
└── man/
Users install the CLI launchers after installing the package:
Rapp::install_pkg_cli_apps("mypkg")
Expose a convenience installer so users don't need to know about Rapp:
#' Install mypkg CLI apps
#' @export
install_mypkg_cli <- function(destdir = NULL) {
Rapp::install_pkg_cli_apps(package = "mypkg", destdir = destdir)
}
By default, launchers set --default-packages=base,<pkg>, so only base
and the package are auto-loaded. Use library() for other dependencies.
NA_integer_, NA_character_) → optional named option.
Test: !is.na(x).#| required: false → optional positional arg.
Test: !is.null(x).input_file <- NA_character_
con <- if (is.na(input_file)) file("stdin") else file(input_file, "r")
lines <- readLines(con)
writeLines(lines, stdout())
message("Error: something went wrong") # writes to stderr
cat("Error:", msg, "\n", file = stderr()) # also stderr
quit(status = 1) # non-zero exit
tryCatch({
result <- do_work()
}, error = function(e) {
cat("Error:", conditionMessage(e), "\n", file = stderr())
quit(status = 1)
})
For less common topics — launcher customization (#| launcher: front matter),
detailed Rapp::install_pkg_cli_apps() API options, and more complete examples
(deduplication filter, variadic install-pkg, interactive fallback) — read
references/advanced.md.
tools
Build modern Shiny dashboards and applications using bslib (Bootstrap 5). Use when creating new Shiny apps, modernizing legacy apps (fluidPage, fluidRow/column, tabsetPanel, wellPanel, shinythemes), or working with bslib page layouts, grid systems, cards, value boxes, navigation, sidebars, filling layouts, theming, accordions, tooltips, popovers, toasts, or bslib inputs. Assumes familiarity with basic Shiny.
development
Review test code for quality, design, and completeness after implementing a feature or fixing a bug. Use when the user asks to "review my tests", "check my test quality", "are these tests good enough", "review testing", or after completing a feature implementation that includes tests. Also use when tests feel brittle, flaky, or superficial. Cross-references production code to find coverage gaps.
tools
Guide for drafting issue closure and decline responses as an open-source package maintainer. Use when helping compose a reply that says "no" to a feature request, closes an issue as won't-fix, redirects a user to a different package, explains why a design choice is intentional, or otherwise declines or closes a community contribution. Also use when the maintainer needs to explain a deprecation, point out a user misunderstanding, or communicate an effort/scope tradeoff to a contributor.
tools
R package development with devtools, testthat, and roxygen2. Use when the user is working on an R package, running tests, writing documentation, or building package infrastructure.