r-cli-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Building CLI Apps with Rapp

使用Rapp构建CLI应用

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

Rapp(v0.3.0)是一款R包,可作为
Rscript
的直接替代工具,能自动将命令行参数解析为R语言数据类型。它无需任何样板代码,就能将简单的R脚本转换为具备参数解析、帮助文本和子命令支持的成熟CLI应用。
R ≥ 4.1.0 | CRAN安装:
install.packages("Rapp")
| GitHub地址:
r-lib/Rapp
安装完成后,将Rapp启动器添加到PATH中:
r
Rapp::install_pkg_cli_apps("Rapp")
此操作会将
Rapp
可执行文件放置在macOS/Linux系统的
~/.local/bin
目录,或Windows系统的
%LOCALAPPDATA%\Programs\R\Rapp\bin
目录。

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.

Rapp会扫描R脚本的顶层表达式,并将特定模式转换为CLI结构。这意味着:
  1. 同一脚本通过
    source()
    调用和作为CLI工具运行时表现完全一致。
  2. 你只需编写常规R代码,Rapp会从你的代码中自动推断CLI的结构。
  3. R代码中的默认值会直接成为CLI的默认参数。
仅顶层赋值语句会被识别,函数、循环或条件语句内部的赋值不会被解析为CLI参数。

Pattern Recognition: R → CLI Mapping

模式识别:R到CLI的映射

This table is the heart of Rapp — each R pattern automatically maps to a CLI surface:
R Top-Level ExpressionCLI SurfaceNotes
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 argRequired by default
foo... <- NULL
variadic positionalZero 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 <- "", ...)
subcommandsSame; captures command name in
cmd
下表是Rapp的核心——每种R模式都会自动映射到对应的CLI元素:
R顶层表达式CLI元素说明
foo <- "text"
--foo <value>
字符串选项
foo <- 1L
--foo <int>
整数选项
foo <- 3.14
--foo <float>
浮点数选项
foo <- TRUE
/
FALSE
--foo
/
--no-foo
布尔开关
foo <- NA_integer_
--foo <int>
可选整数(NA表示未设置)
foo <- NA_character_
--foo <str>
可选字符串(NA表示未设置)
foo <- NULL
位置参数默认必填
foo... <- NULL
可变长度位置参数零个或多个值
foo <- c()
可重复
--foo
选项
多个字符串值
foo <- list()
可重复
--foo
选项
多个解析为YAML/JSON的值
switch("", cmd1={}, cmd2={})
子命令
app cmd1
,
app cmd2
switch(cmd <- "", ...)
子命令同上;将命令名称捕获到
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.

  • 非字符串标量在CLI中会被解析为YAML/JSON格式,并强制转换为默认值对应的R类型。例如
    n <- 5L
    意味着
    --n 10
    会得到整数
    10L
  • NA默认值表示参数是可选的,可通过
    !is.na(myvar)
    判断是否被设置。
  • 蛇形命名的变量名会映射为短横线命名:
    n_flips
    --n-flips
  • 位置参数始终以字符串形式传入,需手动转换类型。

Script Structure

脚本结构

Shebang line

Shebang行

r
#!/usr/bin/env Rapp
Makes the script directly executable on macOS/Linux after
chmod +x
. On Windows, call
Rapp myscript.R
explicitly.
r
#!/usr/bin/env Rapp
在macOS/Linux系统中,执行
chmod +x
后可直接运行该脚本。在Windows系统中,需显式调用
Rapp myscript.R

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).
在任何代码之前,使用哈希竖线注释(
#|
)设置脚本级元数据:
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 `|`.
name:
字段会设置帮助输出中的应用名称(默认使用文件名)。

Per-argument annotations

单参数注解

Place
#|
comments immediately before the assignment they annotate:
r
#| description: Number of coin flips
#| short: 'n'
flips <- 1L
Available annotation fields:
FieldPurpose
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
).

在赋值语句前紧邻添加
#|
注释,为该参数添加注解:
r
#| description: Number of coin flips
#| short: 'n'
flips <- 1L
可用的注解字段:
字段用途
description:
--help
中显示的帮助文本
title:
显示标题(用于子命令和前置元数据)
short:
单字母别名,例如
'n'
-n
required:
true
/
false
— 仅适用于位置参数
val_type:
覆盖类型:
string
,
integer
,
float
,
bool
,
any
arg_type:
覆盖CLI类型:
option
,
switch
,
positional
action:
针对可重复选项:
replace
append
为常用选项添加
#| short:
注解——用户期望常见标志(如详细输出
-v
、输出
-o
、计数
-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)
标量字面量赋值会成为命名选项:
r
name <- "world"          # --name <value>    (字符串,默认值"world")
count <- 1L              # --count <int>     (整数,默认值1)
threshold <- 0.5         # --threshold <flt> (浮点数,默认值0.5)
seed <- NA_integer_      # --seed <int>      (可选,未设置时为NA)
output <- NA_character_  # --output <str>    (可选,未设置时为NA)
对于可选参数,可通过以下方式判断用户是否传入:
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.
TRUE
/
FALSE
赋值会成为开关选项:
r
verbose <- FALSE   # --verbose 或 --no-verbose
wrap <- TRUE       # 默认开启--wrap,可通过--no-wrap关闭
输入
yes
/
true
/
1
会设置为TRUE;输入
no
/
false
/
0
会设置为FALSE。

