Claude Skill

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

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download posit-dev-skills-r-lib_r-cli-app-b58a92e.zip · 12 KB
Part of posit-dev/skills — 23 skills

Install

skills CLI npx skills add https://github.com/posit-dev/skills/tree/main/r-lib/r-cli-app
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
Git 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:

  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

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

No comments yet.

Reviews (0)

No reviews yet.

Related