Introduction to progress bars in cli
Gábor Csárdi
2024-06-23
Source:vignettes/progress.Rmd
progress.Rmd
Introduction
This document discusses the structure and simplest uses of the cli progress bar API. For more advanced usage and the C progress bar API, see the ‘Advanced cli progress bars’ article and the manual pages.
From version 3.0.0 cli provides a set of functions to create progress bars. The main goals of the progress bar API are:
- Reduce clutter. Try to avoid verbose syntax, unless necessary.
- Flexibility from R and C/C++ code. Support all cli features in progress bars: glue interpolation, theming, pluralization, etc.
- Predictably small performance penalty. A very small constant penalty per iteration, and a reasonable penalty per second.
The traditional progress bar API
Add a progress bar in three steps:
- Call
cli_progress_bar()
to add create a progress bar. - Call
cli_progress_update()
to update it. - Call
cli_progress_done()
to terminate it.
For example:
clean <- function() {
cli_progress_bar("Cleaning data", total = 100)
for (i in 1:100) {
Sys.sleep(5/100)
cli_progress_update()
}
cli_progress_done()
}
clean()
The traditional API provides full control w.r.t when to create, update and terminate a progress bar.
The current progress bar
For conciseness, the progress bar functions refer to the current progress bar by default. Every function has at most one current progress bar at any time. The current progress bar of a function is terminated when the function creates another progress bar or when the function returns, errors or is interrupted.
The current progress bar lets us omit the
cli_progress_done()
call:
clean <- function() {
cli_progress_bar("Cleaning data #1", total = 100)
for (i in 1:100) {
Sys.sleep(3/100)
cli_progress_update()
}
cli_progress_bar("Cleaning data #2", total = 100)
for (i in 1:100) {
Sys.sleep(3/100)
cli_progress_update()
}
}
clean()
Unknown total number of units
In some cases the total number of progress units is unknown, so
simply omit them from cli_progress_bar()
(or set them to
NA
). cli uses a different display when total
is unknown:
walk_dirs <- function() {
cli_progress_bar("Walking directories")
while (TRUE) {
if (runif(1) < 0.01) break
Sys.sleep(0.01)
cli_progress_update()
}
cli_progress_update(force = TRUE)
}
walk_dirs()
Quick loops
By default, cli does not show progress bars that are terminated
within two seconds after their creation. The end user can configure this
limit with the cli.progress_show_after
global option.
For example, in this document we set the limit to zero seconds, so progress bars are shown at their first update.
Progress bars for mapping functions:
cli_progress_along()
cli_progress_along()
is currently experimental.
To add a progress bar to a call to lapply()
or another
mapping function, wrap the input sequence into
cli_progress_along()
:
lapply(cli_progress_along(X), fun)
cli_progress_along()
works similarly to
seq_along()
, it returns an index vector. If you use
cli_progress_along()
, then lapply()
will pass
the indices of the elements in X
to
fun
, instead of the elements themselves.
cli_progress_along()
expects that the index vector will
be used only once, from beginning to end. It is best to never assign the
return value of cli_progress_along()
to a variable.
An example:
f <- function() {
rawabc <- lapply(
cli_progress_along(letters),
function(i) {
charToRaw(letters[i])
Sys.sleep(0.5)
}
)
}
f()
cli_progress_along()
uses ALTREP, so it only works from
R 3.5.0 and later. On older R versions it is equivalent to
seq_along()
and it does not create a progress bar.
for
loops
You can also use cli_progress_along()
in
for
loops, with the additional complication that if you use
break
, then you might need to terminate the progress bar
explicitly:
for (i in cli_progress_along(seq)) {
...
if (cond) cli_progress_done() && break
...
}
cli_progress_done()
always returns TRUE
to
allow this form.
Alternatively, you can terminate the progress bar right after loop:
for (i in cli_progress_along(seq)) {
...
if (cond) break
...
}
cli_progress_done()
If the function containing the for
loop returns after
the loop, or you create another progress bar with
cli_progress_along()
or cli_progress_bar()
,
then no explicit cli_progress_done()
is needed.
Simplified API
Often you don’t need the full power of the progress bar API, and only
want to show a status message. The cli_progress_message()
and cli_progress_step()
functions are tailored for
this.
cli_progress_message()
shows a (potentially templated)
message in the status bar. For convenience, the progress bar rules still
apply here by default:
- Status messages are removed when their calling function exits.
- A status message removes the previous status message or progress bar of the same caller function.
f <- function() {
cli_progress_message("Task one is running...")
Sys.sleep(2)
cli_progress_message("Task two is running...")
Sys.sleep(2)
step <- 1L
cli_progress_message("Task three is underway: step {step}")
for (step in 1:5) {
Sys.sleep(0.5)
cli_progress_update()
}
}
f()
Status messages may use glue interpolation, cli styling and
pluralization, as usual. You can call cli_progress_update()
to update a status message.
cli_progress_step()
is slightly different from
cli_progress_message()
: * it adds cli’s alert themes to the
status messages (info, success or danger), * prints the duration of each
step (by default), and * it keeps the messages on the screen after they
are terminated.
f <- function() {
cli_progress_step("Downloading data")
Sys.sleep(2)
cli_progress_step("Importing data")
Sys.sleep(1)
cli_progress_step("Cleaning data")
Sys.sleep(2)
cli_progress_step("Fitting model")
Sys.sleep(3)
}
f()
As usual, you can use cli_progress_step()
to update an
existing status message.
f <- function(n = 10) {
cli_alert_info("About to start downloads of {n} file{?s}")
i <- 0
cli_progress_step("Got {i}/{n} {qty(i)}file{?s}.")
for (i in seq_len(n)) {
Sys.sleep(0.5)
if (i == 5) cli_alert_info("Already half way!")
cli_progress_update()
}
}
f()
If you can update the status message frequently enough, then you can also add a spinner to it:
f <- function() {
cli_progress_step("Downloading data", spinner = TRUE)
for (i in 1:100) { cli_progress_update(); Sys.sleep(2/100) }
cli_progress_step("Importing data", spinner = TRUE)
for (i in 1:100) { cli_progress_update(); Sys.sleep(1/100) }
cli_progress_step("Cleaning data", spinner = TRUE)
for (i in 1:100) { cli_progress_update(); Sys.sleep(2/100) }
cli_progress_step("Fitting model", spinner = TRUE)
for (i in 1:100) { cli_progress_update(); Sys.sleep(3/100) }
}
f()
cli_progress_step()
automatically handles errors, and
styles the status message accordingly:
f <- function() {
cli_progress_step("First step, this will succeed")
Sys.sleep(1)
cli_progress_step("Second step, this will fail")
Sys.sleep(1)
stop("Something is wrong here")
}
f()