Repeatable Options

可重复选项

r
pattern <- c()     # --pattern '*.csv' --pattern 'sales-*'  → character vector
threshold <- list() # --threshold 5 --threshold '[10,20]'   → list of parsed values
r
pattern <- c()     # --pattern '*.csv' --pattern 'sales-*'  → 字符向量
threshold <- list() # --threshold 5 --threshold '[10,20]'   → 解析后的值列表

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)
.
赋值
NULL
会创建位置参数(默认必填):
r
#| description: The input file to process.
input_file <- NULL
添加
#| required: false
可将其设为可选,通过
is.null(myvar)
判断是否传入。

Variadic positional args

可变长度位置参数

Use
...
suffix to collect multiple positional values:
r
pkgs... <- c()
使用
...
后缀来收集多个位置参数值:
r
pkgs... <- c()

install-pkgs dplyr ggplot2 tidyr → pkgs... = c("dplyr", "ggplot2", "tidyr")

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.

使用
switch()
函数并传入字符串作为第一个参数来声明子命令。
switch()
之前的选项为全局选项;分支内部的选项仅对该子命令生效。
r
switch(
  command <- "",

  #| title: Display the todos
  list = {
    #| description: Max entries to display (-1 for all).
    limit <- 30L
    # ... list实现代码
  },

  #| title: Add a new todo
  add = {
    #| description: Task description to add.
    task <- NULL
    # ... add实现代码
  },

  #| title: Mark a task as completed
  done = {
    #| description: Index of the task to complete.
    index <- 1L
    # ... done实现代码
  }
)
帮助信息支持作用域:
myapp --help
会列出所有子命令;
myapp list --help
会显示list子命令的专属选项及全局选项。子命令可嵌套,只需在分支内部再添加一个
switch()
即可。

Built-in Help

内置帮助功能

Every Rapp automatically gets
--help
(human-readable) and
--help-yaml
(machine-readable). These work with subcommands too.

所有Rapp应用都会自动支持
--help
(人类可读格式)和
--help-yaml
(机器可读格式),子命令也同样支持这两个参数。

Development and Testing

开发与测试

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.

在R会话中使用
Rapp::run()
来测试脚本:
r
Rapp::run("path/to/myapp.R", c("--help"))
Rapp::run("path/to/myapp.R", c("--name", "Alice", "--count", "5"))
该函数会返回(隐式)脚本的执行环境,便于调试,同时支持
browser()
进行交互式调试。

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]
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
生成的帮助信息:
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

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

在R包中发布CLI

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.

将CLI脚本放置在
exec/
目录下,并在DESCRIPTION文件中添加
Rapp
Imports
字段:
mypkg/
├── DESCRIPTION
├── R/
├── exec/
│   ├── myapp       # 包含#!/usr/bin/env Rapp shebang的脚本
│   └── myapp2
└── man/
用户安装包后,可通过以下命令安装CLI启动器:
r
Rapp::install_pkg_cli_apps("mypkg")
可提供一个便捷的安装函数,让用户无需了解Rapp的存在:
r
#' Install mypkg CLI apps
#' @export
install_mypkg_cli <- function(destdir = NULL) {
  Rapp::install_pkg_cli_apps(package = "mypkg", destdir = destdir)
}
默认情况下,启动器会设置
--default-packages=base,<pkg>
,因此仅会自动加载
base
包和当前包。如需其他依赖,需使用
library()
显式加载。

Quick Reference: Common Patterns

快速参考:常见模式

NA vs NULL for optional arguments

可选参数的NA与NULL区别

  • NA (
    NA_integer_
    ,
    NA_character_
    ) → optional named option. Test:
    !is.na(x)
    .
  • NULL +
    #| required: false
    → optional positional arg. Test:
    !is.null(x)
    .
  • NA
    NA_integer_
    ,
    NA_character_
    )→ 可选命名选项。判断方式:
    !is.na(x)
  • NULL +
    #| required: false
    → 可选位置参数。判断方式:
    !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())
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
r
message("Error: something went wrong")   # 写入标准错误输出
cat("Error:", msg, "\n", file = stderr()) # 同样写入标准错误输出
quit(status = 1)                          # 非零退出码

Error handling

错误处理

r
tryCatch({
  result <- do_work()
}, error = function(e) {
  cat("Error:", conditionMessage(e), "\n", file = stderr())
  quit(status = 1)
})

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
.
对于较不常见的主题——启动器自定义(
#| launcher:
前置元数据)、
Rapp::install_pkg_cli_apps()
的详细API选项,以及更完整的示例(去重过滤器、可变长度安装包工具、交互式回退)——请阅读
references/advanced.md