library(tidyOhdsiSolutions)
# Disable ANSI colour codes so rendered HTML output is clean
options(pkg.no_color = TRUE)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.
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 setUse 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 completemsg_list() is useful for displaying a set of labelled
items, such as cohort components or domain breakdowns.
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-08Combine 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)
#> 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 loadedmsg_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)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.
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.00smsg_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: charactermsg_warning()msg_warning() emits a proper R warning while also
printing a styled console message.
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 threemsg_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) # resetmsg_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")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")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