cli
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_war
Install
npx skills add https://github.com/posit-dev/skills/tree/main/r-lib/cli
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
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
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:
# 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":
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:
cli_text("Use {{variable}} syntax in glue")
#> Use {variable} syntax in glue
For complete markup reference: See 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
nfile <- 1
cli_text("Found {nfile} file{?s}")
#> Found 1 file
nfile <- 3
cli_text("Found {nfile} file{?s}")
#> Found 3 files
Two Alternatives
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)
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:
nfile <- 0
cli_text("Found {no(nfile)} file{?s}")
#> Found no files
Use qty() to set quantity explicitly:
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 for edge cases and complex patterns.
CLI Conditions: Core Patterns
Use cli conditions instead of base R for better formatting:
cli_abort() - Formatted Errors
# 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
# 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
# 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 for error design principles, rlang integration, testing strategies, and real-world patterns.
Basic Progress Indicators
Simple Progress Steps
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
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:
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 for nested progress, custom formats, parallel processing, all progress variables, and Shiny integration.
Semantic CLI Elements
Headers
cli_h1("Main Section")
cli_h2("Subsection")
cli_h3("Detail")
Alerts
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
# 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
# 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
# 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
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
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 - Complete catalog of inline classes organized by category, advanced patterns, nesting rules, and real-world examples
references/conditions.md - Advanced error design patterns, rlang integration, testing with testthat snapshots, migration guide, and anti-patterns
references/progress.md - Nested progress bars, custom formats, all progress variables, parallel processing, Shiny integration, and debugging
references/themes.md - Complete theming system with CSS-like selectors, container functions, color palettes, custom themes, and accessibility
references/ansi-operations.md - ANSI string operations (align, columns, nchar, etc.), hyperlinks, color detection, testing CLI output, and troubleshooting
External Resources
Related Packages
- rlang - Condition handling and error objects integrate with cli
- glue - String interpolation powers cli's
{}syntax - testthat - Snapshot testing for cli output
Files (skills)
-
references
-
ansi-operations.md 19.3 KB
# ANSI Operations and Advanced Features ## Table of Contents 1. [ANSI String Operations](#ansi-string-operations) - [Character Counting](#character-counting) - [Text Alignment](#text-alignment) - [String Splitting](#string-splitting) - [Substring Operations](#substring-operations) - [Text Wrapping](#text-wrapping) - [Case Conversion](#case-conversion) - [Whitespace Handling](#whitespace-handling) - [Columnar Layout](#columnar-layout) - [Removing ANSI Codes](#removing-ansi-codes) 2. [Hyperlinks](#hyperlinks) 3. [Advanced Colors](#advanced-colors) 4. [Symbol Sets](#symbol-sets) 5. [Testing CLI Output](#testing-cli-output) 6. [Performance](#performance) 7. [Debugging](#debugging) ## ANSI String Operations All base R string functions break when applied to ANSI-formatted text because they count escape codes as characters. The cli package provides ANSI-aware versions of common string operations. ### Character Counting Use `ansi_nchar()` instead of `nchar()` to count visible characters: ```r # Problem with base R text <- col_red("hello") nchar(text) #> [1] 15 # Includes ANSI codes # Solution with cli ansi_nchar(text) #> [1] 5 # Counts only visible characters # Works with vectors texts <- c(col_blue("short"), col_green("longer text")) ansi_nchar(texts) #> [1] 5 11 # type argument works like base nchar() ansi_nchar(text, type = "width") # Display width ansi_nchar(text, type = "chars") # Character count ansi_nchar(text, type = "bytes") # Byte count ``` ### Text Alignment Use `ansi_align()` to align formatted text while accounting for ANSI codes: ```r # Left alignment (default) texts <- c(col_red("Error"), col_yellow("Warning"), col_green("OK")) ansi_align(texts, width = 10) #> [1] "Error " "Warning " "OK " # Right alignment ansi_align(texts, width = 10, align = "right") #> [1] " Error" " Warning" " OK" # Center alignment ansi_align(texts, width = 10, align = "center") #> [1] " Error " " Warning " " OK " # Practical example: aligned status messages statuses <- c(col_green("Success"), col_red("Failed"), col_yellow("Pending")) labels <- c("Database", "API", "Cache") paste0(labels, ": ", ansi_align(statuses, width = 10)) #> [1] "Database: Success " "API: Failed " "Cache: Pending " ``` ### String Splitting Use `ansi_strsplit()` instead of `strsplit()`: ```r # Split colored text text <- paste0(col_red("hello"), " ", col_blue("world")) ansi_strsplit(text, " ") #> [[1]] #> [1] "\033[31mhello\033[39m" "\033[34mworld\033[39m" # Fixed strings ansi_strsplit(col_green("a-b-c"), "-", fixed = TRUE) #> [[1]] #> [1] "\033[32ma\033[39m" "\033[32mb\033[39m" "\033[32mc\033[39m" # Regular expressions text <- col_red("one123two456three") ansi_strsplit(text, "[0-9]+") #> [[1]] #> [1] "\033[31mone\033[39m" "\033[31mtwo\033[39m" "\033[31mthree\033[39m" ``` ### Substring Operations Use `ansi_substr()` and `ansi_substring()` for extracting substrings: ```r # Extract substring text <- col_red("hello world") ansi_substr(text, 1, 5) #> [1] "\033[31mhello\033[39m" # Preserves color ansi_substr(text, 7, 11) #> [1] "\033[31mworld\033[39m" # Negative indices work like base R ansi_substring(text, 7) # From position 7 to end #> [1] "\033[31mworld\033[39m" # Substring replacement text <- col_blue("hello world") ansi_substr(text, 1, 5) <- "HELLO" text #> [1] "\033[34mHELLO world\033[39m" # Multiple strings texts <- c(col_red("abcdef"), col_green("123456")) ansi_substr(texts, 2, 4) #> [1] "\033[31mbcd\033[39m" "\033[32m234\033[39m" ``` ### Text Wrapping Use `ansi_strwrap()` for wrapping formatted text: ```r # Basic wrapping long_text <- paste0( col_blue("This is a long piece of text "), col_red("that needs to be wrapped "), col_green("across multiple lines") ) ansi_strwrap(long_text, width = 30) #> [1] "This is a long piece of text" #> [2] "that needs to be wrapped" #> [3] "across multiple lines" # Custom indent and exdent ansi_strwrap(long_text, width = 40, indent = 2, exdent = 4) #> First line indented by 2, subsequent by 4 # Simplify option ansi_strwrap(c(col_red("short"), col_blue("text")), simplify = FALSE) #> Returns a list # Practical example: formatted help text help_text <- paste0( col_bold("--verbose"), " ", "Enable verbose output with detailed logging information" ) cat(ansi_strwrap(help_text, width = 60), sep = "\n") ``` ### Case Conversion Use `ansi_toupper()`, `ansi_tolower()`, and `ansi_chartr()`: ```r # Convert to uppercase text <- col_red("hello world") ansi_toupper(text) #> [1] "\033[31mHELLO WORLD\033[39m" # Convert to lowercase text <- col_blue("HELLO WORLD") ansi_tolower(text) #> [1] "\033[34mhello world\033[39m" # Character translation ansi_chartr("aeiou", "AEIOU", col_green("hello world")) #> [1] "\033[32mhEllO wOrld\033[39m" # Preserves all formatting text <- paste0(col_red(style_bold("error")), ": ", col_blue("message")) ansi_toupper(text) #> Colors and styles preserved ``` ### Whitespace Handling Use `ansi_trimws()` to trim whitespace from ANSI strings: ```r # Trim both sides (default) text <- col_red(" hello world ") ansi_trimws(text) #> [1] "\033[31mhello world\033[39m" # Trim left only ansi_trimws(text, which = "left") #> [1] "\033[31mhello world \033[39m" # Trim right only ansi_trimws(text, which = "right") #> [1] "\033[31m hello world\033[39m" # Custom whitespace definition ansi_trimws(col_blue("..hello.."), whitespace = ".") #> [1] "\033[34mhello\033[39m" ``` ### Columnar Layout Use `ansi_columns()` to create column layouts with ANSI-formatted text: ```r # Simple two-column layout items <- paste0(col_blue(letters[1:10]), " = ", col_green(1:10)) ansi_columns(items, width = 40) #> Arranges items in columns that fit in 40 characters # Custom number of columns ansi_columns(items, width = 60, fill = "cols") #> fill = "cols" fills by columns, "rows" fills by rows # Practical example: displaying options options <- paste0( col_yellow(sprintf("--option%d", 1:20)), " ", col_grey("Description text") ) ansi_columns(options, width = 80) ``` ### Removing ANSI Codes Use `ansi_strip()` to remove all ANSI formatting: ```r # Remove all ANSI codes text <- col_red(style_bold("error")) ansi_strip(text) #> [1] "error" # Works with complex formatting text <- paste0( col_blue("Status: "), col_green(style_underline("OK")) ) ansi_strip(text) #> [1] "Status: OK" # Essential for testing (see Testing CLI Output section) expect_equal(ansi_strip(my_output()), "expected plain text") ``` ## Hyperlinks Modern terminals support hyperlinks via ANSI escape codes. The cli package provides several link types through inline markup. ### Terminal Support Detection Check if the current terminal supports hyperlinks: ```r # Check for hyperlink support style_hyperlink_supported() #> [1] TRUE or FALSE # Hyperlinks are auto-disabled when not supported # Always safe to use in code ``` ### Link Types Overview ```r # URL links - external websites cli_text("See {.url https://example.com}") # URL with custom text cli_text("Visit our {.href [website](https://example.com)}") # File links cli_text("Check {.file /path/to/file.R}") # File with line and column cli_text("Error at {.file /path/to/file.R:42:10}") # Function documentation cli_text("Use {.fun package::function}") # Help topic cli_text("See {.help topic}") # Topic in package cli_text("Read {.topic stats::lm}") # Vignette cli_text("Tutorial: {.vignette dplyr::introduction}") # Executable code cli_text("Try {.run code_to_execute()}") ``` ### .url vs .href - When to Use Each **Use `.url` when:** - The URL is the meaningful content - Showing the full URL is important - The link destination should be visible ```r cli_text("Documentation: {.url https://cli.r-lib.org}") cli_text("API endpoint: {.url https://api.example.com/v1}") ``` **Use `.href` when:** - The link text should differ from the URL - Creating natural prose with embedded links - The URL is long or technical ```r cli_text("Read the {.href [complete guide](https://very-long-url.com/path)}") cli_text("See {.href [issue #123](https://github.com/org/repo/issues/123)}") ``` ### .file with Line and Column Syntax ```r # Just the file cli_text("Modified {.file script.R}") # File with line number cli_text("Error in {.file script.R:42}") # File with line and column cli_text("Syntax error at {.file script.R:42:10}") # Full path with line numbers cli_text("See {.file /home/user/project/R/utils.R:100:5}") # Practical example in error messages check_syntax <- function(file, line, col) { cli_abort(c( "Syntax error detected", "x" = "Unexpected token at {.file {file}:{line}:{col}}", "i" = "Check for matching braces" )) } ``` ### Documentation Links ```r # Function help - opens help page cli_text("Calculate mean with {.fun mean}") cli_text("Join tables using {.fun dplyr::left_join}") # Help topic cli_text("Learn about {.help vectors}") # Topic from specific package cli_text("Read about {.topic dplyr::mutate}") # Vignette cli_text("Tutorial: {.vignette dplyr::window-functions}") cli_text("Guide: {.vignette tidyr::pivot}") # Practical example: suggesting functions suggest_function <- function() { cli_inform(c( "i" = "For string manipulation, try {.fun stringr::str_replace}", "i" = "See {.topic stringr::str_replace} for examples", "i" = "Learn more: {.vignette stringr::stringr}" )) } ``` ### .run Links - Executable Code **Security Warning:** `.run` links execute arbitrary code when clicked. Use with extreme caution. ```r # Simple command cli_text("Install with {.run install.packages('cli')}") # Multiple statements cli_text("Setup: {.run source('setup.R'); init_project()}") # Display differs from execution cli_text("{.run [reset database](drop_all_tables(); rebuild())}") # Safe use cases only: # - Your own diagnostic commands # - Read-only operations # - Well-understood helper functions # NEVER use .run with: # - User-supplied input # - File system modifications from untrusted sources # - Network operations # - Data deletion ``` ### Practical Hyperlink Examples ```r # Error with file link file_error <- function(path, line) { cli_abort(c( "Parse error in configuration file", "x" = "Invalid syntax at {.file {path}:{line}}", "i" = "Check the {.href [YAML specification](https://yaml.org/spec/)}" )) } # Function suggestion with documentation suggest_alternative <- function(old_fn, new_fn) { cli_warn(c( "{.fun {old_fn}} is deprecated", "!" = "Use {.fun {new_fn}} instead", "i" = "See {.topic {new_fn}} for details" )) } # Help message with links show_resources <- function() { cli_inform(c( "v" = "Package installed successfully", "i" = "Documentation: {.url https://pkg.example.com}", "i" = "Quick start: {.vignette mypkg::quickstart}", "i" = "Get help: {.run help('mypkg')}" )) } ``` ## Advanced Colors ### Color Palette Customization Beyond basic colors, cli supports extensive palette customization: ```r # Create custom color palette my_palette <- list( error = "#FF5555", warn = "#FFB86C", success = "#50FA7B", info = "#8BE9FD" ) # Apply custom theme my_theme <- list( span.error = list(color = my_palette$error), span.warn = list(color = my_palette$warn), span.success = list(color = my_palette$success), span.info = list(color = my_palette$info) ) cli_div(theme = my_theme) ``` ### Terminal Capability Detection ```r # Detect number of colors supported num_ansi_colors() #> Returns: 1 (no colors), 8, 256, or 16777216 (truecolor) # Check capabilities if (num_ansi_colors() >= 256) { # Use 256-color palette style_rgb(100, 150, 200) } else if (num_ansi_colors() >= 8) { # Fall back to 8-color palette col_blue() } else { # No colors, use plain text identity() } ``` ### Color Mode Details **Truecolor (16.7M colors):** ```r # num_ansi_colors() returns 16777216 # Full RGB color space available make_ansi_style("#FF5733") # Hex colors style_rgb(255, 87, 51) # RGB values ``` **256-color mode:** ```r # num_ansi_colors() returns 256 # 216 colors + 24 grayscale # Colors approximated from truecolor ``` **8-color mode:** ```r # num_ansi_colors() returns 8 # Basic ANSI colors only: black, red, green, yellow, # blue, magenta, cyan, white ``` **No color (1):** ```r # num_ansi_colors() returns 1 # All styling removed # Useful for piping to files or non-terminal output ``` ### Custom Color Functions ```r # Define reusable color functions error_color <- make_ansi_style("#FF5555", bg = FALSE) success_color <- make_ansi_style("#50FA7B", bg = FALSE) highlight_bg <- make_ansi_style("#FFFF00", bg = TRUE) # Use in messages cli_text("Status: {error_color('Failed')}") cli_text("Result: {success_color('Success')}") cli_text("{highlight_bg('Important')}") # Combine styles emphasize <- combine_ansi_styles( make_ansi_style("#FF5555"), style_bold ) cli_text("{emphasize('Critical warning')}") ``` ## Symbol Sets The cli package provides Unicode symbols with automatic ASCII fallback. ### Symbol Behavior ```r # Unicode symbols in capable terminals symbol$tick # ✔ symbol$cross # ✖ symbol$arrow_right # → symbol$ellipsis # … symbol$warning # ⚠ # Automatic ASCII fallback when needed # Environment: NO_UNICODE=1 or incapable terminal symbol$tick # v symbol$cross # x symbol$arrow_right # -> symbol$ellipsis # ... symbol$warning # ! ``` ### Available Symbols ```r # Status indicators symbol$tick # ✔ or v symbol$cross # ✖ or x symbol$circle_filled # ● or (*) symbol$circle_dotted # ◌ or ( ) # Arrows symbol$arrow_right # → or -> symbol$arrow_left # ← or <- symbol$arrow_up # ↑ or ^ symbol$arrow_down # ↓ or v # UI elements symbol$ellipsis # … or ... symbol$continue # ⋯ or ... symbol$warning # ⚠ or ! symbol$info # ℹ or i # Pointers symbol$pointer # ❯ or > symbol$radio_on # ◉ or (*) symbol$radio_off # ◯ or ( ) # Box drawing (for tables/borders) symbol$line # ─ or - symbol$double_line # ═ or = ``` ### Practical Symbol Usage ```r # Status messages cli_alert("Processing {symbol$ellipsis}") cli_text("{symbol$tick} Done") cli_text("{symbol$cross} Failed") # Lists with custom bullets cli_text("{symbol$pointer} Option 1") cli_text("{symbol$pointer} Option 2") # Progress indicators cli_text("Step 1 {symbol$arrow_right} Step 2 {symbol$arrow_right} Step 3") ``` ## Testing CLI Output ### Basic Testing with ansi_strip() ```r test_that("function produces correct message", { # Capture output output <- capture.output({ cli_alert_success("Operation complete") }) # Strip ANSI codes for comparison plain <- ansi_strip(paste(output, collapse = "\n")) expect_match(plain, "Operation complete") }) ``` ### Snapshot Testing with testthat Best practice for testing CLI output: ```r test_that("error message is correct", { # Snapshot the entire formatted output expect_snapshot(error = TRUE, { my_function_that_errors() }) }) test_that("progress output is correct", { # Use local options for reproducible output withr::local_options(cli.width = 80) expect_snapshot({ my_function_with_progress() }) }) ``` ### Testing in Non-Interactive Mode ```r test_that("cli works non-interactively", { # Simulate non-interactive environment withr::local_options(cli.dynamic = FALSE) output <- capture.output({ cli_progress_bar("Working", total = 100) for (i in 1:100) cli_progress_update() }) # No progress bar in non-interactive mode expect_length(output, 0) }) ``` ### Using test_that_cli() Special testing helper for CLI output: ```r test_that_cli("formatted message appears", { # Automatically sets up CLI testing environment # - Fixed width (80 columns) # - ANSI colors enabled # - Unicode symbols enabled expect_snapshot({ cli_h1("Header") cli_alert_success("Done") }) }) # Equivalent to: test_that("formatted message appears", { withr::local_options( cli.width = 80, cli.num_colors = 256, cli.unicode = TRUE ) expect_snapshot({ cli_h1("Header") cli_alert_success("Done") }) }) ``` ### Transform for Reproducible Tests ```r test_that("output is stable", { expect_snapshot( my_function_with_timestamps(), transform = function(output) { # Remove variable elements output <- ansi_strip(output) output <- gsub("\\d{4}-\\d{2}-\\d{2}", "[DATE]", output) output <- gsub("\\d+\\.\\d+ seconds", "[TIME]", output) output } ) }) ``` ## Performance ### When CLI Adds Overhead CLI has minimal overhead in most cases, but be aware of: **High-frequency operations:** ```r # Avoid CLI in tight loops for (i in 1:1000000) { cli_alert("Iteration {i}") # Very slow! } # Instead: use progress bar cli_progress_bar("Processing", total = 1000000) for (i in 1:1000000) { # work if (i %% 1000 == 0) cli_progress_update() } ``` **String interpolation cost:** ```r # Expensive: interpolation on every call for (i in 1:10000) { msg <- cli::cli_fmt(cli::cli_text("Value: {i}")) } # Cheaper: use sprintf or paste for simple cases for (i in 1:10000) { msg <- sprintf("Value: %d", i) } ``` ### Optimization Strategies **Disable in production:** ```r # Option to disable all CLI output options(cli.default_handler = function(...) NULL) # Or use environment variable Sys.setenv(CLI_NO_OUTPUT = "true") ``` **Batch updates:** ```r # Bad: update on every item cli_progress_bar("Processing", total = 1000000) for (i in 1:1000000) { process(i) cli_progress_update() # Too frequent } # Good: batch updates cli_progress_bar("Processing", total = 1000000) for (i in 1:1000000) { process(i) if (i %% 100 == 0) cli_progress_update(inc = 100) } ``` **Conditional verbosity:** ```r process_data <- function(data, verbose = TRUE) { if (verbose) { cli_progress_bar("Processing", total = nrow(data)) } for (i in seq_len(nrow(data))) { # work if (verbose) cli_progress_update() } } ``` ## Debugging ### CLI Internal State ```r # Check current CLI state cli::cli_status() #> Shows active containers, progress bars, themes # Debug theme application options(cli.debug = TRUE) cli_alert("Test") #> Shows theme resolution and style application ``` ### Troubleshooting Techniques **Colors not appearing:** ```r # Check color support cli::num_ansi_colors() #> Should be > 1 # Force colors on options(cli.num_colors = 256) # Check if output is to terminal isatty(stdout()) #> Should be TRUE for interactive colors ``` **Hyperlinks not working:** ```r # Check hyperlink support cli::style_hyperlink_supported() # Force enable for testing options(cli.hyperlink = TRUE) # Test with explicit hyperlink writeLines(style_hyperlink("test", "https://example.com")) ``` **Unicode symbols showing as boxes:** ```r # Check Unicode support l10n_info()$`UTF-8` #> Should be TRUE # Force ASCII fallback options(cli.unicode = FALSE) # Or use environment variable Sys.setenv(CLI_NO_UNICODE = "true") ``` **Progress bars not updating:** ```r # Check if output is buffered # Progress requires unbuffered output flush.console() # Force output after updates # Check for interactive mode interactive() #> Should be TRUE for dynamic progress # Test with forced dynamic mode options(cli.dynamic = TRUE) ``` **Debugging custom themes:** ```r # Validate theme structure my_theme <- list( span.error = list(color = "red", "font-weight" = "bold") ) cli_div(theme = my_theme) cli_text("Test {.error message}") # Check resolved styles options(cli.debug = TRUE) # Shows which selectors match and final styles ``` -
conditions.md 18.4 KB
# CLI Conditions Reference ## Table of Contents 1. [CLI Conditions Overview](#cli-conditions-overview) 2. [Error Design Principles](#error-design-principles) 3. [cli_abort() Deep Dive](#cli_abort-deep-dive) 4. [cli_warn() Patterns](#cli_warn-patterns) 5. [cli_inform() Patterns](#cli_inform-patterns) 6. [Testing CLI Conditions](#testing-cli-conditions) 7. [Migration Guide](#migration-guide) 8. [Real-World Examples](#real-world-examples) 9. [Anti-Patterns](#anti-patterns) ## CLI Conditions Overview CLI conditions (cli_abort(), cli_warn(), cli_inform()) provide formatted alternatives to base R's stop(), warning(), and message(). They offer: **Key Benefits:** - **Inline markup** - Format code, paths, variables, and values with semantic meaning - **Structured output** - Use bullet lists to organize problem statements, context, and solutions - **Automatic styling** - Colors, icons, and formatting are applied consistently - **Better readability** - Multi-line messages are easier to scan and understand - **rlang integration** - Seamless integration with structured error handling via rlang **When to Use CLI Conditions:** ```r # Use cli_abort() for errors that stop execution cli_abort("Cannot proceed: {.file {path}} is missing") # Use cli_warn() for warnings about potential issues cli_warn("Column {.field {col}} has {n} missing value{?s}") # Use cli_inform() for informative messages cli_inform("Successfully processed {n} record{?s}") ``` ## Error Design Principles Good error messages follow these principles: ### 1. Clear Problem Statement State what went wrong in plain language: ```r # Bad - Technical jargon cli_abort("NULL pointer in slot `data`") # Good - Clear statement cli_abort("Dataset is missing") ``` ### 2. Actionable Solutions Tell users how to fix the problem: ```r validate_email <- function(email) { if (!grepl("@", email)) { cli_abort(c( "Invalid email address", "x" = "{.val {email}} is not a valid email", "i" = "Email must contain an @ symbol" )) } } ``` ### 3. Context via Bullet Lists Use bullets to structure information hierarchically: ```r check_dimensions <- function(x, y) { if (length(x) != length(y)) { cli_abort(c( "Incompatible vector lengths", "x" = "{.arg x} has length {length(x)}", "x" = "{.arg y} has length {length(y)}", "i" = "Both vectors must have the same length" )) } } ``` ### 4. Caller Information Use the `call` argument to show where the error occurred: ```r # Default - shows the function where cli_abort() is called validate <- function(x) { cli_abort("Invalid input") # Error in: validate(x) } # Explicit - control the call shown in error validate <- function(x) { cli_abort("Invalid input", call = caller_env()) } # Suppress - don't show any call validate <- function(x) { cli_abort("Invalid input", call = NULL) } ``` ## cli_abort() Deep Dive ### Basic Usage ```r # Simple message cli_abort("Something went wrong") # With inline markup cli_abort("Cannot find file {.file {path}}") # With multiple elements cli_abort(c( "Operation failed", "i" = "Additional context here" )) ``` ### Bullet Types Each bullet type has semantic meaning and visual styling: **`"x"` - Error/Problem (red X):** ```r cli_abort(c( "Validation failed", "x" = "File {.file data.csv} does not exist", "x" = "Directory {.path /tmp/output} is not writable" )) ``` **`"i"` - Information (blue i):** ```r cli_abort(c( "Invalid argument type", "x" = "{.arg x} must be numeric", "i" = "You supplied a {.cls {class(x)}} object", "i" = "Use {.fn as.numeric} to convert" )) ``` **`"v"` - Success context (green checkmark):** ```r cli_abort(c( "Partial operation failure", "v" = "Successfully processed {n_success} file{?s}", "x" = "Failed to process {n_failed} file{?s}", "i" = "See {.file error.log} for details" )) ``` **`"*"` - Bullet point:** ```r cli_abort(c( "Invalid configuration", "x" = "Missing required fields in config file", "i" = "Required fields:", "*" = "{.field name}", "*" = "{.field version}", "*" = "{.field author}" )) ``` **`">"` - Arrow/Pointer:** ```r cli_abort(c( "Database connection failed", "x" = "Cannot connect to {.val {host}}:{.val {port}}", "i" = "Troubleshooting steps:", ">" = "Check that the server is running", ">" = "Verify credentials in {.file .env}", ">" = "Ensure firewall allows port {.val {port}}" )) ``` ### Named vs Unnamed Elements **Unnamed elements** are treated as headers or main messages: ```r cli_abort(c( "This is the main error message", "This is a second header line" )) ``` **Named elements** get bullet icons: ```r cli_abort(c( "Main message", "x" = "Problem description", "i" = "Helpful information" )) ``` ### rlang Integration CLI works seamlessly with rlang's structured error handling: **Error Classes:** ```r validate_user <- function(user) { if (is.null(user$id)) { cli_abort( "User ID is required", class = "validation_error" ) } } # Catch specific error class tryCatch( validate_user(list()), validation_error = function(e) { # Handle validation errors specifically } ) ``` **Parent Errors (Error Chaining):** ```r load_data <- function(path) { tryCatch( read.csv(path), error = function(e) { cli_abort( c( "Failed to load data", "i" = "Attempted to read from {.file {path}}" ), parent = e ) } ) } ``` **Multiple Error Classes:** ```r cli_abort( "Invalid input", class = c("invalid_input", "user_error") ) ``` ### Call Specification Patterns **Pattern 1: Default behavior (show internal function):** ```r helper <- function(x) { cli_abort("Invalid x") } my_function <- function(x) { helper(x) } # Error: in `helper()` my_function(NULL) ``` **Pattern 2: Show caller's context:** ```r helper <- function(x, call = caller_env()) { cli_abort("Invalid x", call = call) } my_function <- function(x) { helper(x) } # Error: in `my_function()` my_function(NULL) ``` **Pattern 3: Suppress call entirely:** ```r helper <- function(x) { cli_abort("Invalid x", call = NULL) } # Error: (no function context shown) helper(NULL) ``` **Pattern 4: Custom call:** ```r validate <- function(x) { cli_abort("Invalid", call = quote(custom_function())) } # Error: in `custom_function()` ``` ### Interpolation and Evaluation CLI evaluates expressions in the calling environment: ```r check_file <- function(path) { size <- file.size(path) cli_abort(c( "File too large", "x" = "{.file {path}} is {size} bytes", "i" = "Maximum size is {.val {1e6}} bytes" )) } ``` **Escaping braces:** ```r # Use double braces to show literal braces cli_abort("Use {{variable}} syntax in glue") #> Error: Use {variable} syntax in glue ``` ## cli_warn() Patterns Warnings indicate potential problems that don't stop execution: ### Basic Warnings ```r # Simple warning cli_warn("Deprecated function") # With context cli_warn(c( "Deprecated function", "!" = "{.fn old_function} is deprecated", "i" = "Use {.fn new_function} instead" )) ``` ### Deprecation Warnings ```r old_function <- function(x) { cli_warn(c( "{.fn old_function} is deprecated", "i" = "Use {.fn new_function} instead", "i" = "See {.url https://example.com/migration} for migration guide" )) # Function implementation } ``` ### Data Quality Warnings ```r clean_data <- function(data) { missing_counts <- sapply(data, function(x) sum(is.na(x))) cols_with_missing <- names(missing_counts[missing_counts > 0]) if (length(cols_with_missing) > 0) { cli_warn(c( "Missing values detected", "!" = "Column{?s} with missing values: {.field {cols_with_missing}}", "i" = "Consider using {.fn tidyr::drop_na} or {.fn tidyr::fill}" )) } data } ``` ### Configuration Warnings ```r load_config <- function(path) { config <- read_config(path) if (is.null(config$timeout)) { cli_warn(c( "Missing configuration value", "!" = "{.field timeout} not specified in {.file {path}}", "i" = "Using default value of {.val 30} seconds" )) config$timeout <- 30 } config } ``` ### Once Per Session Warnings ```r experimental_feature <- function() { cli_warn( c( "Experimental feature", "!" = "This function is experimental and may change", "i" = "Use at your own risk" ), .frequency = "once", .frequency_id = "experimental_feature_warning" ) # Implementation } ``` ## cli_inform() Patterns Informative messages provide feedback without indicating problems: ### Progress Updates ```r process_data <- function(data) { cli_inform("Starting data processing") # Processing steps... cli_inform(c( "v" = "Successfully processed {nrow(data)} row{?s}", "i" = "Output saved to {.file results.csv}" )) } ``` ### Startup Messages ```r .onAttach <- function(libname, pkgname) { cli_inform(c( "v" = "Loaded {.pkg mypackage} version {packageVersion('mypackage')}", "i" = "Use {.fn ?mypackage} for help" )) } ``` ### Verbose Mode Information ```r analyze <- function(data, verbose = TRUE) { if (verbose) { cli_inform("Analyzing {nrow(data)} observation{?s}") } result <- expensive_computation(data) if (verbose) { cli_inform(c( "v" = "Analysis complete", "i" = "Found {result$n_groups} group{?s}", "i" = "Mean value: {.val {round(result$mean, 2)}}" )) } result } ``` ### Informative vs Progress **Use cli_inform() for:** - One-time status updates - Package startup messages - Final results or summaries - Debug/verbose output **Use cli_progress_*() for:** - Loops or iterations - Long-running operations - Operations with known total count - Real-time progress tracking ```r # Good - use inform for one-time messages process <- function(data) { cli_inform("Preprocessing data") data <- preprocess(data) # Good - use progress for iteration cli_progress_bar("Processing rows", total = nrow(data)) for (i in seq_len(nrow(data))) { process_row(data[i, ]) cli_progress_update() } cli_inform("v" = "Processing complete") } ``` ## Testing CLI Conditions ### Snapshot Testing Use testthat's snapshot tests to verify condition messages: ```r test_that("validation errors are clear", { expect_snapshot(error = TRUE, { validate_email("") validate_email("not-an-email") validate_email("user@example.com@extra") }) }) ``` Snapshot file (`tests/testthat/_snaps/validation.md`): ```md # validation errors are clear Code validate_email("") Error <rlang_error> Invalid email address x "" is not a valid email i Email must contain an @ symbol Code validate_email("not-an-email") Error <rlang_error> Invalid email address x "not-an-email" is not a valid email i Email must contain an @ symbol ``` ### Testing Bullet Formatting ```r test_that("error messages show proper context", { expect_snapshot(error = TRUE, { check_dimensions(1:3, 1:5) }) }) ``` ### Testing Pluralization ```r test_that("pluralization works in errors", { expect_snapshot(error = TRUE, { report_missing(c("file1.txt")) # singular report_missing(c("file1.txt", "file2.txt")) # plural }) }) ``` ### Testing Warning Frequency ```r test_that("deprecation warning shown once per session", { # Clear warning registry assign("last_shown", NULL, envir = rlang::ns_env("cli")) # First call shows warning expect_warning(old_function(1), "deprecated") # Second call does not (with frequency = "once") expect_no_warning(old_function(2)) }) ``` ### Testing Condition Classes ```r test_that("errors have correct classes", { expect_error( validate_user(list()), class = "validation_error" ) err <- tryCatch( validate_user(list()), error = function(e) e ) expect_s3_class(err, c("validation_error", "rlang_error")) }) ``` ### Mocking for Error Testing ```r test_that("handles missing file gracefully", { local_mocked_bindings( file.exists = function(path) FALSE ) expect_snapshot(error = TRUE, { load_dataset("missing.csv") }) }) ``` ## Migration Guide ### Base R to CLI: stop() to cli_abort() **Before:** ```r validate <- function(x, y) { if (!is.numeric(x)) { stop("x must be numeric") } if (length(x) != length(y)) { stop("x and y must have the same length") } } ``` **After:** ```r validate <- function(x, y) { if (!is.numeric(x)) { cli_abort(c( "{.arg x} must be numeric", "x" = "You supplied a {.cls {class(x)}} object" )) } 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)}" )) } } ``` ### Base R to CLI: warning() to cli_warn() **Before:** ```r process <- function(data) { if (any(is.na(data))) { warning("Data contains missing values") } } ``` **After:** ```r process <- function(data) { if (any(is.na(data))) { n_missing <- sum(is.na(data)) cli_warn(c( "Data contains missing values", "!" = "Found {n_missing} missing value{?s}", "i" = "Consider imputation or removal" )) } } ``` ### Base R to CLI: message() to cli_inform() **Before:** ```r load_data <- function(path) { message("Loading data from ", path) data <- read.csv(path) message("Loaded ", nrow(data), " rows") data } ``` **After:** ```r load_data <- function(path) { cli_inform("Loading data from {.file {path}}") data <- read.csv(path) cli_inform("v" = "Loaded {nrow(data)} row{?s}") data } ``` ### sprintf() to Inline Markup **Before:** ```r stop(sprintf( "File '%s' not found. Expected path: %s", basename(path), dirname(path) )) ``` **After:** ```r cli_abort(c( "File not found", "x" = "Cannot find {.file {basename(path)}}", "i" = "Expected location: {.path {dirname(path)}}" )) ``` ### paste() Concatenation to Glue Syntax **Before:** ```r msg <- paste0( "Processing ", n, " files", if (n > 1) "s" else "", " from ", dirname ) message(msg) ``` **After:** ```r cli_inform("Processing {n} file{?s} from {.path {dirname}}") ``` ## Real-World Examples ### usethis-Style Error Messages The usethis package provides excellent examples of clear, actionable errors: ```r use_github <- function() { if (!uses_git()) { cli_abort(c( "Cannot use GitHub without Git", "x" = "This project is not a Git repository", "i" = "Use {.fn usethis::use_git} to initialize Git first" )) } if (is.null(github_token())) { cli_abort(c( "GitHub token not found", "x" = "No GitHub personal access token (PAT) found", "i" = "Create a token at {.url https://github.com/settings/tokens}", "i" = "Store it with {.fn gitcreds::gitcreds_set}" )) } # Implementation } ``` ### devtools-Style Validation ```r check_package <- function(path = ".") { errors <- character() warnings <- character() # Collect issues if (!file.exists(file.path(path, "DESCRIPTION"))) { errors <- c(errors, "Missing {.file DESCRIPTION} file") } if (!file.exists(file.path(path, "NAMESPACE"))) { warnings <- c(warnings, "Missing {.file NAMESPACE} file") } # Report if (length(errors) > 0) { cli_abort(c( "Package structure invalid", set_names(errors, rep("x", length(errors))), "i" = "Use {.fn usethis::create_package} to create proper structure" )) } if (length(warnings) > 0) { cli_warn(c( "Package structure issues", set_names(warnings, rep("!", length(warnings))) )) } } ``` ### Database Connection with Rich Context ```r connect_db <- function(host, port, database, user, password) { tryCatch( { conn <- DBI::dbConnect( RPostgres::Postgres(), host = host, port = port, dbname = database, user = user, password = password ) cli_inform("v" = "Connected to {.field {database}} at {.val {host}}") conn }, error = function(e) { cli_abort( c( "Database connection failed", "x" = "Cannot connect to {.val {host}}:{.val {port}}", "i" = "Connection details:", "*" = "Host: {.val {host}}", "*" = "Port: {.val {port}}", "*" = "Database: {.val {database}}", "*" = "User: {.val {user}}", "i" = "Troubleshooting:", ">" = "Verify server is running", ">" = "Check firewall settings", ">" = "Confirm credentials" ), parent = e ) } ) } ``` ## Anti-Patterns ### Don't Mix Base R and CLI **Bad:** ```r validate <- function(x) { if (!is.numeric(x)) { stop("x must be numeric") # base R } if (length(x) == 0) { cli_abort("{.arg x} cannot be empty") # cli } } ``` **Good:** ```r validate <- function(x) { if (!is.numeric(x)) { cli_abort("{.arg x} must be numeric") } if (length(x) == 0) { cli_abort("{.arg x} cannot be empty") } } ``` ### Don't Overuse Bullets **Bad - Too many bullets:** ```r cli_abort(c( "Error", "x" = "Problem 1", "i" = "Info 1", "x" = "Problem 2", "i" = "Info 2", "x" = "Problem 3", "i" = "Info 3", "x" = "Problem 4" # ... too much information )) ``` **Good - Focused message:** ```r cli_abort(c( "Validation failed", "x" = "Found {n_errors} error{?s} in configuration", "i" = "See {.file validation.log} for details" )) ``` ### Don't Repeat Information **Bad:** ```r cli_abort(c( "File data.csv not found", "x" = "Cannot read data.csv", "i" = "The file data.csv does not exist" )) ``` **Good:** ```r cli_abort(c( "File not found", "x" = "Cannot read {.file data.csv}", "i" = "Check that the file exists in the working directory" )) ``` ### Don't Use Technical Jargon **Bad:** ```r cli_abort("NULL pointer in slot `data` of S4 object") ``` **Good:** ```r cli_abort(c( "Dataset is missing", "x" = "The {.field data} slot is empty", "i" = "Use {.fn set_data} to provide a dataset" )) ``` ### Don't Forget Pluralization **Bad:** ```r cli_inform("Found {n} files") # "Found 1 files" looks wrong ``` **Good:** ```r cli_inform("Found {n} file{?s}") # "Found 1 file", "Found 2 files" ``` ### Don't Use Bare Errors in Package Code **Bad:** ```r # Package function with no context compute <- function(x) { cli_abort("Invalid input") # Which function failed? What's invalid? } ``` **Good:** ```r compute <- function(x) { if (!is.numeric(x)) { cli_abort( c( "{.arg x} must be numeric", "x" = "You supplied a {.cls {class(x)}} object" ), call = caller_env() # Show caller's context ) } } ``` -
inline-markup.md 19.3 KB
# CLI Inline Markup Reference ## Table of Contents 1. [Introduction](#introduction) 2. [Basic Syntax](#basic-syntax) 3. [Code & Syntax Classes](#code--syntax-classes) 4. [Files & Paths Classes](#files--paths-classes) 5. [Communication Classes](#communication-classes) 6. [Values & Data Classes](#values--data-classes) 7. [Emphasis Classes](#emphasis-classes) 8. [Documentation Classes](#documentation-classes) 9. [Special Classes](#special-classes) 10. [Vector Collapsing](#vector-collapsing) 11. [Advanced Patterns](#advanced-patterns) 12. [Pluralization](#pluralization) 13. [Performance Considerations](#performance-considerations) 14. [Quick Reference Table](#quick-reference-table) ## Introduction Read this file when you need to: - Look up the correct inline markup class for specific content types - Understand how to nest and combine markup classes - Learn vector collapsing behavior and customization - Master pluralization patterns beyond basic `{?s}` - Optimize performance for high-frequency messaging - Troubleshoot unexpected formatting behavior Inline markup classes format text within cli messages using `{.class content}` syntax. They integrate with glue string interpolation and work across all cli functions: `cli_text()`, `cli_abort()`, `cli_warn()`, `cli_inform()`, `cli_alert_*()`, etc. ## Basic Syntax Inline markup uses curly braces with a period-prefixed class name: ```r cli_text("Function {.fn mean} calculates {.field average}") #> Function `mean()` calculates average ``` **Key syntax rules:** - Format: `{.class content}` - Content is interpolated if it contains `{}` - Double braces `{{` and `}}` escape literal braces - Whitespace inside `{}` is preserved in output - Classes can be nested (see [Advanced Patterns](#advanced-patterns)) ## Code & Syntax Classes ### .code Generic code formatting for expressions, statements, or syntax: ```r cli_text("Use {.code sum(x, na.rm = TRUE)} to ignore NA values") cli_text("Set {.code options(width = 120)} in your .Rprofile") cli_text("The {.code return()} statement exits early") ``` **When to use:** - Multi-token code expressions - Syntax examples - Configuration snippets - When other code classes are too specific ### .fn and .fun Function names with automatic parentheses: ```r cli_text("Call {.fn mean} to calculate average") #> Call `mean()` to calculate average cli_text("Use {.fun base::mean} for namespace clarity") #> Use `base::mean()` for namespace clarity ``` **Automatic formatting:** - Adds `()` suffix automatically - Supports `package::function` notation - Both `.fn` and `.fun` are equivalent **When to use:** - Referring to functions by name - Suggesting which function to call - Error messages about function usage ### .arg Function argument names: ```r cli_abort(c( "{.arg x} must be numeric", "i" = "Set {.arg na.rm = TRUE} to handle missing values" )) ``` **When to use:** - Parameter validation errors - Documenting function arguments - Explaining argument behavior ### .cls S3/S4/R6 class names: ```r cli_abort("Expected {.cls data.frame}, got {.cls {class(x)}}") cli_text("Object is {.cls tbl_df}, a tibble subclass") ``` **When to use:** - Type checking errors - Documenting expected types - Explaining inheritance ### .type Broader type descriptions (base types, generic categories): ```r cli_abort("Input must be {.type integer}, not {.type {typeof(x)}}") cli_text("Coercing from {.type character} to {.type numeric}") ``` **When to use:** - Base R types: integer, double, character, logical - Generic type categories - Type coercion messages ### .obj_type_friendly User-friendly type descriptions with indefinite articles: ```r x <- data.frame() cli_text("{.var x} must be a vector, not {.obj_type_friendly {x}}") #> `x` must be a vector, not a data frame y <- 1:10 cli_text("You provided {.obj_type_friendly {y}}") #> You provided an integer vector ``` **Automatic features:** - Adds "a"/"an" article automatically - Uses friendly names ("data frame" not "data.frame") - Handles pluralization for vectors ## Files & Paths Classes ### .file File names and paths with appropriate formatting: ```r cli_text("Reading {.file data/input.csv}") cli_warn("File {.file ~/.ssh/config} has insecure permissions") cli_inform("Created {.file output/results.xlsx}") ``` **Best practices:** - Use for any file reference - Works with relative and absolute paths - Handles home directory expansion ### .path Directory paths and file system locations: ```r cli_text("Installing to {.path /usr/local/lib/R}") cli_text("Working directory: {.path {getwd()}}") cli_abort("Directory {.path {dir}} does not exist") ``` **When to use .file vs .path:** - `.file` - Files, scripts, documents - `.path` - Directories, installation locations, system paths ## Communication Classes ### .email Email addresses: ```r cli_text("Contact maintainer at {.email user@example.com}") cli_inform("Send bug reports to {.email bugs@r-project.org}") ``` **Features:** - Creates `mailto:` links in supported terminals - Formatted distinctly from regular text ### .url Web URLs and URIs: ```r cli_text("Visit {.url https://cli.r-lib.org} for documentation") cli_text("API endpoint: {.url https://api.example.com/v1}") ``` **Features:** - Creates clickable hyperlinks in supported terminals - Formats protocol, domain, and path distinctly ### .href Custom hyperlinks with separate text and URL: ```r cli_text("See {.href [documentation](https://cli.r-lib.org)}") cli_text("Read {.href [vignette](vignette:cli::inline-markup)}") ``` **Syntax:** - Format: `{.href [text](url)}` - Markdown-style link syntax - Text and URL can be styled differently **Link types:** - HTTP/HTTPS: `https://example.com` - Help topics: `help:topic` - Vignettes: `vignette:package::topic` ## Values & Data Classes ### .val Data values, constants, and literals: ```r n <- 42 cli_text("Found {.val {n}} records") cli_text("Default timeout is {.val 30} seconds") cli_text("Status: {.val 'complete'}") ``` **Automatic formatting:** - Quoted for character values - Unquoted for numeric values - Handles vectors with collapsing ### .var Variable names in code or data: ```r cli_abort("Variable {.var x} must be numeric") cli_text("Column {.var age} contains missing values") cli_inform("Using {.var Sepal.Length} as predictor") ``` **When to use:** - Variable names in R code - Column names in data frames - Field names in objects ### .envvar Environment variable names: ```r cli_text("Set {.envvar R_LIBS_USER} to customize library location") cli_abort("{.envvar HOME} is not defined") cli_inform("Using {.envvar PATH}: {.val {Sys.getenv('PATH')}}") ``` **When to use:** - System environment variables - R-specific environment variables - Configuration via environment ### .field Object fields, slots, or attributes: ```r cli_text("The {.field name} field is required") cli_abort("Invalid {.field status} value") cli_text("Access {.field @data} slot in S4 object") ``` **When to use:** - Named list elements - Data frame columns (prefer `.var` for analysis context) - S4 slots - Object attributes ### .str String literals and text values: ```r cli_text("Message starts with {.str 'Error:'}") cli_text("Pattern {.str '^[0-9]+$'} matches digits") ``` **When to use:** - Literal string values - Pattern strings - Format strings - When `.val` formatting is too generic ## Emphasis Classes ### .emph Emphasis for important concepts or terms: ```r cli_text("This function is {.emph deprecated}") cli_text("The file is {.emph locked} by another process") cli_inform("{.emph Note}: This may take several minutes") ``` **Rendering:** - Typically italic or colored - Lighter emphasis than `.strong` ### .strong Strong emphasis for critical information: ```r cli_warn("{.strong Warning}: This action cannot be undone") cli_text("This parameter is {.strong required}") cli_abort("{.strong Error}: Database connection failed") ``` **Rendering:** - Typically bold or brightly colored - Stronger emphasis than `.emph` ## Documentation Classes ### .help R help topic references: ```r cli_text("See {.help stats::lm} for details") cli_inform("More info: {.help base::sum}") ``` **Features:** - Creates link to help topic in RStudio - Supports `package::topic` notation - Fallback to plain text in terminals ### .topic Generic topic or section references: ```r cli_text("See {.topic 'Error Handling'} section") cli_inform("Refer to {.topic 'Advanced Usage'}") ``` **When to use:** - Internal documentation sections - Vignette sections - General topic references ### .vignette Vignette references: ```r cli_text("Read {.vignette cli::semantic-cli} for examples") cli_inform("See {.vignette dplyr::programming}") ``` **Features:** - Links to package vignettes - Format: `package::vignette-name` ### .run Runnable R code examples: ```r cli_text("Try: {.run install.packages('cli')}") cli_inform("Debug with: {.run options(error = recover)}") ``` **Features:** - Creates executable link in RStudio - Click to run code in console - Formatted as code in terminals ## Special Classes ### .kbd Keyboard keys and shortcuts: ```r cli_text("Press {.kbd Ctrl+C} to cancel") cli_text("Use {.kbd Enter} to confirm") cli_inform("Save with {.kbd Cmd+S} (Mac) or {.kbd Ctrl+S} (Windows)") ``` **Rendering:** - Typically in keyboard key style - May show as boxed or distinct formatting ### .key Alternative to `.kbd` for key names: ```r cli_text("Press the {.key RETURN} key") cli_text("Hold {.key SHIFT} while clicking") ``` ### .or Logical OR separator for alternatives: ```r cli_text("Use {.val 'yes'} {.or} {.val 'no'}") cli_abort("Type must be {.val 'auto'} {.or} {.val 'manual'}") ``` **Rendering:** - Formats as " or " with appropriate styling - Maintains class formatting for surrounding elements ### .pkg Package names: ```r cli_text("Install {.pkg dplyr} for data manipulation") cli_inform("Loading {.pkg ggplot2}") cli_abort("{.pkg httr2} is required but not installed") ``` **Features:** - Distinct package name formatting - May include CRAN/GitHub links in supported environments ### .dt Definition term in definition lists: ```r cli_dl(c( "{.dt name}" = "User's full name", "{.dt email}" = "Contact email address" )) ``` **When to use:** - Definition list terms - Glossary entries - Key-value pair keys ### .dd Definition description in definition lists: ```r cli_dl(c( "name" = "{.dd User's full name}", "email" = "{.dd Contact email address}" )) ``` **When to use:** - Definition list descriptions - Glossary definitions - Key-value pair values ## Vector Collapsing Vectors are automatically collapsed with appropriate separators: ### Default Behavior ```r pkgs <- c("dplyr", "tidyr", "ggplot2") cli_text("Loading packages: {.pkg {pkgs}}") #> Loading packages: dplyr, tidyr, and ggplot2 files <- c("a.R", "b.R") cli_text("Modified: {.file {files}}") #> Modified: a.R and b.R single <- "data.csv" cli_text("Found: {.file {single}}") #> Found: data.csv ``` **Rules:** - Length 1: no separators - Length 2: " and " separator - Length 3+: ", " separators with " and " before last item ### Collapsing Empty Vectors ```r empty <- character() cli_text("Files: {.file {empty}}") #> Files: cli_text("Files: {?none/one/some}: {.file {empty}}") #> Files: none: ``` ### Custom Collapse Separators Control collapsing with glue transformers: ```r items <- c("apple", "banana", "cherry") # Custom separator cli_text("Items: {.val {items}}", .transformer = function(text, envir) { glue::glue_collapse(text, sep = " | ", last = " | ") }) #> Items: 'apple' | 'banana' | 'cherry' # OR separator cli_text("Choose: {.val {items}}", .transformer = function(text, envir) { glue::glue_collapse(text, sep = ", ", last = " or ") }) #> Choose: 'apple', 'banana' or 'cherry' ``` ### Truncating Long Vectors ```r many <- letters[1:20] cli_text("Variables: {.var {many}}") #> Variables: a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, and t # Truncate with custom transformer cli_text("Variables: {.var {head(many, 5)}} and {length(many) - 5} more") #> Variables: a, b, c, d, e and 15 more ``` ## Advanced Patterns ### Nested Markup Classes can be nested for combined formatting: ```r cli_text("Call {.fn {.pkg dplyr}::filter} to subset data") cli_abort("File {.file {.path {dir}}/data.csv} not found") cli_text("Set {.arg timeout} to {.val {default_timeout}} by default") ``` **Nesting guidelines:** - Outer class determines primary formatting - Inner classes add semantic meaning - Keep nesting depth reasonable (2-3 levels max) ### Combining with Glue Expressions ```r n <- 5 cli_text("Processing {n} file{?s} from {.path {getwd()}}") status <- "complete" cli_inform("Status: {.val {toupper(status)}}") pkg <- "dplyr" cli_text("Version: {.pkg {pkg}} {.val {packageVersion(pkg)}}") ``` ### Custom Glue Transformers Full control over formatting with transformers: ```r # Highlight differences cli_text( "Expected {.val {expected}}, got {.val {actual}}", .transformer = function(text, envir) { # Custom logic here text } ) # Add prefixes my_transformer <- function(text, envir) { if (startsWith(text, ".pkg")) { paste0("R package: ", glue::glue(text, .envir = envir)) } else { glue::identity_transformer(text, envir) } } cli_text("Install {.pkg cli}", .transformer = my_transformer) ``` ### Conditional Formatting ```r status <- "error" cli_text("Status: {.{if(status == 'error') 'strong' else 'emph'} {status}}") type <- class(x) cli_text("Type: {if(is.numeric(x)) .val else .cls} {type}") ``` ### Multiple Classes on Same Content Combine semantic classes: ```r # Package function cli_text("{.fn {.pkg stats}::median}") # File in path cli_text("{.file {.path /usr/local}/script.R}") # Variable value cli_text("{.var x} = {.val {x}}") ``` ### Working with Lists ```r config <- list(host = "localhost", port = 8080, ssl = TRUE) cli_dl(c( "{.field host}" = "{.val {config$host}}", "{.field port}" = "{.val {config$port}}", "{.field ssl}" = "{.val {config$ssl}}" )) ``` ### Escaping Markup Prevent interpretation with double braces: ```r cli_text("In glue, use {{variable}} syntax") #> In glue, use {variable} syntax cli_text("Literal: {{.code not_markup}}") #> Literal: {.code not_markup} ``` ## Pluralization Pluralization adapts text based on quantities using `{?}` syntax. ### Single Alternative Pattern Add "s" for plural: ```r n <- 1 cli_text("{n} file{?s} found") #> 1 file found n <- 5 cli_text("{n} file{?s} found") #> 5 files found ``` ### Two Alternative Pattern Specify singular/plural forms: ```r n <- 1 cli_text("{n} director{?y/ies}") #> 1 directory n <- 3 cli_text("{n} director{?y/ies}") #> 3 directories ``` **Common patterns:** - `{?y/ies}` - directory/directories - `{?/s}` - item/items - `{?is/are}` - is/are - `{?/es}` - box/boxes - `{?ex/ices}` - index/indices ### Three Alternative Pattern Handle zero/one/many: ```r n <- 0 cli_text("{?No/One/Some} file{?s} {?is/is/are} ready") #> No files are ready n <- 1 cli_text("{?No/One/Some} file{?s} {?is/is/are} ready") #> One file is ready n <- 5 cli_text("{?No/One/Some} file{?s} {?is/is/are} ready") #> Some files are ready ``` ### Setting Quantity with qty() Control pluralization explicitly: ```r updated <- 3 total <- 10 cli_text("{updated}/{total} {qty(updated)} file{?s} {?needs/need} update{?s}") #> 3/10 files need updates ``` **When to use qty():** - When quantity appears elsewhere - Complex expressions with multiple numbers - Explicit pluralization control ### no() Helper Display "no" instead of 0: ```r n <- 0 cli_text("Found {no(n)} error{?s}") #> Found no errors n <- 3 cli_text("Found {no(n)} error{?s}") #> Found 3 errors ``` ### Advanced Pluralization Patterns **Multiple quantities in one message:** ```r nerr <- 2 nwarn <- 1 cli_text( "{nerr} error{?s} and {nwarn} {qty(nwarn)} warning{?s} found" ) #> 2 errors and 1 warning found ``` **Conditional articles:** ```r n <- 1 cli_text("Found {?a /}{n} file{?s}") #> Found a 1 file n <- 5 cli_text("Found {?a /}{n} file{?s}") #> Found 5 files ``` **Complex pluralization:** ```r nfile <- 3 ndir <- 1 cli_text( "{nfile} file{?s} in {ndir} {qty(ndir)} director{?y/ies}" ) #> 3 files in 1 directory ``` **Verb agreement:** ```r n <- 1 cli_text("File {?was/were} modified") #> File was modified n <- 3 cli_text("Files {?was/were} modified") #> Files were modified ``` **Possessives:** ```r n <- 1 cli_text("User{?'s/'s'} setting{?s}") #> User's setting n <- 3 cli_text("Users{?'s/'s'} settings") #> Users' settings ``` ## Performance Considerations ### High-Frequency Messages For loops with many iterations: ```r # Expensive - creates cli context each iteration for (i in 1:10000) { cli_text("Processing {.val {i}}") } # Better - batch messages if (i %% 1000 == 0) { cli_text("Processed {.val {i}} items") } # Best - use progress bar cli_progress_bar("Processing", total = 10000) for (i in 1:10000) { # work here cli_progress_update() } ``` ### Expensive Interpolation Avoid expensive computations in markup: ```r # Expensive - computes every time cli_text("Processing {.file {slow_path_computation()}}") # Better - compute once path <- slow_path_computation() cli_text("Processing {.file {path}}") ``` ### Conditional Messages Use conditions to avoid unnecessary formatting: ```r # Inefficient if (verbose) cli_text("Status: {.val {expensive_status_check()}}") # Better if (verbose) { status <- expensive_status_check() cli_text("Status: {.val {status}}") } ``` ### Large Vectors Truncate large vectors before formatting: ```r # Problematic with huge vectors vars <- names(huge_dataframe) cli_text("Variables: {.var {vars}}") # Better n_vars <- length(vars) if (n_vars > 10) { cli_text("Variables: {.var {head(vars, 10)}} and {n_vars - 10} more") } else { cli_text("Variables: {.var {vars}}") } ``` ## Quick Reference Table | Class | Use For | Example | Output | |-------|---------|---------|--------| | `.code` | Code expressions | `{.code sum(x)}` | `sum(x)` | | `.fn`, `.fun` | Functions | `{.fn mean}` | `mean()` | | `.arg` | Function arguments | `{.arg na.rm}` | `na.rm` | | `.cls` | Class names | `{.cls data.frame}` | data.frame | | `.type` | Base types | `{.type integer}` | integer | | `.obj_type_friendly` | Friendly types | `{.obj_type_friendly {x}}` | an integer vector | | `.file` | File names | `{.file script.R}` | script.R | | `.path` | Directory paths | `{.path /usr/local}` | /usr/local | | `.email` | Email addresses | `{.email user@example.com}` | user@example.com | | `.url` | Web URLs | `{.url https://example.com}` | https://example.com | | `.href` | Custom links | `{.href [text](url)}` | text (linked) | | `.val` | Data values | `{.val {x}}` | 42 or 'text' | | `.var` | Variable names | `{.var column}` | column | | `.envvar` | Environment vars | `{.envvar PATH}` | PATH | | `.field` | Object fields | `{.field name}` | name | | `.str` | String literals | `{.str 'pattern'}` | 'pattern' | | `.emph` | Emphasis | `{.emph important}` | *important* | | `.strong` | Strong emphasis | `{.strong critical}` | **critical** | | `.help` | Help topics | `{.help stats::lm}` | stats::lm (linked) | | `.topic` | Topic references | `{.topic 'Intro'}` | 'Intro' | | `.vignette` | Vignettes | `{.vignette pkg::name}` | pkg::name (linked) | | `.run` | Runnable code | `{.run code}` | code (executable) | | `.kbd`, `.key` | Keyboard keys | `{.kbd Ctrl+C}` | Ctrl+C | | `.or` | Logical OR | `{.val x} {.or} {.val y}` | x or y | | `.pkg` | Package names | `{.pkg dplyr}` | dplyr | | `.dt` | Definition term | `{.dt term}` | term (in definition list) | | `.dd` | Definition desc | `{.dd description}` | description (in definition list) | -
progress.md 20.1 KB
# Progress Indicators ## Table of Contents 1. [Choosing Progress Style](#choosing-progress-style) 2. [cli_progress_bar() Deep Dive](#cli_progress_bar-deep-dive) 3. [cli_progress_step() Advanced](#cli_progress_step-advanced) 4. [cli_progress_message()](#cli_progress_message) 5. [Progress Variables Reference](#progress-variables-reference) 6. [Format Strings](#format-strings) 7. [Progress Styles](#progress-styles) 8. [Advanced Scenarios](#advanced-scenarios) 9. [Shiny Integration](#shiny-integration) 10. [C-Level Progress](#c-level-progress) 11. [Debugging Progress](#debugging-progress) 12. [Performance Considerations](#performance-considerations) ## Choosing Progress Style cli offers three progress indicator functions, each suited for different scenarios: ### cli_progress_bar() Use when: - You know the total number of iterations upfront - Progress can be measured as a percentage - Operation involves loops or iterating over collections - Users need ETA and completion estimates ```r # Good use case process_files <- function(files) { cli_progress_bar("Processing", total = length(files)) for (file in files) { process(file) cli_progress_update() } } ``` ### cli_progress_step() Use when: - Progress happens in discrete named steps - Total work is unknown or variable - Steps have different durations - You want automatic success/failure indicators ```r # Good use case deploy_app <- function() { cli_progress_step("Building assets") build_assets() cli_progress_step("Running tests") run_tests() cli_progress_step("Deploying to server") deploy() } ``` ### cli_progress_message() Use when: - Showing a simple status message - No quantifiable progress to track - Operation duration is uncertain - Want a spinner without step semantics ```r # Good use case cli_progress_message("Waiting for API response...") response <- long_running_api_call() ``` ## cli_progress_bar() Deep Dive ### Basic Parameters ```r cli_progress_bar( name = NULL, # Progress bar name/label status = NULL, # Additional status text type = "iterator", # Bar type: "iterator", "tasks", "download", "custom" total = NA, # Total number of items (NA for unknown) format = NULL, # Custom format string format_done = NULL, # Format when complete format_failed = NULL, # Format when failed clear = TRUE, # Clear bar when done current = TRUE, # Show current position auto_terminate = TRUE, # Auto-close when function exits .auto_close = TRUE # Deprecated, use auto_terminate ) ``` ### Type Parameter Examples ```r # Iterator (default) - for loops cli_progress_bar("Processing items", total = 100, type = "iterator") # Tasks - for discrete tasks cli_progress_bar("Completing tasks", total = 5, type = "tasks") # Download - for file downloads cli_progress_bar("Downloading", total = file_size, type = "download") # Custom - for user-defined formats cli_progress_bar( format = "Working {cli::pb_bar} {cli::pb_percent}", total = 100, type = "custom" ) ``` ### Auto-Termination Behavior Progress bars automatically close when the calling function exits: ```r process_data <- function(data) { cli_progress_bar("Processing", total = nrow(data)) for (i in seq_len(nrow(data))) { process_row(data[i, ]) cli_progress_update() } # Bar automatically closes here when function returns } # Manual control if needed process_data_manual <- function(data) { id <- cli_progress_bar("Processing", total = nrow(data)) for (i in seq_len(nrow(data))) { process_row(data[i, ]) cli_progress_update(id = id) } cli_progress_done(id = id) # Explicit close } ``` ### Clearing Behavior Control whether progress bars remain visible after completion: ```r # Clear after completion (default) cli_progress_bar("Working", total = 100, clear = TRUE) for (i in 1:100) { Sys.sleep(0.01) cli_progress_update() } # Bar disappears when done # Keep visible after completion cli_progress_bar("Working", total = 100, clear = FALSE) for (i in 1:100) { Sys.sleep(0.01) cli_progress_update() } # Bar remains showing 100% completion ``` ### Dynamic Updates Update progress with optional status messages: ```r process_files <- function(files) { cli_progress_bar("Processing files", total = length(files)) for (i in seq_along(files)) { file <- files[[i]] # Update with status cli_progress_update( status = sprintf("Current: %s", basename(file)) ) process_file(file) } } # Update multiple items at once cli_progress_update(inc = 5) # Increment by 5 cli_progress_update(set = 50) # Set to specific value ``` ## Format Strings ### Default Formats ```r # Iterator format (default) # "{cli::pb_spin} {cli::pb_name} {cli::pb_bar} {cli::pb_percent} | ETA: {cli::pb_eta}" # Download format # "Downloaded {cli::pb_current_bytes}/{cli::pb_total_bytes} ({cli::pb_rate_bytes}/s) | ETA: {cli::pb_eta}" # Tasks format # "{cli::pb_name} {cli::pb_current}/{cli::pb_total} | ETA: {cli::pb_eta}" ``` ### Custom Format Examples ```r # Simple percentage only cli_progress_bar( "Working", total = 100, format = "{cli::pb_name} {cli::pb_percent}" ) # With ETA and rate cli_progress_bar( "Processing", total = 1000, format = "{cli::pb_bar} {cli::pb_current}/{cli::pb_total} [{cli::pb_eta} @ {cli::pb_rate}]" ) # With elapsed time cli_progress_bar( "Running", total = 50, format = "{cli::pb_spin} {cli::pb_name} {cli::pb_percent} | Elapsed: {cli::pb_elapsed}" ) # Download-style with bytes cli_progress_bar( "Downloading data.zip", total = 1024^3, # 1 GB format = paste0( "{cli::pb_current_bytes}/{cli::pb_total_bytes} ", "({cli::pb_percent}) | ", "{cli::pb_rate_bytes}/s | ", "ETA: {cli::pb_eta}" ) ) # Custom status display cli_progress_bar( format = "{cli::pb_bar} {cli::pb_status}" ) ``` ### Completion and Failure Formats ```r cli_progress_bar( "Processing", total = 100, format = "Working: {cli::pb_bar} {cli::pb_percent}", format_done = "Completed {cli::pb_total} items in {cli::pb_elapsed}", format_failed = "Failed after processing {cli::pb_current} items" ) # Trigger failure for (i in 1:50) { if (i == 30) { cli_progress_done(result = "failed") break } cli_progress_update() } ``` ## Progress Variables Reference ### Basic Progress Variables ```r # Current position cli::pb_current # Current iteration number (e.g., 45) # Total work cli::pb_total # Total iterations (e.g., 100) # Percentage cli::pb_percent # Completion percentage (e.g., "45%") # Visual bar cli::pb_bar # Progress bar visualization (e.g., "=========> ") # Spinner cli::pb_spin # Animated spinner character # Name and status cli::pb_name # Progress bar name cli::pb_status # Current status message ``` ### Timing Variables ```r # Time elapsed cli::pb_elapsed # Time since start (e.g., "2m 30s") cli::pb_elapsed_raw # Elapsed time in seconds (e.g., 150.234) cli::pb_elapsed_clock # Clock time format (e.g., "02:30") # Time remaining cli::pb_eta # Estimated time remaining (e.g., "1m 15s") cli::pb_eta_raw # ETA in seconds (e.g., 75.0) cli::pb_eta_str # ETA as string (e.g., "ETA: 1m 15s") # Rate cli::pb_rate # Items per second (e.g., "0.6/s") cli::pb_rate_raw # Raw rate value (e.g., 0.6) ``` ### Byte-Based Variables For download/upload progress bars: ```r # Current bytes cli::pb_current_bytes # Formatted current (e.g., "45.2 MB") # Total bytes cli::pb_total_bytes # Formatted total (e.g., "100 MB") # Rate cli::pb_rate_bytes # Bytes per second (e.g., "2.1 MB/s") ``` ### Advanced Variables ```r # Tick information cli::pb_tick # Current tick number cli::pb_tick_rate # Ticks per second # Timestamps cli::pb_start # Start timestamp (POSIXct) cli::pb_timestamp # Current timestamp (POSIXct) # Extra data cli::pb_extra # User-defined extra data ``` ## cli_progress_step() Advanced ### Basic Usage ```r deploy_package <- function() { cli_progress_step("Checking package") check_result <- check_package() cli_progress_step("Building package") build_result <- build_package() cli_progress_step("Uploading to CRAN") upload_result <- upload_package() } ``` ### Spinner Customization ```r # Use different spinner style options(cli.spinner = "dots") cli_progress_step("Processing") # Available spinners options(cli.spinner = "line") # Classic line spinner options(cli.spinner = "dots") # Dots options(cli.spinner = "dots2") # Alternative dots options(cli.spinner = "dots3") # More dots options(cli.spinner = "dots12") # 12-frame dots options(cli.spinner = "arrow") # Rotating arrow options(cli.spinner = "bouncingBar") # Bouncing bar options(cli.spinner = "clock") # Clock hands # Custom spinner frames options(cli.spinner = list( interval = 100, frames = c("◐", "◓", "◑", "◒") )) ``` ### Multiple Concurrent Steps Progress steps can be nested: ```r process_projects <- function(projects) { cli_progress_step("Processing {length(projects)} projects") for (proj in projects) { cli_progress_step("Building {proj}") build_project(proj) cli_progress_step("Testing {proj}") test_project(proj) cli_progress_step("Deploying {proj}") deploy_project(proj) } } ``` ### Dynamic Message Updates Update step messages while they're running: ```r process_with_details <- function(files) { id <- cli_progress_step("Processing files") for (i in seq_along(files)) { cli_progress_update( id = id, status = sprintf("File %d/%d: %s", i, length(files), basename(files[i])) ) process_file(files[i]) } cli_progress_done(id = id) } ``` ### Success/Failure Termination ```r deploy_with_status <- function() { tryCatch({ cli_progress_step("Building application") build_app() cli_progress_step("Running tests") test_result <- run_tests() if (!test_result$passed) { cli_progress_done(result = "failed", msg_failed = "Tests failed!") return(FALSE) } cli_progress_step("Deploying") deploy() cli_progress_done(result = "done", msg_done = "Deployment successful!") TRUE }, error = function(e) { cli_progress_done(result = "failed", msg_failed = "Deployment failed: {e$message}") FALSE }) } ``` ### Custom Success/Failure Messages ```r id <- cli_progress_step( "Processing data", msg_done = "Data processed successfully", msg_failed = "Data processing failed" ) # Mark as done cli_progress_done(id = id, result = "done") # Or mark as failed cli_progress_done(id = id, result = "failed") ``` ## cli_progress_message() Simple progress messages with automatic spinners: ```r # Basic usage cli_progress_message("Loading configuration...") config <- load_config() # With explicit cleanup id <- cli_progress_message("Waiting for server...") wait_for_server() cli_progress_done(id = id) # Updating message id <- cli_progress_message("Connecting...") cli_progress_update(id = id, status = "Authenticating...") cli_progress_update(id = id, status = "Connected!") cli_progress_done(id = id) ``` ## Progress Styles cli includes several built-in progress bar styles: ```r # Classic style (default) options(cli.progress_bar_style = "classic") # Unicode style (requires Unicode support) options(cli.progress_bar_style = "unicode") # ASCII style (for limited terminals) options(cli.progress_bar_style = "ascii") # Custom style options(cli.progress_bar_style = list( complete = "=", incomplete = " ", current = ">", width = 40 )) ``` ## Advanced Scenarios ### Nested Progress Bars ```r process_datasets <- function(datasets) { cli_progress_bar("Processing datasets", total = length(datasets)) for (dataset in datasets) { # Outer progress updates cli_progress_update() # Inner progress bar n_rows <- nrow(dataset) cli_progress_bar("Processing rows", total = n_rows) for (i in seq_len(n_rows)) { process_row(dataset[i, ]) cli_progress_update() # Updates inner bar } # Inner bar auto-closes } # Outer bar auto-closes } ``` ### Progress with Parallel Code Progress bars in parallel contexts require special handling: ```r library(foreach) library(doParallel) # Not recommended - doesn't work well process_parallel_bad <- function(items) { cli_progress_bar("Processing", total = length(items)) foreach(item = items) %dopar% { result <- process_item(item) cli_progress_update() # Won't work across processes result } } # Better approach - update after each completion process_parallel_better <- function(items) { cli_progress_bar("Processing", total = length(items)) results <- foreach(item = items) %dopar% { process_item(item) } # Update in main thread for (i in seq_along(results)) { cli_progress_update() } results } # Best approach - use progress updates between parallel batches process_parallel_best <- function(items, batch_size = 10) { batches <- split(items, ceiling(seq_along(items) / batch_size)) cli_progress_bar("Processing", total = length(items)) results <- list() for (batch in batches) { batch_results <- foreach(item = batch) %dopar% { process_item(item) } results <- c(results, batch_results) cli_progress_update(inc = length(batch)) } results } ``` ### Progress in Loops vs Mapping ```r # Traditional loop with progress process_loop <- function(items) { cli_progress_bar("Processing", total = length(items)) results <- vector("list", length(items)) for (i in seq_along(items)) { results[[i]] <- process_item(items[[i]]) cli_progress_update() } results } # Using cli_progress_along() process_along <- function(items) { results <- lapply(cli_progress_along(items, "Processing"), function(i) { process_item(items[[i]]) }) results } # Using purrr with progress library(purrr) process_purrr <- function(items) { cli_progress_bar("Processing", total = length(items)) map(items, function(item) { result <- process_item(item) cli_progress_update() result }) } ``` ### Progress Output vs Regular CLI Output Progress bars interact with other cli output: ```r process_with_messages <- function(items) { cli_progress_bar("Processing", total = length(items)) for (i in seq_along(items)) { result <- process_item(items[[i]]) # Regular cli messages work alongside progress if (result$warnings) { cli_alert_warning("Item {i} had warnings") } cli_progress_update() } cli_alert_success("Processing complete") } # Progress-aware output functions process_with_progress_output <- function(items) { cli_progress_bar("Processing", total = length(items)) for (item in items) { # These respect progress bars cli_progress_output(cli_text("Processing {item}")) process_item(item) cli_progress_update() } } ``` ### Unknown Total Progress bars can run without knowing the total: ```r process_stream <- function(conn) { cli_progress_bar("Processing stream", total = NA) while (length(line <- readLines(conn, n = 1)) > 0) { process_line(line) cli_progress_update() } # Shows spinner and count, no percentage } # Update total when it becomes known process_dynamic <- function() { cli_progress_bar("Discovering files", total = NA) files <- find_files() # Update total cli_progress_update(total = length(files)) for (file in files) { process_file(file) cli_progress_update() } } ``` ## Shiny Integration Progress indicators in Shiny applications require special consideration: ```r library(shiny) ui <- fluidPage( actionButton("process", "Process Data") ) server <- function(input, output, session) { observeEvent(input$process, { # Use Shiny's progress API withProgress(message = "Processing", { # cli progress works but won't show in Shiny UI cli_progress_bar("Internal progress", total = 100) for (i in 1:100) { Sys.sleep(0.01) # Update Shiny progress incProgress(1/100) # cli progress (for logs) cli_progress_update() } }) }) } # Better: Use cli in Shiny with proper output server <- function(input, output, session) { observeEvent(input$process, { # Redirect cli output to console withProgress(message = "Processing", { # cli progress shows in R console, not Shiny UI process_data() }) }) } ``` ## C-Level Progress For package developers using C/C++: ```r # In R process_with_c <- function(data) { cli_progress_bar("Processing in C", total = nrow(data)) .Call(C_process_data, data, environment()) } # In C code (using cli's C API) # #include <cli/progress.h> # # SEXP C_process_data(SEXP data, SEXP progress_env) { # R_xlen_t n = Rf_xlength(data); # # for (R_xlen_t i = 0; i < n; i++) { # // Process data[i] # # // Update progress from C # cli_progress_update(progress_env); # } # # return R_NilValue; # } ``` Note: C-level progress requires the cli package C headers and proper linking. ## Debugging Progress ### Common Issues ```r # Issue: Progress bar not updating # Problem: Not calling cli_progress_update() for (i in 1:100) { process(i) # Missing: cli_progress_update() } # Issue: Multiple progress bars interfering # Solution: Store IDs and update correct bar id1 <- cli_progress_bar("Task 1", total = 10) id2 <- cli_progress_bar("Task 2", total = 20) cli_progress_update(id = id1) cli_progress_update(id = id2) # Issue: Progress bar not visible # Check: Is stderr redirected? Is output captured? # Progress bars use stderr by default # Issue: Progress bar flickers # Problem: Output mixing with progress # Solution: Use cli_progress_output() cli_progress_output(cli_alert_info("Status update")) ``` ### Testing Progress Bars ```r test_that("progress bar updates correctly", { # Mock progress to test without output mockery::stub(my_function, "cli_progress_bar", NULL) mockery::stub(my_function, "cli_progress_update", NULL) result <- my_function() expect_true(result) }) # Test with captured output test_that("progress messages are correct", { output <- capture.output({ process_data(test_data) }) # Note: Progress bars may not appear in captured output # Consider testing function logic separately }) ``` ### Disabling Progress ```r # Disable all progress indicators options(cli.progress_show_after = Inf) # Or use show_after parameter cli_progress_bar("Working", total = 100, show_after = Inf) # Conditional progress process_data <- function(data, verbose = TRUE) { if (verbose) { cli_progress_bar("Processing", total = nrow(data)) } for (i in seq_len(nrow(data))) { process_row(data[i, ]) if (verbose) cli_progress_update() } } ``` ## Performance Considerations ### Update Frequency ```r # Bad: Update too frequently (slows down loop) for (i in 1:1e6) { fast_operation(i) cli_progress_update() # 1 million updates! } # Better: Update periodically cli_progress_bar("Processing", total = 1e6) for (i in 1:1e6) { fast_operation(i) if (i %% 1000 == 0) cli_progress_update(set = i) } # Best: Use show_after to delay display cli_progress_bar( "Processing", total = 1e6, show_after = 2 # Only show if takes >2 seconds ) ``` ### Overhead Progress bars add minimal overhead, but consider: ```r # Negligible overhead for slow operations process_files <- function(files) { cli_progress_bar("Processing", total = length(files)) for (file in files) { slow_operation(file) # 1 second per file cli_progress_update() # <1ms overhead } } # Noticeable overhead for fast operations process_numbers <- function(n = 1e6) { cli_progress_bar("Processing", total = n) for (i in 1:n) { fast_operation(i) # 1μs per operation cli_progress_update() # 1ms overhead - 1000x slower! } } # Solution: Batch updates process_numbers_fast <- function(n = 1e6, update_every = 1000) { cli_progress_bar("Processing", total = n) for (i in 1:n) { fast_operation(i) if (i %% update_every == 0) { cli_progress_update(set = i) } } } ``` ### Terminal Performance ```r # Progress bars can be slow on Windows # Use ASCII style for better performance if (.Platform$OS.type == "windows") { options(cli.progress_bar_style = "ascii") } # Disable Unicode for remote sessions if (Sys.getenv("SSH_CONNECTION") != "") { options(cli.unicode = FALSE) } ``` -
themes.md 13.4 KB
# CLI Themes and Styling ## Table of Contents - [Theme Basics](#theme-basics) - [Container Functions](#container-functions) - [Selector Types](#selector-types) - [Theme Properties](#theme-properties) - [Built-in Themes](#built-in-themes) - [Custom Themes](#custom-themes) - [App and Package Themes](#app-and-package-themes) - [Color Palettes](#color-palettes) - [Accessibility](#accessibility) - [Debugging Themes](#debugging-themes) ## Theme Basics CLI uses a CSS-like theming system to style console output. Themes consist of selectors that match elements and properties that define their appearance. ### How Themes Work 1. Elements are identified by selectors (like `.alert-success` or `.code`) 2. Selectors are matched against the element hierarchy 3. Properties are applied to matched elements 4. Properties cascade through the element tree ### Basic Theme Structure ```r my_theme <- list( ".alert-success" = list( "color" = "green", "font-weight" = "bold" ), ".code" = list( "color" = "blue", "background-color" = "grey90" ) ) cli_div(theme = my_theme) cli_alert_success("Operation completed") cli_code("result <- compute()") cli_end() ``` ## Container Functions Containers create themed regions and manage element hierarchy. They auto-close when the function exits or can be closed explicitly with `cli_end()`. ### General Containers **`cli_div()`** - Generic container for applying themes: ```r cli_div(theme = list(".emph" = list(color = "red"))) cli_text("This is {.emph emphasized} text") cli_end() ``` With classes: ```r cli_div(class = "my-section", theme = list( ".my-section" = list("margin-left" = 2), ".my-section .code" = list(color = "blue") )) cli_text("Code: {.code mean(x)}") cli_end() ``` **`cli_par()`** - Paragraph container: ```r cli_par() cli_text("First line") cli_text("Second line") cli_end() ``` ### List Containers **`cli_ul()` / `cli_ol()` / `cli_dl()`** - List containers: ```r # Unordered list cli_ul() cli_li("First item") cli_li("Second item") cli_end() # Ordered list cli_ol() cli_li("Step one") cli_li("Step two") cli_end() # Definition list cli_dl() cli_li(c(term = "Definition of term")) cli_end() ``` ### Auto-closing Behavior Containers automatically close when the calling function exits: ```r my_function <- function() { cli_div(theme = list(".alert" = list(color = "red"))) cli_alert("Alert message") # No need to call cli_end() - auto-closes here } ``` Explicit closing with `cli_end()`: ```r id <- cli_div(theme = my_theme) cli_text("Themed content") cli_end(id) # Close specific container ``` ### Theme Scoping and Inheritance Themes inherit from parent containers and can be overridden: ```r # Outer theme cli_div(theme = list(".code" = list(color = "blue"))) # Inner theme overrides cli_div(theme = list(".code" = list(color = "red"))) cli_text("Code is {.code red} here") cli_end() cli_text("Code is {.code blue} here") cli_end() ``` ## Selector Types ### Simple Selectors Match elements by class: ```r list( ".code" = list(color = "blue"), # Matches {.code ...} ".file" = list(color = "magenta"), # Matches {.file ...} ".pkg" = list(color = "cyan") # Matches {.pkg ...} ) ``` ### Element Type Selectors Match by element type: ```r list( "h1" = list(color = "blue", "font-weight" = "bold"), "ul" = list("margin-left" = 2), "li" = list(before = "* ") ) ``` ### Descendant Selectors Match elements within other elements: ```r list( ".my-section .code" = list(color = "blue"), ".alert .emph" = list(color = "red") ) ``` ### Multiple Selectors Apply same styles to multiple selectors: ```r list( ".code, .fun, .fn" = list(color = "blue") ) ``` ### Pseudo-selectors Match specific states or positions: ```r list( "li:before" = list(content = "-> "), "ul li:first-child" = list("margin-top" = 0) ) ``` ## Theme Properties ### Color Properties **`color`** - Text color: ```r list(".alert" = list(color = "red")) ``` **`background-color`** - Background color: ```r list(".code" = list("background-color" = "grey90")) ``` Color formats: - Named colors: `"red"`, `"blue"`, `"green"` - ANSI colors: `"ansi_red"`, `"ansi_bright_blue"` - RGB hex: `"#FF5733"` - RGB function: `rgb(255, 87, 51)` ### Text Formatting **`font-weight`** - Text weight: ```r list(".strong" = list("font-weight" = "bold")) ``` **`font-style`** - Text style: ```r list(".emph" = list("font-style" = "italic")) ``` **`text-decoration`** - Text decoration: ```r list(".url" = list("text-decoration" = "underline")) ``` ### Spacing Properties **`margin-left`** - Left margin (in characters): ```r list(".par" = list("margin-left" = 2)) ``` **`margin-right`** - Right margin: ```r list(".par" = list("margin-right" = 2)) ``` **`margin-top`** - Top margin (in lines): ```r list("h1" = list("margin-top" = 1, "margin-bottom" = 1)) ``` **`padding-left`** - Left padding: ```r list(".alert" = list("padding-left" = 2)) ``` ### Content Properties **`before`** - Content before element: ```r list( ".alert-success:before" = list(content = "[OK] "), "ul li:before" = list(content = "* ") ) ``` **`after`** - Content after element: ```r list(".code:after" = list(content = " }")) ``` ### List Properties **`list-style-type`** - List marker style: ```r list( "ul" = list("list-style-type" = "bullet"), "ol" = list("list-style-type" = "decimal") ) ``` Values: `"bullet"`, `"circle"`, `"square"`, `"decimal"`, `"lower-alpha"`, `"upper-alpha"` **`start`** - Ordered list start number: ```r list("ol" = list(start = 5)) ``` ### Line Properties **`line-type`** - Line drawing style: ```r list(".rule" = list("line-type" = "double")) ``` Values: `"single"`, `"double"`, `"bar1"` through `"bar8"` ### Format Control **`fmt`** - Custom format function: ```r list( ".timestamp" = list( fmt = function(x) format(Sys.time(), "%Y-%m-%d %H:%M:%S") ) ) ``` **`transform`** - Transform function: ```r list( ".upper" = list(transform = toupper) ) ``` ## Built-in Themes ### Default Theme The standard cli theme with semantic colors and spacing: ```r # View built-in theme structure str(builtin_theme(), max.level = 2) ``` Key elements: - Blue for code, functions, arguments - Magenta for files and paths - Cyan for packages - Green for success - Red for errors - Yellow for warnings ### Simple Theme A minimal theme without colors: ```r options(cli.theme = simple_theme()) ``` Useful for: - Terminals without color support - Logging to files - Screen readers - Testing ### Dark Theme Optimized for dark terminal backgrounds (included in default theme with automatic detection). ## Custom Themes ### Creating a Custom Theme Build themes incrementally: ```r my_theme <- list( # Headers "h1" = list( color = "blue", "font-weight" = "bold", "margin-top" = 1, "margin-bottom" = 1, before = "== ", after = " ==" ), # Code elements ".code" = list( color = "cyan", "background-color" = "grey10" ), ".fn" = list( color = "blue", after = "()" ), # Alerts ".alert-success" = list( before = "[OK] ", color = "green" ), ".alert-danger" = list( before = "[ERROR] ", color = "red", "font-weight" = "bold" ), # Lists "ul li" = list( before = "• " ), "ol li" = list( "list-style-type" = "decimal" ) ) ``` ### Extending Built-in Themes Merge your theme with the built-in theme: ```r my_theme <- utils::modifyList( builtin_theme(), list( ".code" = list(color = "magenta"), ".custom" = list(color = "cyan") ) ) cli_div(theme = my_theme) ``` ### Theme Functions Create reusable theme functions: ```r create_brand_theme <- function(primary_color = "blue") { list( "h1" = list(color = primary_color, "font-weight" = "bold"), "h2" = list(color = primary_color), ".code" = list(color = primary_color), ".alert-success" = list(color = "green"), ".alert-danger" = list(color = "red") ) } cli_div(theme = create_brand_theme("purple")) ``` ## App and Package Themes ### Setting Package Theme Define a package-level theme in your `.onLoad()`: ```r .onLoad <- function(libname, pkgname) { my_theme <- list( ".code" = list(color = "blue"), ".pkg" = list(color = "cyan") ) options(cli.theme = my_theme) } ``` ### User Configuration Users can override package themes via options: ```r # In .Rprofile options(cli.theme = list( ".code" = list(color = "magenta") )) ``` ### Conditional Themes Apply themes based on environment: ```r .onLoad <- function(libname, pkgname) { theme <- if (cli::num_ansi_colors() >= 256) { rich_theme() # Full color theme } else { simple_theme() # Basic theme } options(cli.theme = theme) } ``` ### Theme Precedence Themes are applied in this order (highest to lowest): 1. Inline theme in `cli_div(theme = ...)` 2. User's `cli.theme` option 3. Package's default theme 4. Built-in theme ## Color Palettes ### Configuring Palettes Set the ANSI color palette with the `cli.palette` option: ```r options(cli.palette = "vscode") ``` ### Built-in Palettes **`dichro`** - Dichromat-friendly palette: ```r options(cli.palette = "dichro") ``` **`vscode`** - VS Code color scheme: ```r options(cli.palette = "vscode") ``` **`iterm`** - iTerm2 default colors: ```r options(cli.palette = "iterm") ``` ### Custom 16-Color Palettes Define custom ANSI colors: ```r my_palette <- c( # Normal colors (0-7) "#000000", # black "#CD0000", # red "#00CD00", # green "#CDCD00", # yellow "#0000EE", # blue "#CD00CD", # magenta "#00CDCD", # cyan "#E5E5E5", # white # Bright colors (8-15) "#7F7F7F", # bright black (grey) "#FF0000", # bright red "#00FF00", # bright green "#FFFF00", # bright yellow "#5C5CFF", # bright blue "#FF00FF", # bright magenta "#00FFFF", # bright cyan "#FFFFFF" # bright white ) options(cli.palette = my_palette) ``` ### Truecolor Support Check for truecolor support: ```r cli::num_ansi_colors() # Returns: # 1 - no color # 8 - 8 colors # 256 - 256 colors # 16777216 - truecolor (24-bit) ``` Use truecolor when available: ```r if (cli::num_ansi_colors() >= 16777216) { # Use RGB hex colors list(".code" = list(color = "#6A9FB5")) } else { # Fall back to named colors list(".code" = list(color = "blue")) } ``` ### Color Detection CLI automatically detects color support from: - `NO_COLOR` environment variable (disables color) - `TERM` environment variable - System capabilities - RStudio version Force color support: ```r options(cli.num_colors = 256) # Force 256 colors ``` Disable colors: ```r options(cli.num_colors = 1) # Or set environment variable Sys.setenv(NO_COLOR = "1") ``` ## Accessibility ### Color Contrast Ensure sufficient contrast for readability: ```r # Good contrast list( ".code" = list(color = "blue"), # Dark on light ".emph" = list(color = "ansi_red") # High contrast ) # Poor contrast (avoid) list( ".code" = list(color = "grey80"), # Low contrast on white ".emph" = list(color = "#EEEEEE") # Nearly invisible on white ) ``` ### Unicode Fallbacks Provide ASCII alternatives for Unicode symbols: ```r bullet <- if (cli::is_utf8_output()) "\u2022" else "*" list( "ul li:before" = list(content = bullet) ) ``` Built-in Unicode detection: ```r cli::is_utf8_output() # TRUE if UTF-8 is supported ``` ### Color-blind Friendly Themes Use the dichromat palette or ensure patterns work without color: ```r color_blind_theme <- list( ".alert-success" = list( before = "[OK] ", color = "green" ), ".alert-danger" = list( before = "[ERROR] ", color = "red", "font-weight" = "bold" ) ) ``` Benefits: - Symbols provide meaning without color - Bold text adds emphasis - Works for colorblind users ### Screen Reader Compatibility Keep semantic meaning in text, not just styling: ```r # Good: Meaning is in text cli_alert_success("File saved successfully") # Poor: Meaning only in style cli_text("{.green File saved}") ``` ## Debugging Themes ### Using cli_debug_doc() Visualize document structure and applied themes: ```r # Enable debug mode withr::local_options(cli.debug = TRUE) cli_div(class = "my-section") cli_h1("Header") cli_text("Text with {.code code}") cli_end() ``` Debug output shows: - Element hierarchy - Applied selectors - Computed properties - Theme inheritance ### Inspecting Theme Application Check which theme properties are applied: ```r # Create a test container cli_div(theme = my_theme, class = "test") # Debug output will show: # - Matched selectors # - Applied properties # - Inherited values cli_text("Test content") cli_end() ``` ### Theme Testing Strategy Test themes across different environments: ```r test_theme <- function(theme) { old_colors <- options(cli.num_colors = 256) on.exit(options(old_colors)) cli_div(theme = theme) cli_h1("Header") cli_alert_success("Success message") cli_text("Code: {.code mean(x)}") cli_end() } # Test with different color depths test_colors <- c(1, 8, 256, 16777216) for (n in test_colors) { options(cli.num_colors = n) test_theme(my_theme) } ``` ### Common Theme Issues **Colors not appearing:** - Check `cli::num_ansi_colors()` output - Verify terminal supports colors - Check for `NO_COLOR` environment variable **Spacing incorrect:** - Use `cli.debug = TRUE` to see computed margins - Check for inherited spacing properties - Verify units (characters vs. lines) **Selectors not matching:** - Use `cli.debug = TRUE` to see selector matching - Check selector syntax (spaces for descendants) - Verify class names match inline markup
-
-
SKILL.md 11 KB
--- 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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.