r-cli-app — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited r-cli-app (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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 |
R type of the default. n <- 5L means --n 10 gives integer 10L.
!is.na(myvar).n_flips → --n-flips.#!/usr/bin/env RappMakes 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 <- 1LAvailable 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-wrapValues 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 valuesAssign NULL for positional args (required by default):
#| description: The input file to process.
input_file <- NULLMake 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](references/advanced.md#testing-cli-apps) 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 --helpGenerated 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 listPlace 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 exittryCatch({
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.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.