7 min read

Cliff notes about the cli package

We’ve both coincidentally dipped our toes in the wonderful world of pretty messaging with cli.

cli::cli_alert_success("marvellous package adopted!")
#>  marvellous package adopted!

In this post, we transform the hurdles we encountered in a series of tips so that your own journey might be slightly smoother, and also to encourage you to try out cli in your package!

This post was featured on the R Weekly highlights podcast hosted by Eric Nantz and Mike Thomas.

cli is the thing for package interfaces now!

You can view cli as a domain-specific language (DSL) for command-line interfaces (CLI): Just like tidyverse makes your data pipelines easier to construct and more readable, cli makes your communication producing code simpler to write.

cli deals with pesky details and we package developers only need to use the high-level interface. Mo has previously got lost in the rabbit hole of making prettier outputs, and thinks noone else should ever have to do that! As an example, text is automatically wrapped to the terminal width.

cli is truly feature-rich, for instance allowing you to make URLs in messages clickable and code in messages runnable at a click!

cli is part of the tidyverse capsule wardrobe, which shows how important it’s becoming, near other important tools such as withr and rlang.

What if you were making pretty interfaces with usethis?

Is your package using usethis::ui_ functions? If you have time for a bit of upkeep, you can do the switch from that to cli by reading and following the cli article on the topic. Do not discover this article well after starting a transition, cough (that’s what happened to Maëlle).

cli formatting: all the curly braces

You’ll probably end up calling various cli functions whose names start with cli_ to create semantic elements: headers, lists, alerts, etc. What do they expect as inputs?

cli has a glue-like syntax: if there’s an object called say thing and you want to use it in a message, you use {thing}.

thing <- "a string"
cli::cli_li("Hey you provided this text: {thing}!")
#> • Hey you provided this text: a string!

Then, you can use classes in messages. Classes, like classes in CSS, help format the elements. For instance if thing is a variable, we write:

thing <- "a string"
cli::cli_li("Hey you provided this text: {thing} ({.var thing})!")
#> • Hey you provided this text: a string (`thing`)!

Curly braces can be nested if you are using a class and glue-like syntax at the same time:

variable_name <- "blop"
cli::cli_li("Hey you entered {.var {variable_name}}!")
#> • Hey you entered `blop`!

See the full list of classes in cli docs. To mention only a few of them (you might notice clickability is a star here…if the terminal supports ANSI hyperlinks to runnable code (for instance, RStudio IDE)):

  • .run means that the code in the message will be clickable! Best code hints ever!
  • .help will have a clickable link to a help topic.
  • .file will have a clickable link to a file.
  • .obj_type_friendly, for instance {.obj_type_friendly {mtcars}}, prints the object type in, well, a friendly way (thanks to Jon Harmon for reminding us about this one).

It’s well worth going through the list of classes at least once.

What if my string contains curly braces?

If you want to actually communicate something with curly braces, you’ll need to double them to escape them.

What about plural?

cli has support for pluralization (presumably only for 0/1/more than one, not for more complex forms of pluralization).

How to add a custom class / theme

How to define a custom class or theme seems to be a bit under-documented at the moment, which is unsurprising as it’s an advanced topic. Jenny Bryan assumes adding a custom class needs to happen as part of a theme, which she does in googlesheets4. Michael McCarthy recommends looking at what cli itself does.

What about dark themes?

Together, we cover both sides of having dark (Mo) and light (Maëlle) themes in RStudio. It would be easy to assume that this all would not work well on dark theme, but Mo can tell you that it all works seamlessly. The output colors change to be dark friendly! For that reason, user-overrides of colors can be a tricky thing, as you are no longer relying on the excellent tooling of cli to make sure this works in both light and dark mode.

An user can define their own themes, for instance putting this in their profile to make variables underlined:

# This goes in your .Rprofile 
options(cli.user_theme = list(.var = list("text-decoration" = "underline")))

# not needed if the previous line is in your .Rprofile 
# we do this here to expose the effect in this post directly
cli::start_app() 

# Code is underlined!
cli::cli_text("Don't call a variable {.var variable}!")

How to turn off colors

cli respects the nocolor standard, therefore an user can set the environment variable NO_COLOR to any string in order to not get colored output.

cli for expressing what?

One of the things the usethis:: functions do so well, it be very verbose of that they are doing.

pkg_dir <- withr::local_tempdir()
usethis::create_package(pkg_dir, open = FALSE)
#>  Setting active project to '/tmp/Rtmpll7FtZ/file522e43136aa8'
#>  Creating 'R/'
#>  Writing 'DESCRIPTION'
#> Package: file522e43136aa8
#> Title: What the Package Does (One Line, Title Case)
#> Version: 0.0.0.9000
#> Authors@R (parsed):
#>     * Maëlle Salmon <msmaellesalmon@gmail.com> [cre, aut] (<https://orcid.org/0000-0002-2815-0399>)
#> Description: What the package does (one paragraph).
#> License: MIT + file LICENSE
#> Encoding: UTF-8
#> Roxygen: list(markdown = TRUE)
#> RoxygenNote: 7.2.3
#>  Writing 'NAMESPACE'
#>  Setting active project to '<no active project>'

This is great information so the user can build an understanding of what a function they ran actually does. This is particularly important for functions that do something with a users settings or file system in some persistent way, as it makes it possible to back-track what has been done and alter it at need. See the the tidy design guide for more information on this subject.

Progress

As seen when installing packages with pak! Not only does cli support progress bars, it documents how to create them in two articles!

How does it look in logfiles?

It’s likely not what you are after for a logfile, as each new update will create another line. The cli output in non-interactive sessions or to file is discussed in the article about advanced progress bar topics.

Error messages

You can use cli::cli_abort() instead of stop() for errors, as recommended by the tidyverse style guide. Note that using this function necessitates your package importing rlang. A neat thing about this (and companions cli::cli_alert() and cli::cli_warn()), is that you can construct very complex messages through syntax seen throughout the cli package. For instance, providing a vector of text will create multi-line messages, and giving the elements specific names can turn them into list types.

cli::cli_abort(
  c(
    "This is a complex error message.",
    "The input is missing important things:",
    "*" = "important thing 1", # bullet point
    "*" = "important thing 2", # bullet point
    "To learn more about these see the {.url www.online.docs}"
  )
)
#> Error:
#> ! This is a complex error message.
#> The input is missing important things:
#>  important thing 1
#>  important thing 2
#> To learn more about these see the <www.online.docs>

How to make cli quiet or not

You can choose to silence/shush/muffle cli messages!

For cli functions whose name starts with cli_, see the docs.

Let’s try an example.

cli::cli_li("hello")
#> • hello
rlang::local_options(cli.default_handler = function(msg) invisible(NULL))
cli::cli_li("hello")

This can be useful in tests for instance! Who wants to be looking at a wall of cli output when debugging one’s tests.

For other functions, probably write your own wrapper and make it responsive to an option (rather than arguments in all functions, see discussion), like what happens in usethis with the usethis.quiet option.

Speaking of options, let’s remind you about rlang::is_interactive() whose output is controllable via the rlang_interactive global option.

How to test cli output

You should probably look into snapshot tests, maybe in combination with cli::test_that_cli().

Conclusion

In this post we presented what we know about cli: in short, it’s a fantastic package for building informative and pretty command-line interfaces, and its docs are extensive!