Rapp (short for "R application") makes it fun to write and share command line applications in R.
It is an alternative front end to R, a drop-in replacement for Rscript
that automatically parses command line arguments into R values. The goal
is to make it easy to build a polished CLI application from a simple R
script.
It aims to provide a seamless transition from interactive repl-driven development at the R console to non-interactive execution at the command line.
Here is an example Rapp, a simple R script named flip-coin.R:
#!/usr/bin/env Rapp
#| description: Flip a coin.
#| description: Number of coin flips
n <- 1L
cat(sample(c("heads", "tails"), n, TRUE), fill = TRUE)You can invoke it from the command line:
$ flip-coin
tails$ flip-coin --n 3
tails heads tails$ flip-coin --help
Usage: flip-coin [OPTIONS]
Flip a coin.
Options:
--n <FLIPS> Number of coin flips
[default: 1] [type: integer]$ flip-coin --help-yaml
name: flip-coin
description: Flip a coin.
options:
'n':
default: 1
val_type: integer
arg_type: option
action: replace
description: Number of coin flipsRapp recognizes a small set of expression patterns at the top level of your script and converts them into command-line options, flags, positional arguments, and commands. The sections below cover the supported patterns.
All Rapps come with built-in flags for help.
--helpshows usage, description, and options for the app (and for subcommands when used after a command, e.g.,todo list --help).--help-yamlprints machine-readable metadata for the app as YAML.
When a command is missing, Rapp automatically prints the same help as
--help.
Simple assignments of scalar (length-1) literals at the top level of the R script are automatically treated as command line options.
n <- 1becomes an option at the command line:
flip-coin --n 1Non-string option values passed from the command line are parsed as
YAML/JSON and must match the original R type. For example, an integer
option rejects values like 10.2 or true, while float options accept
integer or float YAML values. Values can be supplied after the option
flag, or as part of the option flag string with =. The following two
usages are the same:
flip-coin --n=1
flip-coin --n 1Assignments of TRUE, FALSE, or NA are a little different from other
options. They support usage as switches or toggles at the command line,
and the default controls which aliases are exposed. For example in an R
script:
echo <- TRUEmeans that at the command line the negative alias is supported:
my-app --no-echo # FALSEWith echo <- FALSE, the positive alias --echo is supported.
Assignments of NA are tri-state boolean switches: omitting the flag
leaves the value as NA, --echo sets it to TRUE, and --no-echo
sets it to FALSE. Boolean switches also accept explicit values, such
as --echo=true, --echo=false, --echo true, and --echo false.
To omit the generated --no-* alias for a boolean switch, add
#| negative_alias: false above the assignment:
#| description: Print version and exit.
#| negative_alias: false
version <- FALSERapp parses option values as YAML 1.2, where bare yes and no are
strings rather than boolean aliases for non-bool values. For declared
bool options, Rapp also accepts YAML 1.1 bool aliases such as yes,
no, y, n, on, and off for backward compatibility.
See Boolean option behavior for a full table of boolean defaults, annotations, and accepted command-line forms.
Assigning c() or list() declares an option that can be supplied
multiple times. Use c() when you want to keep the exact strings
provided on the command line, and list() when you want Rapp to attempt
to parse the supplied strings as YAML/JSON values and convert them into
R objects. For example, a repeatable filter option that keeps raw
strings:
#| description: File name patterns to include (repeatable).
pattern <- c()can be invoked as:
list-files --pattern '*.csv' --pattern 'sales-*'Or, to collect numeric limits and have them parsed into integers:
#| description: Score thresholds (parsed as numbers, repeatable).
threshold <- list()which lets callers supply structured values:
report --threshold 5 --threshold '[10, 20, 30]'Assigning NULL to a symbol declares a positional argument. If the
symbol has a ... suffix or prefix, it becomes a collector for a
variable number of positional arguments. Positional arguments always
come into the R app as character strings, and they are required by
default unless you mark them as required: false via annotations.
For example, this small greet app declares a required <NAME> positional
argument and prints it:
#!/usr/bin/env Rapp
#| name: greet
#| description: Greet someone.
#| description: Name to greet.
name <- NULL
cat("Hello ", name, "!\n", sep = "")Running it shows how positional arguments appear in --help:
$ greet --help
Greet someone.
Usage: greet <NAME>
Arguments:
<NAME> Name to greet.To make the positional argument optional, add an annotation above the assignment:
#| required: false
name <- NULLThis changes the usage to Usage: greet [<NAME>] (with brackets).
Use a switch() statement whose first argument is either a character
scalar or an assignment to declare commands. Command switches are
required by default; if no command is supplied, Rapp prints help for the
current command level. To allow running without a command, add
#| required: false above the switch(). The corresponding branch runs
when the matching command is supplied on the command line. Declare
command specific options and positional arguments with the same rules
inside the branch.
#!/usr/bin/env Rapp
#| name: todo
#| title: Todo manager
#| description: Manage a simple todo list.
#| description: Path to the todo list file.
#| short: s
store <- ".todo.yml"
switch(
"",
#| title: Display the todos
#| description: Print the contents of the todo list.
list = {
limit <- 30L
...
},
#| title: Add a new todo
add = {
task <- NULL
...
},
#| title: Mark a task as completed
done = {
index <- 1L
...
}
)The command shown above exposes a todo app with list, add, and
done commands. Each command can declare its own options (limit,
index) or positional arguments (task), and command metadata can be
documented with the same hash-pipe annotations used for options.
Command-line help reflects the available commands, and each command has its own help page:
$ todo --help
Todo manager
Usage: todo [OPTIONS] <COMMAND>
Manage a simple todo list.
Commands:
list Display the todos
add Add a new todo
done Mark a task as completed$ todo list --help
Display the todos
Usage: todo list [OPTIONS]
Print the contents of the todo list.
Options:
--limit <LIMIT> Maximum number of entries to display (-1 for all).
[default: 30] [type: integer]
Global options:
-s, --store <STORE> Path to the todo list file.
[default: ".todo.yml"] [type: string]Commands can be nested by including additional switch() blocks inside
a command branch; each level adds its own command-specific options,
help, and positional arguments.
Help output automatically includes any parent and global options for nested commands.
You can add YAML hash-pipe annotations in the script front matter or
right above individual options. YAML annotations are primarily used to
adjust help output. The entries you'll most commonly use are title and
description. Another YAML annotation you can provide is short, a
short option name. For example:
#!/usr/bin/env Rapp
#| description: Flip a coin.
#| description: Number of coin flips
#| short: n
n_flips <- 1L
cat(sample(c("heads", "tails"), n_flips, TRUE))then lets you supply the alias -n or the full option name --n-flips
at the command line (note also the automatic mapping of snake case
n_flips to kebab-case --n-flips).
$ flip-coin --help
Usage: flip-coin [OPTIONS]
Flip a coin.
Options:
-n, --n-flips <N-FLIPS> Number of coin flips
[default: 1] [type: integer]$ flip-coin -n 3
tails heads headsOther YAML fields you can supply to change the behavior of Rapp
val_type: expected value type (string,integer,float,bool;any).arg_type: how the input appears on the CLI (option,switch,positional).action: whether values replace or accumulate (replacevsappendfor repeatable options and collectors).negative_alias: override whether boolean switches include a generated--no-*alias.examples: usage examples to show in--help. Use a YAML sequence to list multiple examples.
Here is a summary table of different R expressions that Rapp treats as command line arguments.
| R Expression | CLI surface |
|---|---|
Assignment of scalar literalfoo <- "" |
OptionAPP --foo value |
Assignment of NULLfoo <- NULL |
Positional ArgAPP foo-value |
Assignment of FALSEfoo <- FALSE |
Boolean switchAPP --foo |
Assignment of TRUEfoo <- TRUE |
Boolean switchAPP --no-foo |
Assignment of NAfoo <- NA |
Tri-state boolean switchAPP --foo or APP --no-foo |
Assignment of c() or list()foo <- c() |
Repeatable optionAPP --foo val1 --foo val2 |
Assignment of NULL to name with ...args... <- NULL |
Positional Arg CollectorAPP foo bar baz |
Switch with string literalswitch("", cmd1 = {}, cmd2 = {}) |
Required commandsAPP --helpAPP cmd1 --helpAPP cmd2 --help |
While developing, you can drive the app directly from R:
Rapp::run("path/to/app.R", c("--help"))
Rapp::run("path/to/app.R", c("--myopt", "my-opt-val"))Pass a character vector of arguments exactly as you would supply them on
the command line. Inside the app you can drop browser() statements to
pause execution and inspect state while Rapp::run() executes.
# Install the package
install.packages("Rapp")
# Add `Rapp` to your PATH
Rapp::install_pkg_cli_apps("Rapp")Alternatively, install the development version:
# pak::pak("r-lib/Rapp")
# remotes::install_github("r-lib/Rapp")On macOS and Linux, make your Rapp script executable
(chmod +x flip-coin.R) and run them directly. On Windows, or if you
prefer, call the front end explicitly with Rapp flip-coin.R --n 3.
Rapp::install_pkg_cli_apps("Rapp") installs Rapp on the PATH.
If you are shipping Rapps via an R package, you can call
Rapp::install_pkg_cli_apps("mypackage") to install lightweight
launchers for every .R script in the package's exec/ directory whose
shebang line invokes Rapp or Rscript. A script named exec/myapp.R
is installed as the command myapp by default.
Rapp::install_pkg_cli_apps("mypackage")You can either include the command in install instructions, or export your own thin wrapper:
#' Install `mypackage` cli applications.
#'
#' @inheritDotParams Rapp::install_pkg_cli_apps -package -lib.loc
#' @export
install_mypackage_cli <- function(...) {
Rapp::install_pkg_cli_apps(package = "mypackage", lib.loc = NULL, ...)
}App launchers are written to destdir, which defaults to the first
available location from RAPP_BIN_DIR, XDG_BIN_HOME,
XDG_DATA_HOME/../bin, or the default location, ~/.local/bin on macOS
and Linux and %LOCALAPPDATA%\Programs\R\Rapp\bin on Windows. On
Windows the directory is automatically added to PATH; on macOS, the
default ~/.local/bin directory is added to ~/.zprofile when needed
($ZDOTDIR/.zprofile when ZDOTDIR is set). If that profile cannot be
updated, Rapp warns and continues installing launchers. On Linux the
directory generally is already present on PATH (you may need to log
out and back in if the Rapp installer created the directory). Use the
destdir argument if you prefer an alternate location.
Use #| launcher: front matter to customize the installed launcher. For
example, name changes the installed command name, and vanilla,
no-environ, and default-packages tune the generated Rscript
invocation.
#!/usr/bin/env Rapp
#| launcher:
#| name: myapp-fast
#| vanilla: true
#| default-packages: [base, utils, mypackage]If you are working with a standalone .R file on Windows, call the
launcher explicitly (Rapp path\to\flip-coin.R --n 3) because native
shebangs are not supported.
Launchers are optional. You can add Rapp and a package's exec/
directory to the PATH and run the apps directly from the package's
installed directory. Direct execution uses the script filename, such as
myapp.R; extensionless command names come from generated launchers. For
example, after installing {Rapp}, you can place something like this in a
shell startup script like .bashrc:
export PATH="$(Rscript -e 'cat(normalizePath(system.file("exec", package = "Rapp")))'):$PATH"
export PATH="$(Rscript -e 'cat(normalizePath(system.file("exec", package = "mypackage")))'):$PATH"On Windows, run
Rscript -e "cat(normalizePath(system.file('exec', package = 'Rapp')))"
to print the directory and add it to PATH via System Properties →
Environment Variables.
You can easily share your R app command line executable as part of an R package.
-
Add {Rapp} as a dependency in your DESCRIPTION.
-
Place your app in the
execfolder in your package as an.Rscript with aRapporRscriptshebang line (for example,exec/myapp.R).install_pkg_cli_apps()installs it asmyappby default. -
Encourage users to run
Rapp::install_pkg_cli_apps("mypackage")or your own exported wrappermypackage::install_mypackage_cli()after installing your package so the launchers land in a directory on theirPATH.install_pkg_cli_apps()keeps existing launchers up to date and also deletes old launchers for apps that have been removed from your package. -
If
rigis already on thePATH, you can also userigto run a script in a package'sexecdirectory (Stay tuned, improved rig support is in flight):rig run <pkg>::<script>
Rapp works on Windows. Running install_pkg_cli_apps() creates .bat
wrappers for each app and installs a top-level Rapp.bat, adding their
location to PATH. After that, you can invoke apps from R packages just
like on other platforms:
flip-coin --n 3Because Windows does not natively support shebangs, to invoke an Rapp
developed outside an R package, you'll need to invoke the Rapp
front-end directly (after calling Rapp::install_pkg_cli_apps("Rapp")):
Rapp path/to/flip-coin.R --n 3See the inst/examples folder for more example R apps.
This package is just one set of ideas for how to build command line apps in R. Some other packages in this space:
Also, some interesting examples of other approaches to exporting cli interfaces from R packages:
