r-cli-app

Building CLI Apps with Rapp

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "r-cli-app" with this command: npx skills add posit-dev/skills/posit-dev-skills-r-cli-app

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 <- 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

#!/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

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.

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 &#x3C;- if (file.exists(store)) yaml::read_yaml(store) else list()
if (!length(tasks)) {
  cat("No tasks yet.\n")
} else {
  if (limit >= 0L) tasks &#x3C;- head(tasks, limit)
  writeLines(sprintf("%2d. %s\n", seq_along(tasks), tasks))
}

},

add = { #| description: Task description to add. task <- NULL

tasks &#x3C;- if (file.exists(store)) yaml::read_yaml(store) else list()
tasks[[length(tasks) + 1L]] &#x3C;- task
yaml::write_yaml(tasks, store)
cat("Added:", task, "\n")

},

done = { #| description: Index of the task to complete. #| short: i index <- 1L

tasks &#x3C;- if (file.exists(store)) yaml::read_yaml(store) else list()
task &#x3C;- tasks[[as.integer(index)]]
tasks[[as.integer(index)]] &#x3C;- 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 .

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

quarto-authoring

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

critical-code-reviewer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

brand-yml

No summary provided by upstream source.

Repository SourceNeeds Review