Console Messaging with handyCli

library(tidyOhdsiSolutions)

# Disable ANSI colour codes so rendered HTML output is clean
options(pkg.no_color = TRUE)

Overview

handyCli is a zero-dependency console messaging system built on base R. It provides styled, consistently prefixed messages for every severity level, structured output helpers (msg_list(), msg_kv()), progress and spinner indicators, a timing wrapper, and safe error-handling utilities.

All functions write to message() (stderr) so they are silenceable with suppressMessages() and do not pollute stdout() or function return values.


1 Basic message levels

Each message type carries a distinct prefix symbol.

msg_info("Loading configuration from disk")
#> i Loading configuration from disk
msg_success("All 42 concept sets validated")
#> v All 42 concept sets validated
msg_warn("Column 'mappedSourceCode' is missing — using NA")
#> ! Column 'mappedSourceCode' is missing — using NA
msg_danger("Connection pool exhausted (non-fatal, retrying)")
#> x Connection pool exhausted (non-fatal, retrying)
msg_process("Uploading results to the remote schema")
#> -> Uploading results to the remote schema
msg_bullet("concept_id 201826 — Type 2 Diabetes Mellitus")
#> * concept_id 201826 — Type 2 Diabetes Mellitus
msg_todo("Verify descendant flag for hypertension concept set")
#> - Verify descendant flag for hypertension concept set

2 Section structure: headers, rules, and blank lines

Use msg_header() and msg_rule() to visually group output in long-running pipelines.

msg_header("Step 1: Validate Input")
#> --------------------------- Step 1: Validate Input ---------------------------
msg_info("Checking required columns")
#> i Checking required columns
msg_success("Validation passed")
#> v Validation passed
msg_blank()
#> 
msg_header("Step 2: Build Cohort")
#> ---------------------------- Step 2: Build Cohort ----------------------------
msg_process("Assembling concept set expressions")
#> -> Assembling concept set expressions
msg_rule()
#> --------------------------------------------------------------------------------
msg_info("Pipeline complete")
#> i Pipeline complete

3 Structured output: lists and key-value tables

Named list

msg_list() is useful for displaying a set of labelled items, such as cohort components or domain breakdowns.

domains <- c(
  Condition   = "201826, 442793",
  Drug        = "1503297, 40163554",
  Measurement = "3004501"
)
msg_list(domains, header = "Concept set domains")
#> i Concept set domains
#>   > Condition: 201826, 442793
#>   > Drug: 1503297, 40163554
#>   > Measurement: 3004501

Key-value table

msg_kv() aligns keys and values into two columns — handy for configuration summaries or run metadata.

run_info <- list(
  Package   = "tidyOhdsiSolutions",
  Version   = as.character(packageVersion("tidyOhdsiSolutions")),
  R_version = paste0(R.version$major, ".", R.version$minor),
  Date      = format(Sys.Date())
)
msg_kv(run_info)
#>   Package    tidyOhdsiSolutions
#>   Version    0.1.0
#>   R_version  4.5.3
#>   Date       2026-04-08

4 Iteration patterns

4a Logging inside a loop

Combine msg_header() and message functions to annotate each iteration of a processing loop.

concept_sets <- list(
  diabetes     = c(201826L, 442793L),
  hypertension = c(320128L),
  obesity      = c(433736L, 4215968L)
)

for (nm in names(concept_sets)) {
  msg_header(nm)
  msg_info("Concepts: ", paste(concept_sets[[nm]], collapse = ", "))
  msg_success("Processed ", length(concept_sets[[nm]]), " concept(s)")
  msg_blank()
}
#> ---------------------------------- diabetes ----------------------------------
#> i Concepts: 201826, 442793
#> v Processed 2 concept(s)
#> 
#> -------------------------------- hypertension --------------------------------
#> i Concepts: 320128
#> v Processed 1 concept(s)
#> 
#> ---------------------------------- obesity -----------------------------------
#> i Concepts: 433736, 4215968
#> v Processed 2 concept(s)
#> 

4b Safe iteration with msg_try()

msg_try() wraps an expression so errors and warnings are caught and styled without stopping the loop. The on_error argument controls the behaviour: "warn" downgrades errors to styled warnings; "ignore" silences them.

sources <- list(
  schema_a = list(valid = TRUE,  rows = 1200L),
  schema_b = list(valid = FALSE, rows = 0L),
  schema_c = list(valid = TRUE,  rows = 850L)
)

results <- vector("list", length(sources))
names(results) <- names(sources)

for (nm in names(sources)) {
  results[[nm]] <- msg_try(
    on_error = "warn",
    expr = {
      src <- sources[[nm]]
      if (!src$valid) stop("Schema '", nm, "' failed validation")
      msg_success(nm, ": ", src$rows, " rows loaded")
      src$rows
    }
  )
}
#> v schema_a: 1200 rows loaded
#> ! Schema 'schema_b' failed validation
#> v schema_c: 850 rows loaded

4c Verbose mode — conditional logging inside helpers

msg_verbose() only emits output when getOption("pkg.verbose") is TRUE (or the verbose argument is set explicitly). This lets callers opt in/out without modifying the function body.

process_file <- function(path, verbose = TRUE) {
  msg_verbose("Opening: ", path, verbose = verbose)
  # ... processing ...
  msg_verbose("Done:    ", path, verbose = verbose)
  invisible(path)
}

# Verbose on (default)
process_file("data/concepts.csv")
#> i Opening: data/concepts.csv
#> i Done:    data/concepts.csv

