Console Messages

Let’s make console messages in our packages pretty and unified! Base R tools for this are quite limited, but luckily there’s a package called cli with a wide range of possibilities. So the first recommendation would be - use cli. It has a great vignette highlighting all the nice things it has to offer. It is highly recommended to get acquainted with the vignette first.

The purpose of this document is to give more specific guides on cli functions usage to unify how console messages look in all our packages.

Errors and warnings

For errors, i.e. where in base R you’d use stop(), use cli::cli_abort().

For warnings, i.e. where in base R you’d use warning(), use cli::cli_warn().

Alerts

For alerts or in other words, short status messages, use cli::cli_alert* functions family or cli::cli_inform(). The former differ only by the symbol which is displayed in front of the message, the latter shows messages as is, without additional symbols in front. So take a look at these functions and decide on what’s more appropriate for your particular case.

Recommendations for most common cases are:

  • cli::cli_alert_info() - to indicate that something is about to be done, or that something is in progress, or just to inform about something.
  • cli::cli_alert_success() - to indicate that something is done.
  • cli::cli_alert_warning() - a minor problem or a reminder for user to do something.
  • cli::cli_alert_danger() - a bigger problem.

There’s a function called cli::cli_bullets() which allows to push multiple such alerts and a few other types with one call. But, for clarity, it’s better to use cli_alert* functions where possible.

Inline text formatting

cli provides rich capabilities for inline text formatting, so please use it! In particular:

  • If you refer to a function parameter in your notification, always mark it with .arg. Example:
cli::cli_abort("{.arg x} must be a character of length 1.")
  • For a short piece of code, use .code.
cli::cli_alert_warning("Don't forget to run {.code renv::snapshot()} when you are done.")
  • For file paths - use .path.
cli::cli_alert_info("Current working directory: {.path {getwd()}}")
  • For URLs - use .url.
cli::cli_alert_warning("Wasn't able to reach {.url https://cran.r-project.org/}")

For all other cases just use what you think is more appropriate. But these listed above are the most common and it’s important that we format them in the same way in all our code.

Collapsing inline vectors

cli automatically collapses vectors in glue substitutions. Example:

pkgs <- c("pkg1", "pkg2", "pkg3")
cli_text("Packages: {.pkg {pkgs}}.")
#> Packages: pkg1, pkg2, and pkg3.

However the default style, when “and” is put before the last element is not always something we need. One way to change this style is to use the cli_vec() function.

vals <- c("foo", "baz", "bar")
vals <- cli_vec(vals, style = list(vec_last = " or "))
cli_abort("{.arg x} must be one of: {.val {vals}}.")

#> Error:
#> ! `x` must be one of: "foo", "baz" or "bar".

cli_vec() does not change vector data in any way, it only adds an attribute containing styling information to be used by other cli functions. See cli_vec() documentation for more details.

Another way is to use cli_div() where you can override global styling settings:

cli::cli_div(theme = list(.val = list(vec_last = " or ")))
vals_1 <- c("foo", "bar", "baz")
vals_2 <- c("x", "y", "z")
cli::cli_alert_info("{.arg x1} can be one of the following: {.val {vals_1}}.")
#> ℹ `x1` can be one of the following: "foo", "bar" or "baz".
cli::cli_alert_info("{.arg x2} can be one of the following: {.val {vals_2}}.")
#> ℹ `x2` can be one of the following: "x", "y" or "z".

Line breaks

If you think you need a line break in your alert message please first check if it’s more appropriate to separate your alert call into multiple alert calls or to rewrite it as a cli::cli_bullets() call.

So if you want to do this:

cli::cli_alert_info("All done!\n Using the following model: \n{.code y ~ x + 1}")

it’s better to do the following instead:

cli::cli_alert_success("All done!")
cli::cli_alert_info("Using the following model:")
cli::cli_bullets(c(" " = "{.code y ~ x + 1}")) # " " is for indentation

Or, using one cli_bullets() call:

cli::cli_bullets(c(
  "v" = "All done!",
  "!" = "Using the following model:",
  " " = "{.code y ~ x + 1}"
))

With regards to errors and warnings, \n does not work in cli_abort and cli_warn, but fortunately they accept the cli_bullets() notation. If you need a multiple-line error/warning message, just provide the lines in a form of a character vector. If you need the lines to be indented or decorated with some symbols, provide a named character vector according to cli_bullets() rules.

bad_vals <- c("one", "dotted", "whatever")
cli::cli_warn(c(
  "Following provided values are not colors:",
  " " = "{.val {bad_vals}}"
))

Headings

If your function executes a complex workflow consisting of multiple tasks and you think it makes sense to notify the user about the tasks progress, please divide notifications in chapters using cli header functions: cli::cli_h1(), cli::cli_h2() and cli::cli_h3().

Lists

To display a list of things, one item per row, ordered or unordered, use cli::cli_ol() or cli::cli_ul() correspondingly. Example:

cli::cli_alert_info("We are about to do 3 things:")
cli::cli_ol(c("This thing", "The other one", "The last one"))

cli_ol is useful in particular because you don’t need to manually write sequence numbers.

Bigger text paragraphs or code chunks

For big text paragraphs use cli::cli_text().

For big code chunks use cli::cli_code().

Progress Bars

cli has great tools for constructing progress bars as well. So please use cli progress bars. Check out the corresponding vignette.

Requesting user input

The most common case is when you need user to confirm something by asking a yes/no question. cli does not have tools for it, but we’ve added a helper function to our dev package: g6tr.ui::cli_yeah(). It is inspired by usethis::ui_yeah() and also supports cli inline formatting.

