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
!/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 (string, default "world")
count <- 1L
--count (integer, default 1)
threshold <- 0.5
--threshold (float, default 0.5)
seed <- NA_integer_
--seed (optional, NA if omitted)
output <- NA_character_
--output (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
!/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,
| 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 .