Environment variables can be a tremendously helpful alternative to options for behaviors that one might want to handle ubiquitously across R sessions, that might be administered, or that might be needed in a command-line context.
They can provide a helpful interface for supporting tasks that might
be called during continuous integration jobs, on a compute cluster, or
within a containerized environment. Since use cases can be hard to
predict, options
will consider appropriately named
environment variables by default.
Perhaps the default environment names don’t suit you. When defining
your options, you can always use your own! However, this level of
customization is only available through the define_option()
interface.
By default environment variables look like
R_<PACKAGE>_<OPTION>
define_option(
"volume",
default = "shout",
desc = "Print output in uppercase ('shout') or lowercase ('whisper')",
option_name = "volume",
envvar_name = "VOL"
)
#>
#> volume = "shout"
#>
#> Print output in uppercase ('shout') or lowercase ('whisper')
#>
#> option : volume
#> envvar : VOL (evaluated if possible, raw string otherwise)
#> *default : "shout"
twist_and <- function(what = opt("volume")) {
lyric <- paste(
"Well, shake it up, baby, now (Shake it up, baby)",
"Twist and shout (Twist and shout)",
sep = "\n"
)
cat(if (what == "shout") toupper(lyric) else tolower(lyric), "\n")
}
twist_and() # by default, "shout"
#> WELL, SHAKE IT UP, BABY, NOW (SHAKE IT UP, BABY)
#> TWIST AND SHOUT (TWIST AND SHOUT)
We can now alter our behavior using the environment variable,
VOL
.
Sys.setenv(VOL = "whisper")
twist_and() # picks up our environment variable, "whisper"
#> well, shake it up, baby, now (shake it up, baby)
#> twist and shout (twist and shout)
That’s better!
Although individually mapping options to environment variables is handy for one-off options, it can be tedious to do throughout your package, especially if you want all your variables to follow some consistent naming scheme.
For this we can provide a function which is used to name all future options’ environment variables.
set_envvar_name_fn(function(package, name) {
gsub("[^A-Z0-9]", "_", toupper(paste0(package, "_", name)))
})
Now any future option environment variables will use this convention when they are defined. Existing options will be unaffected until they are redefined, so it’s often best to make sure this code runs before you start defining options.
define_options(
"Print output in uppercase ('shout') or lowercase ('whisper')",
volume = "shout"
)
#>
#> volume = "shout"
#>
#> Print output in uppercase ('shout') or lowercase ('whisper')
#>
#> option : globalenv.volume
#> envvar : GLOBALENV_VOLUME (evaluated if possible, raw string otherwise)
#> *default : "shout"
You’ll notice that our redefined option now uses our custom naming scheme for its environment variable.
You can always write your own function, or choose from some of the
pre-built ones in ?naming_formats
.
So far we’ve just been using the environment variable’s value as-is. Environment variable values, by default, will try to be parsed into R objects. If that fails, they’ll deliver the raw string value.
This can be a nice default behavior, handling many simple cases as expected
Environment Variable | Default options value |
---|---|
whisper |
[1] "whisper" (character) |
"shout" |
[1] "shout" (character) |
12345 |
[1] 12345 (numeric) |
TRUE |
[1] TRUE (logical) |
NULL |
NULL |
list(1, 'a') |
|
list(1, 'a',) (error!) |
[1] "list(1, 'a',)" (character) |
But you’ll notice that a typo in our last example completely changed the type of data that is read in. Depending on the way that you intend to use this variable, perhaps this default is more error-prone than necessary.
To help with this, there is a whole family of functions that allow
you to customize the way that environment variables are internalized as
option values (?envvar_fns
). Generally, it’s best to keep
these specific to the data type of the option that you intend to use -
for example, using envvar_is_true
to always coerce the
value to a logical scalar.
Just like before, these are used to specify your option’s behaviors:
define_option(
"volume",
default = TRUE,
desc = "Print output in uppercase (TRUE) or lowercase (FALSE)",
envvar_fn = envvar_is_true()
)
#>
#> volume = TRUE
#>
#> Print output in uppercase (TRUE) or lowercase (FALSE)
#>
#> option : globalenv.volume
#> envvar : GLOBALENV_VOLUME (TRUE if one of 'TRUE', '1', FALSE otherwise)
#> *default : TRUE
Of course you can define this function however you like.
Let’s put it all together. We’ll customize our environment variable
name, and provide a custom envvar_fn
which handles how we
interpret the raw environment variable value.
define_option(
"volume",
default = 1,
desc = paste0(
"Print output in uppercase (shout) or lowercase (whisper), or any ",
"number from 1-10 for random uppercasing"
),
envvar_name = "VOL",
envvar_fn = function(raw, ...) {
choice_of_nums <- envvar_choice_of(1:11)
switch(raw, shout = 10, whisper = 1, choice_of_nums(raw))
}
)
#>
#> volume = 1
#>
#> Print output in uppercase (shout) or lowercase (whisper), or any
#> number from 1-10 for random uppercasing
#>
#> option : globalenv.volume
#> envvar : VOL
#> *default : 1
Note that the
?envvar_fns
family of functions, likeenvvar_choice_of()
return functions. Although this is a very powerful mechanism of customizing behaviors, it can look odd at first glance.We first generate our function by giving it which values to choose from (
envvar_choice_of(1:11)
), then use that function when we have our raw value (choice_of_nums(raw)
).
Now we need to update our twist_and
function to work
with our newly consistent numeric volumes.
twist_and_shout <- function(vol = opt("volume")) {
lyric <- c(
"Well, shake it up, baby, now (Shake it up, baby)",
"Twist and shout (Twist and shout)"
)
# handle case where volume knob is broken
if (is.null(vol)) stop("someone turned off the stereo")
# randomly uppercase characters to match volume
lyric <- strsplit(tolower(lyric), "")
lyric <- lapply(lyric, function(line) {
char_sample <- runif(nchar(line)) < (vol - 1) / 9
line[char_sample] <- toupper(line[char_sample])
paste0(line, collapse = "")
})
# in case someone turns it up to 11
if (vol == 11) lyric <- gsub("(\\s*\\(|\\))", "!!!\\1", lyric)
cat(paste(lyric, collapse = "\n"), "\n")
}
Let’s try it out!
Sys.setenv(VOL = "whisper")
twist_and_shout()
#> well, shake it up, baby, now (shake it up, baby)
#> twist and shout (twist and shout)
Sys.setenv(VOL = 5)
twist_and_shout()
#> WElL, shaKe iT UP, bABy, nOw (ShaKe It up, bAbY)
#> twiST AND SHoUt (tWiST aNd sHOut)
Sys.setenv(VOL = "shout")
twist_and_shout()
#> WELL, SHAKE IT UP, BABY, NOW (SHAKE IT UP, BABY)
#> TWIST AND SHOUT (TWIST AND SHOUT)
Sys.setenv(VOL = 11)
twist_and_shout()
#> WELL, SHAKE IT UP, BABY, NOW!!! (SHAKE IT UP, BABY!!!)
#> TWIST AND SHOUT!!! (TWIST AND SHOUT!!!)
Sys.setenv(VOL = "off") # parsed as NULL
twist_and_shout()
#> Error in twist_and_shout(): someone turned off the stereo
Looks pretty good! We handle just the inputs we want, without having to worry about unexpected data slipping into our R code.