r-cli-app
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 scri
Install
npx skills add https://github.com/posit-dev/skills/tree/main/r-lib/r-cli-app
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
git clone https://github.com/posit-dev/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole posit-dev/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Building CLI Apps with Rapp
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).
Core Concept: Scripts Are the Spec
Rapp scans top-level expressions of an R script and converts specific patterns into CLI constructs. This means:
- The same script works identically via
source()and as a CLI tool. - You write normal R code — Rapp infers the CLI from what you write.
- Default values in your R code become the CLI defaults.
Only top-level assignments are recognized. Assignments inside functions, loops, or conditionals are not parsed as CLI arguments.
Pattern Recognition: R → CLI Mapping
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 |
Type behavior
- Non-string scalars are parsed as YAML/JSON at the CLI and coerced to the
R type of the default.
n <- 5Lmeans--n 10gives integer10L. - NA defaults signal optional arguments. Test with
!is.na(myvar). - Snake case variable names map to kebab-case:
n_flips→--n-flips. - Positional args always arrive as character strings — convert manually.
Script Structure
Shebang line
#!/usr/bin/env Rapp
Makes the script directly executable on macOS/Linux after chmod +x.
On Windows, call Rapp myscript.R explicitly.
Front matter metadata
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).
Per-argument annotations
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).
Named Options
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)
Boolean Switches
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.
Repeatable Options
pattern <- c() # --pattern '*.csv' --pattern 'sales-*' → character vector
threshold <- list() # --threshold 5 --threshold '[10,20]' → list of parsed values
Positional Arguments
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).
Variadic positional args
Use ... suffix to collect multiple positional values:
pkgs... <- c()
# install-pkgs dplyr ggplot2 tidyr → pkgs... = c("dplyr", "ggplot2", "tidyr")
Subcommands
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.
Built-in Help
Every Rapp automatically gets --help (human-readable) and --help-yaml
(machine-readable). These work with subcommands too.
Development and Testing
Interactive Development
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.
Testing CLI Apps in Packages
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:
- Accessing computed values via the evaluation environment
- Snapshot testing for help output and formatted text
- Testing file side effects and state changes
Complete Example: 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)
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]
Complete Example: Todo Manager (Subcommands)
#!/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
Shipping CLIs in an R Package
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.
Quick Reference: Common Patterns
NA vs NULL for optional arguments
- NA (
NA_integer_,NA_character_) → optional named option. Test:!is.na(x). - NULL +
#| required: false→ optional positional arg. Test:!is.null(x).
stdin/stdout
input_file <- NA_character_
con <- if (is.na(input_file)) file("stdin") else file(input_file, "r")
lines <- readLines(con)
writeLines(lines, stdout())
Exit codes and stderr
message("Error: something went wrong") # writes to stderr
cat("Error:", msg, "\n", file = stderr()) # also stderr
quit(status = 1) # non-zero exit
Error handling
tryCatch({
result <- do_work()
}, error = function(e) {
cat("Error:", conditionMessage(e), "\n", file = stderr())
quit(status = 1)
})
Additional Reference
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.
Files (skills)
-
.evals
-
convert-script
-
eval.md 2.4 KB
You are evaluating an R CLI script generated by an AI agent using the Rapp package. The agent was given `input-script.R` (a hardcoded markdown-to-HTML renderer) and asked to convert it into a Rapp CLI app saved as `md-render.R`. Read both `input-script.R` (the original) and `md-render.R` (the generated output) and evaluate against the criteria below. For each criterion, state whether it passes or fails and give a brief explanation. ## Rapp Structure 1. **Shebang**: Does `md-render.R` start with `#!/usr/bin/env Rapp`? 2. **Front matter**: Does it have `#|` metadata with at least `name:` and `description:`? 3. **No boilerplate arg parsing**: The script should NOT use `optparse`, `argparse`, `docopt`, or manual `commandArgs()` parsing. ## Argument Conversion 4. **`input_dir` as positional**: Is it declared as `input_dir <- NULL` (positional, required)? 5. **`output_dir` as positional**: Is it declared as `output_dir <- NULL` (positional, required)? 6. **`template` as optional named**: Is it declared with an NA default (`template <- NA_character_`) so it becomes an optional `--template` flag? It should NOT be a positional arg. 7. **`recursive` as boolean**: Is it declared as `recursive <- TRUE` so it becomes `--recursive`/`--no-recursive`? 8. **Correct ordering**: Are positional args and named options declared at the top level before the main logic? ## Annotations 9. **Descriptions**: Do the arguments have `#| description:` annotations? ## Faithfulness to Original 10. **Core logic preserved**: Does the script still use `commonmark::markdown_html()`, process files in a loop, handle the template substitution, and create output directories? 11. **Template handling updated**: Does the script correctly check `!is.na(template)` (instead of `file.exists(template)`) to decide whether to apply the template, or does it handle both the NA case and the file existence check? 12. **Library preserved**: Does the script still load `commonmark` via `library()`? ## Overall Assessment Rate the output as one of: - **Excellent**: All criteria pass, faithful conversion with correct Rapp patterns for each argument type - **Good**: Minor issues but the conversion correctly identifies which values should be positional, optional named, and boolean - **Needs Work**: Argument type choices are wrong (e.g., template as positional, or required instead of optional) - **Failed**: Doesn't use Rapp, or loses the core logic during conversion -
input-script.R 1 KB · in bundle
-
prompt.md 628 B
Read @../../SKILL.md I have an R script at `input-script.R` that renders markdown files to HTML. It works fine but all the paths and options are hardcoded. Convert it into a proper CLI app using Rapp and save the result as `md-render.R`. The hardcoded values that should become CLI arguments: - `input_dir` — should be a positional argument (required) - `output_dir` — should be a positional argument (required) - `template` — should be an optional named option (not always needed) - `recursive` — should be a boolean flag (default TRUE) Keep the core logic the same, just make it configurable from the command line.
-
-
simple-cli
-
eval.md 2.1 KB
You are evaluating an R CLI script generated by an AI agent using the Rapp package. The agent was asked to create a `csv-summary.R` tool that reads a CSV file and prints summary statistics, with options for column selection, verbose output, and optional file output. Read the generated `csv-summary.R` file and evaluate it against the criteria below. For each criterion, state whether it passes or fails and give a brief explanation. ## Rapp Structure 1. **Shebang**: Does the script start with `#!/usr/bin/env Rapp`? 2. **Front matter**: Does it have `#|` metadata with at least `name:` and `description:`? 3. **Top-level pattern correctness**: Are CLI arguments declared as top-level assignments using the correct Rapp patterns? - The CSV file path should be a positional arg (`<- NULL`) - `--columns` should be a named option (`<- NA_character_` or a string default) - `--verbose` should be a boolean switch (`<- FALSE`) - `--output` should be an optional named option (`<- NA_character_`) 4. **No boilerplate arg parsing**: The script should NOT use `optparse`, `argparse`, `docopt`, or manual `commandArgs()` parsing. Rapp handles all argument parsing automatically. ## Annotations 5. **Descriptions**: Do the arguments have `#| description:` annotations that would produce clear `--help` output? 6. **Short flags**: Are short aliases (`#| short:`) used where they'd be natural (e.g., `-v` for verbose, `-o` for output)? ## Functional Correctness 7. **NA/NULL handling**: Does the script correctly test optional arguments? (`!is.na()` for NA-default options, `is.null()` for optional positional args) 8. **Logic**: Does the script plausibly work — reading the CSV, selecting columns, printing a summary, and writing to file when requested? ## Overall Assessment Rate the output as one of: - **Excellent**: All criteria pass, script is clean and idiomatic Rapp - **Good**: Minor issues (e.g., missing a short flag) but structurally correct - **Needs Work**: One or more structural Rapp patterns are wrong (wrong default types, boilerplate parsing, etc.) - **Failed**: Doesn't use Rapp at all, or fundamentally misunderstands the patterns -
prompt.md 574 B
Read @../../SKILL.md Create an R CLI tool called `csv-summary` (save it as `csv-summary.R`) that takes a CSV file path, reads it, and prints summary statistics. It should have: - A `--columns` option to specify which columns to summarize (comma-separated string, defaults to all columns if omitted) - A `--verbose` flag that, when set, also prints the number of rows and columns before the summary - An `--output` option to optionally write the summary to a file instead of stdout (if omitted, print to stdout) The positional argument should be the path to the CSV file.
-
-
subcommand-tool
-
eval.md 2.3 KB
You are evaluating an R CLI script generated by an AI agent using the Rapp package. The agent was asked to create a `dotenv.R` tool for managing environment variables in `.env` files, with `get`, `set`, and `list` subcommands. Read the generated `dotenv.R` file and evaluate it against the criteria below. For each criterion, state whether it passes or fails and give a brief explanation. ## Rapp Structure 1. **Shebang**: Does the script start with `#!/usr/bin/env Rapp`? 2. **Front matter**: Does it have `#|` metadata with at least `name:` and `description:`? 3. **Global option**: Is `--file` declared as a top-level assignment BEFORE the `switch()`, with a string default (e.g., `file <- ".env"`)? 4. **Subcommand pattern**: Does it use `switch(cmd <- "", get = {...}, set = {...}, list = {...})` or equivalent? 5. **No boilerplate arg parsing**: The script should NOT use `optparse`, `argparse`, `docopt`, or manual `commandArgs()` parsing. ## Subcommand Correctness 6. **`get` subcommand**: Does it declare `key` as a positional arg (`<- NULL`) inside the `get` branch? 7. **`set` subcommand**: Does it declare both `key` and `value` as positional args (`<- NULL`) inside the `set` branch? 8. **`list` subcommand**: Does it declare `sort` as a boolean switch (`<- FALSE`) inside the `list` branch? 9. **Scoping**: Are subcommand-specific options declared inside their respective `switch()` branches (not at the top level)? ## Annotations 10. **Subcommand metadata**: Do the subcommand branches have `#| title:` and/or `#| description:` annotations? 11. **Argument descriptions**: Do positional args and options within subcommands have `#| description:` annotations? ## Functional Correctness 12. **File I/O**: Does the script plausibly read from and write to a `.env` file (parsing `KEY=VALUE` format)? 13. **Global option usage**: Do all subcommands reference the global `file` variable for their `.env` file path? ## Overall Assessment Rate the output as one of: - **Excellent**: All criteria pass, clean idiomatic Rapp with proper subcommand structure - **Good**: Minor issues but subcommand pattern and global/local scoping are correct - **Needs Work**: Subcommand pattern is wrong, or global vs local option scoping is confused - **Failed**: Doesn't use Rapp subcommands, or fundamentally misunderstands the `switch()` pattern -
prompt.md 608 B
Read @../../SKILL.md Create an R CLI tool called `dotenv` (save it as `dotenv.R`) for managing environment variables in `.env` files. It should have: - A global `--file` option (defaults to `.env`) that controls which `.env` file to use - Three subcommands: - `get` — looks up a key and prints its value. Takes a positional `key` argument. - `set` — sets a key-value pair. Takes positional `key` and `value` arguments. - `list` — prints all key-value pairs. Has a `--sort` boolean flag (default FALSE) to sort alphabetically. Each subcommand should have a title and description for help text.
-
-
-
references
-
advanced.md 5.6 KB
# Rapp Advanced Reference ## Table of Contents - [API Reference](#api-reference) - [Testing CLI Apps](#testing-cli-apps) - [Launcher Customization](#launcher-customization) - [PATH Setup](#path-setup) - [Additional Examples](#additional-examples) --- ## API Reference ### `Rapp::run(app, args = commandArgs(TRUE))` Run an Rapp script from within R. Returns the evaluation environment (invisibly) for inspection. Returns `NULL` when `--help` is used. ```r env <- Rapp::run("exec/myapp", c("--count", "5")) ls(env) # inspect variables set by the app ``` ### `Rapp::install_pkg_cli_apps(package, destdir, lib.loc, overwrite)` Install CLI launchers for scripts in a package's `exec/` directory. - `package`: Package name(s). Defaults to all installed packages when called outside a package. - `destdir`: Where to write launchers. Resolution order: `RAPP_INSTALL_DIR` env var → `XDG_BIN_HOME` → `~/.local/bin` (macOS/Linux) or `%LOCALAPPDATA%\Programs\R\Rapp\bin` (Windows). - `overwrite`: `TRUE` always; `FALSE` never; `NA` (default) prompts interactively. - Returns: Invisibly, paths of launchers written. ### `Rapp::uninstall_pkg_cli_apps(package, destdir)` Remove launchers previously installed by `install_pkg_cli_apps()`. --- ## Testing CLI Apps ### Using `Rapp::run()` for Testing In tests, `Rapp::run()` returns the evaluation environment invisibly, giving you access to all variables and computed values from the script: ```r # tests/testthat/test-myapp.R test_that("myapp computes correctly", { app_path <- system.file("exec/myapp", package = "mypkg") env <- Rapp::run(app_path, c("--input", "42", "--double", "true")) # Access computed values expect_equal(env$result, 84) expect_equal(env$count, 42) }) ``` ### Testing Output with Snapshots Use `expect_snapshot()` to test help text, error messages, and formatted output: ```r test_that("myapp help is correct", { app_path <- system.file("exec/myapp", package = "mypkg") expect_snapshot(Rapp::run(app_path, c("--help"))) }) test_that("todo list command help", { app_path <- system.file("exec/todo", package = "mypkg") expect_snapshot(Rapp::run(app_path, c("list", "--help"))) }) ``` Snapshot files live in `tests/testthat/_snaps/`. When help text changes, run `testthat::snapshot_accept()` to review and approve updates. ### Testing Side Effects For apps that modify files or state, test the behavior directly: ```r test_that("todo add writes to store", { app_path <- system.file("exec/todo", package = "mypkg") store <- tempfile(fileext = ".yml") on.exit(unlink(store), add = TRUE) Rapp::run(app_path, c("add", "Buy milk", "--store", store)) expect_true(file.exists(store)) tasks <- yaml::read_yaml(store) expect_equal(tasks, "Buy milk") }) ``` ### Testing Strategy Summary - **Computed values & state**: Access via the returned environment (`env$variable`) - **Output/help text**: Use `expect_snapshot()` to capture exact text - **Side effects**: Test directly (file creation, data integrity, state changes) - **When `--help` is used**: `Rapp::run()` returns `NULL` instead of an environment --- ## Launcher Customization Scripts shipped in packages can customize their launcher via `#| launcher:` front matter: ```r #!/usr/bin/env Rapp #| description: About this app #| launcher: #| vanilla: true #| default-packages: [base, utils, mypkg] ``` Options map to `Rscript`/`Rapp` flags: - `vanilla: true` → `--vanilla` - `no-environ: true` → `--no-environ` - `default-packages: [base, mypkg]` → controls auto-loaded packages --- ## PATH Setup ### macOS/Linux Add `~/.local/bin` to PATH in `~/.bashrc` or `~/.zshrc`: ```sh export PATH="$HOME/.local/bin:$PATH" ``` Override the install directory: ```sh export RAPP_INSTALL_DIR="$HOME/bin" ``` ### Windows - `install_pkg_cli_apps()` creates `.bat` wrappers - The install directory is auto-added to PATH (unless `RAPP_NO_MODIFY_PATH=1` is set) - For standalone scripts: `Rapp path\to\myapp.R --count 5` --- ## Additional Examples ### Deduplication Filter (stdin/stdout + Optional Positional) ```r #!/usr/bin/env Rapp #| description: | #| Remove duplicate values from a file or input #| description: remove duplicates in reverse order from_last <- FALSE #| description: Filepath. If omitted, output is written to stdout. output <- NA_character_ #| description: Filepath. If omitted, input is read from stdin. #| required: false input <- NULL if (is.null(input)) { input <- file("stdin") } if (is.na(output)) { output <- stdout() } readLines(input) |> unique(fromLast = from_last) |> writeLines(output) ``` ```sh cat data.txt | unique.R unique.R data.txt unique.R data.txt --output deduped.txt unique.R data.txt --from-last ``` ### Variadic Args (install-pkg style) ```r #!/usr/bin/env Rapp library(remotes) force <- FALSE Ncpus <- 4L pkgs... <- c() options("Ncpus" = Ncpus) install <- function(pkg, ...) { if (grepl("^[./]", pkg)) return(install_local(pkg, ...)) if (grepl("/", pkg, fixed = TRUE)) return(install_github(pkg, ...)) install_cran(pkg, ...) } for (pkg in pkgs...) { install(pkg, force = force) } ``` ```sh install-pkg dplyr ggplot2 tidyr install-pkg r-lib/rlang --force install-pkg --Ncpus 8 dplyr ggplot2 ``` ### Interactive Fallback (magic-8-ball style) ```r #!/usr/bin/env Rapp #| name: magic-8-ball #| description: | #| Ask a yes-no question and get your answer. #| description: The question you want to ask. question <- NULL if (is.null(question)) { question <- if (interactive()) { readline("question: ") } else { cat("question: ") readLines(file("stdin"), 1) } } else { cat("question:", question, "\n") } cat("answer:", sample(c("Yes.", "No.", "Ask again later."), 1), "\n") ```
-
-
SKILL.md 11.2 KB
--- name: r-cli-app description: 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. metadata: author: Garrick Aden-Buie (@gadenbuie) version: "1.1" license: MIT --- # Building CLI Apps with Rapp 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: ```r Rapp::install_pkg_cli_apps("Rapp") ``` This places the `Rapp` executable in `~/.local/bin` (macOS/Linux) or `%LOCALAPPDATA%\Programs\R\Rapp\bin` (Windows). --- ## Core Concept: Scripts Are the Spec Rapp scans **top-level expressions** of an R script and converts specific patterns into CLI constructs. This means: 1. The same script works identically via `source()` and as a CLI tool. 2. You write normal R code — Rapp infers the CLI from what you write. 3. Default values in your R code become the CLI defaults. Only top-level assignments are recognized. Assignments inside functions, loops, or conditionals are not parsed as CLI arguments. --- ## Pattern Recognition: R → CLI Mapping 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` | ### Type behavior - **Non-string scalars** are parsed as YAML/JSON at the CLI and coerced to the R type of the default. `n <- 5L` means `--n 10` gives integer `10L`. - **NA defaults** signal optional arguments. Test with `!is.na(myvar)`. - **Snake case** variable names map to kebab-case: `n_flips` → `--n-flips`. - **Positional args** always arrive as character strings — convert manually. --- ## Script Structure ### Shebang line ```r #!/usr/bin/env Rapp ``` Makes the script directly executable on macOS/Linux after `chmod +x`. On Windows, call `Rapp myscript.R` explicitly. ### Front matter metadata Hash-pipe comments (`#|`) before any code set script-level metadata: ```r #!/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). ### Per-argument annotations Place `#|` comments immediately before the assignment they annotate: ```r #| 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`). --- ## Named Options Scalar literal assignments become named options: ```r 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: ```r seed <- NA_integer_ if (!is.na(seed)) set.seed(seed) ``` ## Boolean Switches `TRUE`/`FALSE` assignments become toggles: ```r verbose <- FALSE # --verbose or --no-verbose wrap <- TRUE # --wrap (default) or --no-wrap ``` Values `yes`/`true`/`1` set TRUE; `no`/`false`/`0` set FALSE. ## Repeatable Options ```r pattern <- c() # --pattern '*.csv' --pattern 'sales-*' → character vector threshold <- list() # --threshold 5 --threshold '[10,20]' → list of parsed values ``` ## Positional Arguments Assign `NULL` for positional args (required by default): ```r #| description: The input file to process. input_file <- NULL ``` Make optional with `#| required: false`. Test with `is.null(myvar)`. ### Variadic positional args Use `...` suffix to collect multiple positional values: ```r pkgs... <- c() # install-pkgs dplyr ggplot2 tidyr → pkgs... = c("dplyr", "ggplot2", "tidyr") ``` --- ## Subcommands Use `switch()` with a string first argument to declare subcommands. Options before the `switch()` are global; options inside branches are local to that subcommand. ```r 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. --- ## Built-in Help Every Rapp automatically gets `--help` (human-readable) and `--help-yaml` (machine-readable). These work with subcommands too. --- ## Development and Testing ### Interactive Development Use `Rapp::run()` to test scripts from an R session: ```r 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. ### Testing CLI Apps in Packages 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: - Accessing computed values via the evaluation environment - Snapshot testing for help output and formatted text - Testing file side effects and state changes --- ## Complete Example: Coin Flipper ```r #!/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) ``` ```sh 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] ``` ## Complete Example: Todo Manager (Subcommands) ```r #!/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") } ) ``` ```sh todo add "Write quarterly report" todo list todo list --limit 5 todo done 1 todo --store /tmp/work.yml list ``` --- ## Shipping CLIs in an R Package 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: ```r Rapp::install_pkg_cli_apps("mypkg") ``` Expose a convenience installer so users don't need to know about Rapp: ```r #' 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. --- ## Quick Reference: Common Patterns ### NA vs NULL for optional arguments - **NA** (`NA_integer_`, `NA_character_`) → optional **named option**. Test: `!is.na(x)`. - **NULL** + `#| required: false` → optional **positional arg**. Test: `!is.null(x)`. ### stdin/stdout ```r input_file <- NA_character_ con <- if (is.na(input_file)) file("stdin") else file(input_file, "r") lines <- readLines(con) writeLines(lines, stdout()) ``` ### Exit codes and stderr ```r message("Error: something went wrong") # writes to stderr cat("Error:", msg, "\n", file = stderr()) # also stderr quit(status = 1) # non-zero exit ``` ### Error handling ```r tryCatch({ result <- do_work() }, error = function(e) { cat("Error:", conditionMessage(e), "\n", file = stderr()) quit(status = 1) }) ``` --- ## Additional Reference 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`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.