cli: semantic terminal output for R, with themes and pluralisation
Tools for making beautiful & useful command line interfaces
At a glance
- What is it?
- The cli package from r-lib is a set of helpers for building attractive command line interfaces in R, built from semantic elements such as headings, lists, alerts and paragraphs, with a CSS-like theming language, glue interpolation, pluralisation and progress bars. It suits R package authors who want their tool output to look designed rather than assembled from cat and message, and the elements are composable enough that a custom theme is a list, not a fork.
- Who is it for?
- Adopt cli if you are writing an R command line tool and want headings, alerts, lists, progress bars and themed text without hand-rolling ANSI escape sequences, since the semantic elements and the theme language cover most of what a polished tool needs.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly R, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Semantic elements instead of escape sequences
cli is described as a suite of tools to build attractive command line interfaces, from semantic elements: headers, lists, alerts, paragraphs and so on. The word semantic is the important one. The package does not expose a colour and a bold flag and leave you to compose them; it exposes a set of named things you can emit, and it decides how each one looks.
The alerts are the clearest example, and they come in five variants. A success alert:
``` r pkgs <- c("foo", "bar", "foobar") cli_alert_success("Downloaded {length(pkgs)} packages.")
An info alert, which also shows the inline markup in action:
r db_url <- "example.com:port" cli_alert_info("Reopened database {.url {db_url}}.")
A warning and a danger variant, `cli_alert_warning("Cannot reach GitHub, using local database cache.")` and `cli_alert_danger("Failed to connect to database.")`, plus a generic `cli_alert("A generic alert")` when the severity is decided at runtime.
Headings come in three levels, with `cli_h1`, `cli_h2` and `cli_h3`.
Lists are the other semantic element, and they are the ones that need the most machinery. Ordered, unordered and description lists, that can be nested, and the nesting is explicit rather than inferred, which is what allows a nested list to be built across separate function calls:
r fun <- function() { cli_ol() cli_li("Item 1") ulid <- cli_ul() cli_li("Subitem 1") cli_li("Subitem 2") cli_end(ulid) cli_li("Item 2") cli_end() } fun() ```
The `cli_ul()` call returns an identifier and `cli_end(ulid)` closes that specific list, while the outer `cli_end()` closes the ordered list. That explicit handle is what makes it safe to interleave other output with the list rather than requiring the whole list to be built in one uninterrupted sequence.
The package also contains a number of lower level CLI elements: rules, boxes, trees, and Unicode symbols with ASCII alternatives. That last part is a practical decision rather than a stylistic one, since a symbol that renders as a box in one terminal and as mojibake in another is worse than a plain character.
A theme is a list, and it applies to spans inside the text
The theming system is the most interesting part of the package, because it is a small language rather than a set of arguments.
``` r fun <- function() { cli_div(theme = list(span.emph = list(color = "orange"))) cli_text("This is very {.emph important}") cli_end() cli_text("Back to the {.emph previous theme}") } fun() ```
A theme is a list, and the keys are CSS-like selectors. `span.emph` names a span class, and the value is a list of style properties, here a colour. The text then refers to that class with `{.emph important}`, in the same brace syntax used for interpolation. `cli_div` opens a themed scope and `cli_end()` closes it, which is why the last line reverts to the previous theme automatically.
The scope behaviour is the part that makes this usable rather than decorative. A theme does not have to be applied to an entire function; it applies to a block, and everything after `cli_end()` goes back to what it was. That means a library can define a theme for its own output without affecting the rest of the program's output, and a program can set a theme for a section without a global option.
Below that, the package supports ANSI markup for terminal colours and font styles, and it has ANSI styled string manipulation as a feature of its own, which is what lets the styled text be embedded in a longer message rather than only being the whole message.
The documentation notes that all cli text can contain interpreted string literals, via the glue package. That is the second half of the design, and it turns out to matter more than it sounds: because the braces are glue's, a literal brace in your text has to be escaped, and any expression inside braces is evaluated at format time rather than being printed.
Glue interpolation and plural forms in the same braces
The command substitution example shows how much work happens inside those braces.
``` r size <- 123143123 dt <- 1.3454 cli_alert_info(c( "Downloaded {prettyunits::pretty_bytes(size)} in ", "{prettyunits::pretty_sec(dt)}"))
Any R expression can be evaluated inside the braces, including a namespaced call to another package, which is how a byte count becomes a human readable size and a duration becomes a readable time without the calling code doing the formatting itself.
Pluralisation is built on the same syntax. The README calls it pluralization support, and the mechanism is a plural marker inside the braces:
r nfiles <- 3 ndirs <- 1 cli_alert_info("Found {nfiles} file{?s} and {ndirs} director{?y/ies}.")
`{?s}` appends an s when the count is not one, and `{?y/ies}` gives two explicit forms. This is the kind of detail that separates a tool that reads as though a person wrote the output from one that reads as though a template was filled in, and it is the sort of thing that is tedious to implement and tedious to test if you do it yourself.
Progress bars are in the same family. The example is a function that creates a bar for a total of 100 and updates it in a loop:
r clean <- function() { cli_progress_bar("Cleaning data", total = 100) for (i in 1:100) { Sys.sleep(5/100) cli_progress_update() } } clean() ```
The feature list notes that progress bars work from R and from C code, which is the point for a package that is often used underneath a compiled dependency.
Error and warning messages with rich text formatting are also listed, so the same styling that applies to an alert also applies to a condition, and a package's error handling does not have to look different from the rest of its output.
Installing from CRAN, and the README that is generated
Installation is the standard R pair. The stable version comes from CRAN:
``` r install.packages("cli")
and the development version from GitHub:
r pak::pak("r-lib/cli") ```
The second one is worth a note, because it is not a devtools idiom. It uses pak, the package manager from the tidyverse organisation, which handles the dependency resolution and the local library setup in one call. The repository is a standard R package layout, with DESCRIPTION, NAMESPACE, an R/ directory, src/ for the compiled parts, man/ for the documentation, tests/, vignettes/, inst/ and tools/.
The compiled parts are what make the C-level progress bar possible. There is an src/ directory and an exec/ directory alongside it, and a `.Rbuildignore` and a `cli.Rproj` for the RStudio project file, plus an `air.toml` and a `Makefile` for the formatting and build tooling. The Makefile has a single rule, and it shows that the README is generated rather than hand written: README.md is built from README.Rmd with rmarkdown, and a sed command then rewrites the local figure paths to point at a CDN copy of the figures in the repository.
The README itself says so at the top, with a comment noting that README.md is generated from README.Rmd and asking that the Rmd file be edited instead. That matters for anyone who wants to contribute an example: the entry point is the Rmd, and the Markdown you see is a build product.
The published releases are v3.6.6 from 2026-04-09, v3.6.5 from 2025-04-24 and v3.6.4 from 2025-02-13, and the last push to the repository was on 2026-10-19. The homepage is cli.r-lib.org, and the documentation is also available in the installed package with `help(package = "cli")`.
Against printing strings and against a lower level terminal library
The comparison worth making is with the two other ways to produce nice terminal output from an R program.
The first is to not bother. `cat` and `message` and `sprintf` will produce correct output with no dependency at all, and for a script that runs once and prints three lines, that is the right choice. The cost shows up when the output has structure: a list of items with a heading above it, a progress indication, a warning in the middle of a run. Each of those becomes manual string assembly, and manual string assembly in a terminal is where the escape sequences start.
The second is a lower level terminal library that gives you colour and cursor control and nothing else. That is the opposite tradeoff: you get full control and no opinion, and you end up writing the same layout code for every tool, including the part where the output has to look right in a narrow terminal and in a redirected file.
cli sits in between, and its position is best described as opinionated but themeable. The opinions are the semantic elements and the fact that a message is composed of spans with classes rather than raw markup. The escape hatch is the theme system, which is a list, and the fact that the lower level elements are available for the cases where the semantic ones do not fit.
There is a cost to that opinionation, and it is the one worth stating. A library that styles its output for a human reader is producing output that is harder to consume programmatically, and the package is built for the human case. If your tool's output is meant to be piped into another program, styled output with ANSI sequences is a liability, and a package that prints plain text is the better choice.
The other cost is that cli is not a terminal emulator. It does not know the width of your terminal, and it does not re-wrap text. That job belongs to the calling code, and the examples in the README are all short enough that it never comes up.
Editorial conclusion
Adopt cli if you are writing an R command line tool and want headings, alerts, lists, progress bars and themed text without hand-rolling ANSI escape sequences, since the semantic elements and the theme language cover most of what a polished tool needs. Do not adopt it if your output has to be parsed by other programs, because the library is built to render for a human reader, and check first that the colour and style output survives the terminal your users actually run in, because the package styles text with ANSI and degrades differently depending on the host.
Frequently asked questions
What is the cli R package?
It is a suite of tools to build attractive command line interfaces from semantic elements such as headers, lists, alerts and paragraphs, with CSS-like theming, ANSI markup for terminal colours and font styles, and lower level elements including rules, boxes, trees and Unicode symbols with ASCII alternatives.
How do I install cli?
The stable version comes from CRAN with `install.packages("cli")`, and the development version from GitHub with `pak::pak("r-lib/cli")`. The package supports terminal colours and font styles through ANSI markup.
How does theming work in cli?
A theme is a list with CSS-like selector keys, for example `span.emph = list(color = "orange")`, and text refers to that class with `{.emph important}`. `cli_div(theme = ...)` opens a themed scope and `cli_end()` closes it, so the previous theme applies again afterwards.
Does cli support pluralization in messages?
Yes, through plural markers in the interpolated braces. The README example is `cli_alert_info("Found {nfiles} file{?s} and {ndirs} director{?y/ies}.")`, where `{?s}` appends an s and `{?y/ies}` gives two explicit forms.
Can cli be used from C code?
Yes for progress bars, which the feature list describes as working from R and from C code. The package has src/ and exec/ directories for the compiled parts, and the C-level progress bar is one of the reasons the package is not pure R.
Is cli still being maintained?
The repository is not archived and the last push was on 2026-10-19. The listed releases are v3.6.6 from 2026-04-09, v3.6.5 from 2025-04-24 and v3.6.4 from 2025-02-13, and the README states the licence as MIT with Posit Software, PBC as the copyright holder.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/r-lib-cli)Was this useful?