# Verbose off
process_file("data/concepts.csv", verbose = FALSE)

5 Timing expressions with msg_timed()

msg_timed() evaluates an expression, prints a labelled elapsed-time message, and returns the result invisibly. It is composable — the timed block can be any R expression, including a whole pipeline.

result <- msg_timed(
  expr  = Sys.sleep(0.05),
  msg   = "Sleeping"
)
#> i Sleeping: 0.08s

Timing an iteration

concept_ids <- as.list(c(201826L, 442793L, 320128L, 433736L))

processed <- msg_timed(
  msg  = "Total batch time",
  expr = lapply(concept_ids, function(id) {
    msg_info("Processing concept_id ", id)
    id * 2L           # stand-in for real work
  })
)
#> i Processing concept_id 201826
#> i Processing concept_id 442793
#> i Processing concept_id 320128
#> i Processing concept_id 433736
#> i Total batch time: 0.00s

6 Error and warning handling

Raising styled errors with msg_abort()

msg_abort() throws a proper R error condition so it integrates with tryCatch() and withCallingHandlers() like any other error.

validate_schema <- function(x) {
  if (!is.data.frame(x)) {
    msg_abort("Expected a data.frame, got: ", class(x)[1])
  }
  invisible(x)
}

# Catch the error and show its message
tryCatch(
  validate_schema("not a data frame"),
  error = function(e) msg_danger("Caught: ", conditionMessage(e))
)
#> x Caught: x Expected a data.frame, got: character

Raising styled warnings with msg_warning()

msg_warning() emits a proper R warning while also printing a styled console message.

withCallingHandlers(
  {
    msg_warning("Deprecated argument 'schema' — use 'cdm_schema' instead")
    msg_info("Continuing with default")
  },
  warning = function(w) {
    # Muffle so it does not print twice
    invokeRestart("muffleWarning")
  }
)
#> i Continuing with default

msg_try() on_error modes

# "warn"    — downgrade error to a styled warning
msg_try(stop("something went wrong"), on_error = "warn")
#> ! something went wrong

# "message" — emit as a styled danger message, no stop
msg_try(stop("non-critical failure"), on_error = "message")
#> x non-critical failure

# "ignore"  — silently swallow the error
msg_try(stop("ignored error"),        on_error = "ignore")
msg_info("Execution continued after all three")
#> i Execution continued after all three

7 Debug messages

msg_debug() is a no-op unless options(pkg.debug = TRUE) is set, making it safe to leave in production code.

# Default: pkg.debug = FALSE, so nothing is printed
msg_debug("SQL query: SELECT * FROM concept WHERE ...")
msg_info("(no debug output above — pkg.debug is FALSE)")
#> i (no debug output above — pkg.debug is FALSE)
options(pkg.debug = TRUE)
msg_debug("SQL query: SELECT * FROM concept WHERE ...")
#> [DEBUG] SQL query: SELECT * FROM concept WHERE ...
options(pkg.debug = FALSE)   # reset

8 Progress bar (interactive sessions)

msg_progress() is designed for interactive terminal use. In rendered documents the \r cursor updates do not display, so the code below is shown but not evaluated.

files <- paste0("file_", seq_len(8), ".csv")
pb    <- msg_progress(length(files), prefix = "Loading")

for (f in files) {
  Sys.sleep(0.1)   # simulated I/O
  pb$tick()
}
pb$done("All files loaded")

9 Spinner (interactive sessions)

Similarly, the animated spinner is for interactive use only.

sp <- msg_spinner("Querying vocabulary server")

for (i in seq_len(30)) {
  Sys.sleep(0.05)
  sp$spin()
}
sp$done("Query complete")

10 Putting it all together — annotated pipeline

The example below combines several handyCli helpers to produce readable console output for a multi-step pipeline.

run_pipeline <- function(concept_sets, verbose = TRUE) {

  msg_header("tidyOhdsiSolutions Pipeline")
  msg_kv(list(
    Steps       = as.character(length(concept_sets)),
    Verbose     = as.character(verbose)
  ))
  msg_blank()

  results <- vector("list", length(concept_sets))
  names(results) <- names(concept_sets)

  for (nm in names(concept_sets)) {
    msg_process("Processing: ", nm)

    results[[nm]] <- msg_try(on_error = "warn", expr = {
      ids <- concept_sets[[nm]]
      if (length(ids) == 0L) stop("'", nm, "' has no concept IDs")
      msg_verbose("  concept IDs: ", paste(ids, collapse = ", "),
                  verbose = verbose)
      ids
    })
  }

  msg_blank()
  msg_rule()

  succeeded <- sum(!vapply(results, is.null, logical(1L)))
  msg_success(succeeded, " / ", length(concept_sets), " concept sets processed")

  invisible(results)
}

concept_sets <- list(
  diabetes     = c(201826L, 442793L),
  hypertension = c(320128L),
  empty_set    = integer(0)      # will trigger a warning
)

out <- run_pipeline(concept_sets, verbose = TRUE)
#> ------------------------ tidyOhdsiSolutions Pipeline -------------------------
#>   Steps    3
#>   Verbose  TRUE
#> 
#> -> Processing: diabetes
#> i   concept IDs: 201826, 442793
#> -> Processing: hypertension
#> i   concept IDs: 320128
#> -> Processing: empty_set
#> ! 'empty_set' has no concept IDs
#> 
#> --------------------------------------------------------------------------------
#> v 2 / 3 concept sets processed