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.
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().
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.
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.
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".
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}}"
))
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().
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.
For big text paragraphs use cli::cli_text().
For big code chunks use cli::cli_code().
cli has great tools for constructing progress bars as well. So please use
cli progress bars. Check out the
corresponding vignette.
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().
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!
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.