Sometimes package authors want to run code only once for a given user on a given computer. Not just “once per session”, but “only once ever” or “only once per month” at most. Here are some use cases:
The onetime package lets you do this.
Onetime is a lightweight package, with just two package dependencies, rappdirs and filelock. Its total size including dependencies is less than 50 Kb. So it is cheap to include as a dependency in your own package.
You can install onetime from CRAN like this:
Next, on the R command line, run
If you haven’t used onetime before, you will be asked if it is OK to
store files in onetime’s configuration directory. Answer Y
.
Now you can try out onetime’s functions.
Let’s use onetime_message()
to show a message just once.
On the command line, enter:
id <- "vignette-1"
onetime_message("I shall say this only once!", id = id)
#> I shall say this only once!
You should see the message, displayed using the base R
message()
function.
Now if you enter the same code again:
… you won’t see anything! Even if you restart R and again run
onetime_message()
with the same id
, nothing
will be shown. Onetime has stored a file on your computer to record that
the message was already shown, and it doesn’t show it again.
You still won’t see any message, even if you change the message itself:
That’s because onetime identifies actions by their id
.
If you change the id
, you can send a new message:
id <- "vignette-2"
onetime_message("I repeat... I shall say this only once!", id = id)
#> I repeat... I shall say this only once!
You aren’t limited to sending messages. You can also give warnings:
id <- "vignette-3"
onetime_warning("you cannot expect me to shoot everyone in the town. ",
"I'm unpopular enough as it is!", id = id)
#> Warning: you cannot expect me to shoot everyone in the town. I'm unpopular
#> enough as it is!
You can print package startup messages using
onetime_startup_message()
. If the rlang package is
installed, you can also use onetime_rlang_inform()
and
onetime_rlang_warn()
to print messages and warnings using
rlang format:
id <- "vignette-4"
onetime_rlang_inform(c(
"Let that be a lesson to you.",
i = "Never again will you burn my toast."
),
id = id)
#> Let that be a lesson to you.
#> ℹ Never again will you burn my toast.
Underlying all these functions is onetime_do()
, which
allows you to run arbitrary code just once:
id <- "vignette-5"
onetime_do(light_the_candle_with_the_handle(), id = id)
#> If we kill him with the pill from the till
#> by making with it the drug in the jug, you need not
#> light the candle with the handle on the Gateau from the Chateau!
Onetime uses file locks to avoid race conditions. So even if you use multiple R processes, a onetime action will be run only once:
# NB: This chunk will only be run on Unix-alikes
cl <- parallel::makeCluster(2, outfile = "check.txt")
otd <- getOption("onetime.dir")
results <- parallel::parSapply(cl, 1:20, otd = otd,
function (x, otd) {
options(onetime.dir = otd)
onetime::onetime_message("I will say this only once!", id = "vignette-6")
}
)
parallel::stopCluster(cl)
table(results)
#> results
#> FALSE TRUE
#> 19 1
readLines("check.txt")
#> [1] "starting worker pid=4572 on localhost:11348 at 02:58:34.423"
#> [2] "starting worker pid=4573 on localhost:11348 at 02:58:34.424"
#> [3] "I will say this only once!"
Sometimes you may wish to show a message to the user but give them
the option to hide it in future. You can do this with
onetime_message_confirm()
.
In interactive sessions, this will ask the user if they want to show the message again:
id <- "vignette-7"
onetime_message_confirm(
"What are you doing with that serving girl in your arms?",
id = id)
#> What are you doing with that serving girl in your arms?
#> Show this message again? [yN]
In non-interactive sessions, it will tell the user how they can hide the message in future:
You can set an expiry time by passing a difftime()
object to the expiry
argument of these functions. For
example, this will print a message, but only if it has not been printed
in the past week:
Onetime works by writing a file, typically to a folder in the user’s configuration directory. As a package author, it is your responsibility to check for permission to store lockfiles. CRAN policy demands that you do this. Onetime functions will check for this permission, and by default won’t store the file until it has been granted.
You have several options to handle this:
If your package is used directly from the command line, the
simplest option is to call functions using
without_permission = "ask"
. This will ask the user for
permission to store files if it has not been granted. If not, the action
won’t be run.
id <- "vignette-10"
onetime_message("Thank you for your kind applause.",
id = id,
without_permission = "ask")
#> The onetime package requests to store files in '~/Library/Application Support/onetime'.
#> Is this OK? [Yn]
#> Thank you for your kind applause.
If you want more control over when and how you ask users, you can
call check_ok_to_store(ask = TRUE)
manually before using
onetime functions.
check_ok_to_store(
ask = TRUE,
message = "Please let this package store files in your config directory '%s'.",
confirm_prompt = "OK? (Y/N)"
)
#> Please let this package store files in your config directory'~/Library/Application Support/onetime'
#> OK? (Y/N)
In code which might be run non-interactively, or as part of a
long-running command, you can manually call
set_ok_to_store(TRUE)
before you use other onetime
functions. This will grant permission to store files, and will print a
warning to the user explaining how they can change this:
By default, configuration files are stored beneath
rappdirs::user_config_dir()
. If you want to store
configuration files in a non-standard directory, set
options(onetime.dir = <path to directory>)
. When this
option is set, onetime assumes that permission has been granted. So, you
can also use this approach to avoid raising the issue of permissions
with the user - so long as you don’t plan to submit your package to
CRAN.
Use this mechanism with care. Package authors should always
set options(onetime.dir)
locally within their functions and
set it back to its original value afterwards. Otherwise you risk
changing the directory for other packages, or overwriting the user’s
preferred value. You can do this using
withr::local_options()
:
my_func <- function () {
withr::local_options(onetime.dir = "path to preferred directory")
onetime_message("Hit it hard with your spoon.",
"They always break in the end.",
id = "flick-1")
}
or in base R:
If onetime has already been installed by a different package, then it is likely that the user will have already granted file permissions, and onetime functions will just work.
onetime_been_done()
checks whether an action has been
performed:
To reset a particular id, so that functions will be run again, use
onetime_reset()
:
onetime_reset(id = "vignette-1")
onetime_message("I shall say this only once!", id = "vignette-1")
#> I shall say this only once!
From version 0.2.0 of the package, you can use
onetime_mark_as_done()
to manually mark a particular action
as done:
More information is available on the onetime website: