cli

$npx mdskill add posit-dev/skills/cli

Provides R CLI styling, messaging, progress bars, and formatting.

  • Formats console output with colors, inline markup, and semantic elements.
  • Depends on the cli R package for ANSI strings and themes.
  • Selects cli functions based on task type: error, warning, progress, or formatting.
  • Outputs styled messages, progress indicators, or formatted text to the console.

SKILL.md

.github/skills/cliView on GitHub ↗
---
name: cli
description: >
  Comprehensive R package for command-line interface styling, semantic messaging,
  and user communication. Use this skill when working with R code that needs to:
  (1) Format console output with inline markup and colors,
  (2) Display errors, warnings, or messages with cli_abort/cli_warn/cli_inform,
  (3) Show progress indicators for long-running operations,
  (4) Create semantic CLI elements (headers, lists, alerts, code blocks),
  (5) Apply themes and customize output styling,
  (6) Handle pluralization in user-facing text,
  (7) Work with ANSI strings, hyperlinks, or custom containers.
  Also use when migrating from base R message/warning/stop, debugging cli code,
  or improving existing cli usage.
metadata:
  author: Garrick Aden-Buie (@gadenbuie)
  version: "1.0"
license: MIT
---

# CLI for R Packages

## When to Use What

task: Display error with context and formatting
use: `cli_abort()` with inline markup and bullet lists

task: Show warning with formatting
use: `cli_warn()` with inline markup

task: Display informative message
use: `cli_inform()` with inline markup

task: Show progress for counted operations
use: `cli_progress_bar()` with total count

task: Show simple progress steps
use: `cli_progress_step()` with status messages

task: Format code or function names
use: `{.code ...}` or `{.fn package::function}`

task: Format file paths
use: `{.file path/to/file}`

task: Format package names
use: `{.pkg packagename}`

task: Format variable names
use: `{.var variable_name}`

task: Format values
use: `{.val value}`

task: Handle singular/plural text
use: `{?s}` or `{?y/ies}` with pluralization

task: Create headers
use: `cli_h1()`, `cli_h2()`, `cli_h3()`

task: Create alerts
use: `cli_alert_success()`, `cli_alert_danger()`, `cli_alert_warning()`, `cli_alert_info()`

task: Create lists
use: `cli_ul()`, `cli_ol()`, `cli_dl()` with `cli_li()`

## Inline Markup Essentials

Use inline markup with `{.class content}` syntax to format text:

```r
# Basic formatting
cli_text("Function {.fn mean} calculates averages")
cli_text("Install package {.pkg dplyr}")
cli_text("See file {.file ~/.Rprofile}")
cli_text("{.var x} must be numeric, not {.obj_type_of {x}}")
cli_text("Got value {.val {x}}")

# Code formatting
cli_text("Use {.code sum(x, na.rm = TRUE)}")

# Paths and arguments
cli_text("Reading from {.path /data/file.csv}")
cli_text("Set {.arg na.rm} to TRUE")

# Types and classes
cli_text("Object is {.cls data.frame}")

# Emphasis
cli_text("This is {.emph important}")
cli_text("This is {.strong critical}")

# Fields
cli_text("The {.field name} field is required")
```

### Vector Collapsing

Vectors are automatically collapsed with commas and "and":

```r
pkgs <- c("dplyr", "tidyr", "ggplot2")
cli_text("Installing packages: {.pkg {pkgs}}")
#> Installing packages: dplyr, tidyr, and ggplot2

files <- c("data.csv", "script.R")
cli_text("Found {length(files)} file{?s}: {.file {files}}")
#> Found 2 files: data.csv and script.R
```

### Escaping Braces

Use double braces `{{` and `}}` to escape literal braces:

```r
cli_text("Use {{variable}} syntax in glue")
#> Use {variable} syntax in glue
```

**For complete markup reference**: See [references/inline-markup.md](references/inline-markup.md) for all 50+ inline classes, edge cases, nesting rules, and advanced patterns.

## Pluralization Basics

Use `{?}` for pluralization with three patterns:

### Single Alternative

```r
nfile <- 1
cli_text("Found {nfile} file{?s}")
#> Found 1 file

nfile <- 3
cli_text("Found {nfile} file{?s}")
#> Found 3 files
```

### Two Alternatives

```r
ndir <- 1
cli_text("Found {ndir} director{?y/ies}")
#> Found 1 directory

ndir <- 5
cli_text("Found {ndir} director{?y/ies}")
#> Found 5 directories
```

### Three Alternatives (zero/one/many)

```r
nfile <- 0
cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
#> Found 0 files: no files

nfile <- 1
cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
#> Found 1 file: the file

nfile <- 3
cli_text("Found {nfile} file{?s}: {?no/the/the} file{?s}")
#> Found 3 files: the files
```

### Helpers: qty() and no()

Use `no()` to display "no" instead of zero:

```r
nfile <- 0
cli_text("Found {no(nfile)} file{?s}")
#> Found no files
```

Use `qty()` to set quantity explicitly:

```r
nupd <- 3
ntotal <- 10
cli_text("{nupd}/{ntotal} {qty(nupd)} file{?s} {?needs/need} updates")
#> 3/10 files need updates
```

**For advanced pluralization**: See [references/inline-markup.md](references/inline-markup.md) for edge cases and complex patterns.

## CLI Conditions: Core Patterns

Use cli conditions instead of base R for better formatting:

### cli_abort() - Formatted Errors

```r
# Before (base R)
stop("File not found: ", path)

# After (cli)
cli_abort("File {.file {path}} not found")

# With bullets for context
check_file <- function(path) {
  if (!file.exists(path)) {
    cli_abort(c(
      "File not found",
      "x" = "Cannot read {.file {path}}",
      "i" = "Check that the file exists"
    ))
  }
}
```

### cli_warn() - Formatted Warnings

```r
# Before (base R)
warning("Column ", col, " has missing values")

# After (cli)
cli_warn("Column {.field {col}} has missing values")

# With context
cli_warn(c(
  "Data quality issues detected",
  "!" = "Column {.field {col}} has {n_missing} missing value{?s}",
  "i" = "Consider using {.fn tidyr::drop_na}"
))
```

### cli_inform() - Formatted Messages

```r
# Before (base R)
message("Processing ", n, " files")

# After (cli)
cli_inform("Processing {n} file{?s}")

# With structure
cli_inform(c(
  "v" = "Successfully loaded {.pkg dplyr}",
  "i" = "Version {packageVersion('dplyr')}"
))
```

### Bullet Types

- `"x"` - Error/problem (red X)
- `"!"` - Warning (yellow !)
- `"i"` - Information (blue i)
- `"v"` - Success (green checkmark)
- `"*"` - Bullet point
- `">"` - Arrow/pointer

**For advanced error design**: See [references/conditions.md](references/conditions.md) for error design principles, rlang integration, testing strategies, and real-world patterns.

## Basic Progress Indicators

### Simple Progress Steps

```r
process_data <- function() {
  cli_progress_step("Loading data")
  data <- load_data()

  cli_progress_step("Cleaning data")
  clean <- clean_data(data)

  cli_progress_step("Analyzing data")
  analyze(clean)
}
```

### Basic Progress Bar

```r
process_files <- function(files) {
  cli_progress_bar("Processing files", total = length(files))

  for (file in files) {
    process_file(file)
    cli_progress_update()
  }
}
```

### Auto-Cleanup

Progress bars auto-close when the function exits:

```r
process <- function() {
  cli_progress_bar("Working", total = 100)
  for (i in 1:100) {
    Sys.sleep(0.01)
    cli_progress_update()
  }
  # No need to call cli_progress_done() - auto-closes
}
```

**For advanced progress**: See [references/progress.md](references/progress.md) for nested progress, custom formats, parallel processing, all progress variables, and Shiny integration.

## Semantic CLI Elements

### Headers

```r
cli_h1("Main Section")
cli_h2("Subsection")
cli_h3("Detail")
```

### Alerts

```r
cli_alert_success("Operation completed successfully")
cli_alert_danger("Critical error occurred")
cli_alert_warning("Potential issue detected")
cli_alert_info("Additional information available")
```

### Text and Code

```r
# Regular text with markup
cli_text("This is formatted text with {.emph emphasis}")

# Code blocks
cli_code(c(
  "library(dplyr)",
  "mtcars %>% filter(mpg > 20)"
))

# Verbatim text (no formatting)
cli_verbatim("This is displayed exactly as-is: {not interpolated}")
```

### Lists

```r
# Unordered list
cli_ul()
cli_li("First item")
cli_li("Second item")
cli_end()

# Ordered list
cli_ol()
cli_li("First step")
cli_li("Second step")
cli_end()

# Definition list
cli_dl()
cli_li(c(name = "The name field"))
cli_li(c(email = "The email address"))
cli_end()
```

## Common Workflows

### Base R to CLI Migration

```r
# Before: Base R error handling
validate_input <- function(x, y) {
  if (!is.numeric(x)) {
    stop("x must be numeric")
  }
  if (length(y) == 0) {
    stop("y cannot be empty")
  }
  if (length(x) != length(y)) {
    stop("x and y must have the same length")
  }
}

# After: CLI error handling
validate_input <- function(x, y) {
  if (!is.numeric(x)) {
    cli_abort(c(
      "{.arg x} must be numeric",
      "x" = "You supplied a {.cls {class(x)}} vector",
      "i" = "Use {.fn as.numeric} to convert"
    ))
  }

  if (length(y) == 0) {
    cli_abort(c(
      "{.arg y} cannot be empty",
      "i" = "Provide at least one element"
    ))
  }

  if (length(x) != length(y)) {
    cli_abort(c(
      "{.arg x} and {.arg y} must have the same length",
      "x" = "{.arg x} has length {length(x)}",
      "x" = "{.arg y} has length {length(y)}"
    ))
  }
}
```

### Error Message with Rich Context

```r
check_required_columns <- function(data, required_cols) {
  actual_cols <- names(data)
  missing_cols <- setdiff(required_cols, actual_cols)

  if (length(missing_cols) > 0) {
    cli_abort(c(
      "Required column{?s} missing from data",
      "x" = "Missing {length(missing_cols)} column{?s}: {.field {missing_cols}}",
      "i" = "Data has {length(actual_cols)} column{?s}: {.field {actual_cols}}",
      "i" = "Add the missing column{?s} or check for typos"
    ))
  }

  invisible(data)
}
```

### Function with Progress Bar

```r
process_files <- function(files, verbose = TRUE) {
  n <- length(files)

  if (verbose) {
    cli_progress_bar(
      format = "Processing {cli::pb_bar} {cli::pb_current}/{cli::pb_total} [{cli::pb_eta}]",
      total = n
    )
  }

  results <- vector("list", n)

  for (i in seq_along(files)) {
    results[[i]] <- process_file(files[[i]])

    if (verbose) {
      cli_progress_update()
    }
  }

  results
}
```

## Resources & Advanced Topics

### Reference Files

- **[references/inline-markup.md](references/inline-markup.md)** - Complete catalog of inline classes organized by category, advanced patterns, nesting rules, and real-world examples

- **[references/conditions.md](references/conditions.md)** - Advanced error design patterns, rlang integration, testing with testthat snapshots, migration guide, and anti-patterns

- **[references/progress.md](references/progress.md)** - Nested progress bars, custom formats, all progress variables, parallel processing, Shiny integration, and debugging

- **[references/themes.md](references/themes.md)** - Complete theming system with CSS-like selectors, container functions, color palettes, custom themes, and accessibility

- **[references/ansi-operations.md](references/ansi-operations.md)** - ANSI string operations (align, columns, nchar, etc.), hyperlinks, color detection, testing CLI output, and troubleshooting

### External Resources

- [cli package documentation](https://cli.r-lib.org)
- [cli GitHub repository](https://github.com/r-lib/cli)
- [Building a semantic CLI (article)](https://cli.r-lib.org/articles/semantic-cli.html)

### Related Packages

- **rlang** - Condition handling and error objects integrate with cli
- **glue** - String interpolation powers cli's `{}` syntax
- **testthat** - Snapshot testing for cli output

More from posit-dev/skills

SkillDescription
alt-text>
brand-ymlCreate and use brand.yml files for consistent branding across Shiny apps and Quarto documents. Covers: (1) Creating new _brand.yml files, (2) Applying to Shiny (R and Python), (3) Using in Quarto, (4) Modifying existing files, and (5) Troubleshooting. Includes complete specifications and integration guides.
cran-extrachecks>
create-release-checklist>
critical-code-reviewerConduct rigorous, adversarial code reviews with zero tolerance for mediocrity. Use when users ask to "critically review" my code or a PR, "critique my code", "find issues in my code", or "what's wrong with this code". Identifies security holes, lazy patterns, edge case failures, and bad practices across Python, R, JavaScript/TypeScript, SQL, and front-end code. Scrutinizes error handling, type safety, performance, accessibility, and code quality. Provides structured feedback with severity tiers (Blocking, Required, Suggestions) and specific, actionable recommendations.
describe-designResearch a codebase and create architectural documentation describing how features or systems work. Use when the user asks to: (1) Document how a feature works, (2) Create an architecture overview, (3) Explain code structure for onboarding or knowledge transfer, (4) Research and describe a system's design. Produces markdown documents with Mermaid diagrams and stable code references suitable for humans and AI agents.
ggsqlWrite ggsql queries — a grammar of graphics for SQL. Use when the user wants to create, modify, or understand a ggsql visualization query.
implementOrchestrates implementation of a plan file by delegating work to subagents in parallel. Verifies git branch state, tracks progress, and ensures high-quality implementation. Invoke with a plan file path and optional model override: /implement plans/my-plan.md [--model sonnet]
lifecycleGuidance for managing R package lifecycle according to tidyverse principles using the lifecycle package. Use when: (1) Setting up lifecycle infrastructure in a package, (2) Deprecating functions or arguments, (3) Renaming functions or arguments, (4) Superseding functions, (5) Marking functions as experimental, (6) Understanding lifecycle stages (stable, experimental, deprecated, superseded), or (7) Writing deprecation helpers for complex scenarios.
maintainer-declineGuide for drafting issue closure and decline responses as an open-source package maintainer. Use when helping compose a reply that says \"no\" to a feature request, closes an issue as won't-fix, redirects a user to a different package, explains why a design choice is intentional, or otherwise declines or closes a community contribution. Also use when the maintainer needs to explain a deprecation, point out a user misunderstanding, or communicate an effort/scope tradeoff to a contributor.