--- title: "Console Messaging with handyCli" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Console Messaging with handyCli} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} 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. ```{r basic-messages} msg_info("Loading configuration from disk") msg_success("All 42 concept sets validated") msg_warn("Column 'mappedSourceCode' is missing — using NA") msg_danger("Connection pool exhausted (non-fatal, retrying)") msg_process("Uploading results to the remote schema") msg_bullet("concept_id 201826 — Type 2 Diabetes Mellitus") msg_todo("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. ```{r layout} msg_header("Step 1: Validate Input") msg_info("Checking required columns") msg_success("Validation passed") msg_blank() msg_header("Step 2: Build Cohort") msg_process("Assembling concept set expressions") msg_rule() msg_info("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. ```{r msg-list} domains <- c( Condition = "201826, 442793", Drug = "1503297, 40163554", Measurement = "3004501" ) msg_list(domains, header = "Concept set domains") ``` ### Key-value table `msg_kv()` aligns keys and values into two columns — handy for configuration summaries or run metadata. ```{r msg-kv} 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) ``` --- ## 4 Iteration patterns ### 4a Logging inside a loop Combine `msg_header()` and message functions to annotate each iteration of a processing loop. ```{r iteration-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() } ``` ### 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. ```{r iteration-safe} 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 } ) } ``` ### 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. ```{r verbose-pattern} 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") # 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. ```{r timing-simple} result <- msg_timed( expr = Sys.sleep(0.05), msg = "Sleeping" ) ``` ### Timing an iteration ```{r timing-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 }) ) ``` --- ## 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. ```{r msg-abort} 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)) ) ``` ### Raising styled warnings with `msg_warning()` `msg_warning()` emits a proper R warning while also printing a styled console message. ```{r msg-warning} 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") } ) ``` ### `msg_try()` on_error modes ```{r msg-try-modes} # "warn" — downgrade error to a styled warning msg_try(stop("something went wrong"), on_error = "warn") # "message" — emit as a styled danger message, no stop msg_try(stop("non-critical failure"), on_error = "message") # "ignore" — silently swallow the error msg_try(stop("ignored error"), on_error = "ignore") msg_info("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. ```{r msg-debug-off} # 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)") ``` ```{r msg-debug-on} options(pkg.debug = TRUE) msg_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. ```{r progress-example, eval = FALSE} 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. ```{r spinner-example, eval = FALSE} 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. ```{r full-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) ```