g6tr.ui::cli_yeah(
  "This is meant to be done on {.val master} branch. \\
  Are you sure you want to continue?"
)

For choices other than yes/no ones, we don’t have any standard helper functions and one will have use base R tools like utils::menu(), select.list().

Other things to be aware of

cli provides tools for message pluralization. That is, tools to compactly encode both plural and singular message forms in one string. Check out the vignette on pluralization!

Examples

Console messages are used in the g6tr.cjt:::assert_id() function to provide a user-friendly command line interface for interacting with the user.

The cli_h1() function is used to create a large header at the start of the function output, indicating that the function is asserting identifiers.

The cli_abort() function is used to raise an error if the id parameter is not a single column name from the dataset. If an error occurs, the user will be presented with a clear error message indicating the issue with their input.

The cli_alert_success() function is used to indicate to the user that the function has successfully verified that all identifiers are unique and do not contain missing values. This provides the user with reassurance that the function has completed successfully.

assert_id <- function(data, id) {
  cli_h1("Asserting Identifiers")

  if (is.null(id) || !is.character(id) || length(id) != 1 ||
    !id %in% colnames(data)) {
    cli_abort("The id parameter must be a single column name from the dataset.")
  }

  data %>%
    assert(not_na, !!id) %>%
    assert(is_uniq, !!id) %>%
    verify(has_all_names("response_id"))

  cli_alert_success(
    "All Identifiers are unique and do not contain missing values. Moving on..."
  )
  id
}

Console messages are used in the g6tr.survey::crunch_update_aliases() function to provide a user-friendly command line interface for interacting with the user.

The cli::cli_progress_bar() function creates a progress bar with a specified total number of tasks. In this case, the total number of tasks is the number of variables being updated.

The cli::cli_progress_update() function updates the progress bar for each variable that is updated, and cli::cli_progress_done() completes the progress bar once all the variables have been updated.

crunch_update_aliases <- function(ds,...) {
  
  ## ds is of crunch dataset
  g6tr.ui::check_class(ds, "CrunchDataset")
  
  ## Prepare main data
  var_tbl <-
    variables(ds) %>%
    as_tibble() %>%
    dplyr::filter(...)
  
  ## Make new alias and clean it up
  var_tbl <- var_tbl %>%
    mutate(new_alias = janitor::make_clean_names(name))
  
  ## Apply new aliases to `ds` with a progress bar
  cli::cli_progress_bar("Updating aliases", total = nrow(var_tbl), type = "tasks")
  for (var in var_tbl$alias) {
    alias(ds[[var]]) <- var_tbl[var_tbl$alias == var,]$new_alias
    cli::cli_progress_update()
  }
  cli::cli_progress_done()
}

Console messages are used in the g6tr.theme:::get_scale_fun() function to provide a user-friendly command line interface for interacting with the user.

First, cli::cli_vec() is used to create a nicely formatted list of allowed strings for the scale argument. This is used later in the error message to inform the user of the allowed options.

Then, cli::cli_abort() is used to raise an error if the scale argument is not one of the allowed strings or a valid scaling function. The msg variable contains the error message, which is printed to the console when the function is called with invalid arguments.

get_scale_fun <-
  function(scale = c("percent", "dollar", "none"),
           accuracy = 1) {
    
    allowed_strings <- c("percent", "dollar", "none")
    
    allowed_strings <-
      cli::cli_vec(allowed_strings, style = list(vec_last = " or "))
    msg <-
      "{.arg scale} must be either one of {.val {allowed_strings}} or a \\
  scaling function, e.g {.code scales::comma}"
    
    if (is.character(scale)) {
      if (!(scale %in% c("percent", "dollar", "none")))
        cli::cli_abort(msg)
      fun <- switch(scale,
                    none = identity,
                    percent = scales::percent,
                    dollar = scales::dollar)
    } else {
      if (!is.function(scale))
        cli::cli_abort(msg)
      fun <- scale
    }
    # if `accuracy` is within function parameters
    if ("accuracy" %in% names(formals(fun))) {
      res <- function(x)
        fun(x, accuracy = accuracy)
    } else {
      res <- function(x)
        fun(x)
    }
    res
  }

Console messages are used in the g6tr:::g6tr_renv_prompt() function to provide a user-friendly command line interface for interacting with the user.

First, the function prints a heading using cli::cli_h3(). Then, it uses renv::status to check the status of the renv environment for the specified project.

If the renv environment is not synchronized, meaning that the state of the packages installed in the environment does not match the state recorded in the renv.lock file, the function prompts the user with a menu of options.

The cli::cli_inform() function is used to print a message prompting the user for their choice, as well as a blank line for readability.

The utils::menu function is used to display the menu of options to the user and return the user’s choice.

Finally, depending on the user’s choice, either renv::snapshot or renv::restore is called with the specified project.

g6tr_renv_prompt <- function(project) {
  cli::cli_h3("Checking renv status")
  renv_status <- renv::status(project = project)
  if (!renv_status$synchronized) {
    cli::cli_inform("How would you like to continue?")
    cli::cli_inform("")
    cli::cli_inform("0: skip")
    out <- utils::menu(c(
      "`renv::snapshot()`",
      "`renv::restore()`"
    ))
    if (out == 0) {
      cli::cli_inform("... skipping")
    } else {
      if (out == 1) renv::snapshot(project = project)
      if (out == 2) renv::restore(project = project)
    }
  }
}

In conclusion, by using the {cli} package to provide informative messages and alerts, the user can quickly identify and fix issues with their input arguments and be notified when the function has completed successfully. This leads to a more efficient and user-friendly experience.