├── .Rbuildignore ├── .codecov.yml ├── .github ├── .gitignore └── README.md ├── .gitignore ├── .woodpecker └── r-pkg-standard.yaml ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── aaa.R ├── crayon.R ├── define_option.R ├── envvars.R ├── errors.R ├── naming.R ├── options_env.R ├── options_get.R ├── options_roxygen.R ├── options_spec.R └── utils.R ├── README.md ├── inst └── options.example │ ├── DESCRIPTION │ ├── LICENSE │ ├── NAMESPACE │ ├── R │ ├── helpers.R │ ├── options.R │ └── roxygen2.R │ └── man │ ├── fizzbuzz.Rd │ ├── hello.Rd │ ├── options.Rd │ ├── options_params.Rd │ └── show_option.Rd ├── man ├── as_params.Rd ├── as_roxygen_docs.Rd ├── assert_naming_fn_signature.Rd ├── defining_options.Rd ├── envvar_fns.Rd ├── err.Rd ├── format.option_spec.Rd ├── format.options_env.Rd ├── format.options_list.Rd ├── format_field.Rd ├── format_value.Rd ├── get_options_env.Rd ├── naming.Rd ├── naming_formats.Rd ├── opt.Rd ├── option_spec.Rd ├── options_env.Rd ├── options_fmts.Rd ├── pkgname.Rd └── reflow_option_desc.Rd ├── pkgdown ├── _pkgdown.yml └── extra.scss ├── tests ├── testthat.R └── testthat │ ├── setup.R │ ├── test-define-option.R │ ├── test-envvars.R │ ├── test-naming.R │ ├── test-opt.R │ ├── test-opt_set.R │ ├── test-opt_set_local.R │ ├── test-options_env.R │ ├── test-opts_list.R │ ├── test-output.R │ ├── test-precedence.R │ ├── test-rcmdcheck.R │ └── test-roxygen2.R └── vignettes ├── .gitignore ├── envvars.Rmd └── options.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^\.git$ 2 | ^\.github$ 3 | 4 | ^LICENSE\.md$ 5 | ^_pkgdown\.yml$ 6 | ^docs$ 7 | ^pkgdown$ 8 | 9 | ^inst/options\.example/man$ 10 | 11 | ^\.woodpecker$ 12 | ^\.codecov\.yml$ 13 | 14 | \.Rcheck$ 15 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | ci: 3 | - codeberg.org 4 | - ci.codeberg.org 5 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # :no_entry: Read-Only Mirror :no_entry: 2 | 3 | _This repository has moved to 4 | [codeberg.org/dgkf/options](https://codeberg.org/dgkf/options)_ 5 | 6 | Don't worry! This mirror is in place so that you can still file issues. 7 | That said, it would mean a lot to me if you joined the discussion over at 8 | [codeberg.org/dgkf/options](https://codeberg.org/dgkf/options). 9 | 10 | ## Why [codeberg](https://codeberg.org)? 11 | 12 | I build open-source tools because I want to give back to the general public 13 | by improving our collective software stack. Codeberg is an alternative to 14 | GitHub that is better aligned with this perspective. 15 | 16 | Codeberg is based on a fully open-source stack, is run by a non-profit, 17 | and has no other ancillary for-profit products that it is trying to sell using 18 | its platform. 19 | 20 | ## Why migrate `options`? 21 | 22 | Of all my tools, `options` is the one that is most specifically targetted at 23 | developers, and developers are the ones who need to spark a change in which 24 | platforms the open source community adopts. If you're here, you're the 25 | person who needs to drive the change toward better software ecosystem 26 | diversity and I believe the best alternative at this moment is 27 | [codeberg](https://codeberg.org). 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | inst/doc 3 | **.tar.gz 4 | **.Rcheck 5 | -------------------------------------------------------------------------------- /.woodpecker/r-pkg-standard.yaml: -------------------------------------------------------------------------------- 1 | # this workflow maintained at https://codeberg.org/r-codeberg/woodpecker-ci 2 | # please file bug reports and feature requests to help it improve! 3 | 4 | matrix: 5 | IMAGE: 6 | - rocker/verse 7 | 8 | when: 9 | - branch: "${CI_REPO_DEFAULT_BRANCH}" 10 | event: push 11 | - event: pull_request 12 | - event: tag 13 | 14 | variables: 15 | site_config: &site_config 16 | - export WOODPECKER_TMP=/woodpecker/tmp 17 | 18 | r_envvars: &r_envvars 19 | R_NCPU: 1 20 | RCMDCHECK_ERROR_ON: "note" 21 | 22 | _R_CHECK_FORCE_SUGGESTS_: "false" 23 | _R_CHECK_CRAN_INCOMING_: "false" 24 | _R_CHECK_SYSTEM_CLOCK_: "false" 25 | 26 | # CRAN OMP thread limit (https://stackoverflow.com/a/77323812) 27 | # Resolves issue with R CMD check for CPU time exceeding elapsed time 28 | OMP_THREAD_LIMIT: 2 29 | 30 | r_site_config: &r_site_config 31 | # These environment variables used to help find appropriate binaries in PPM 32 | - cat /etc/os-release >> "$(R RHOME)/etc/Renviron.site" 33 | 34 | # Store our temporary directory, accessible across workflow steps 35 | - echo "WOODPECKER_TMP=/woodpecker/tmp" >> "$(R RHOME)/etc/Renviron.site" 36 | 37 | # Configure R profile to use during ci 38 | # - sets options used throughout workflow 39 | # - configures a package library that will be re-used by each step 40 | - | 41 | cat << END >> $(R RHOME)/etc/Rprofile.site 42 | options( 43 | Ncpus = Sys.getenv("R_NCPU", unset = getOption("Ncpus", 1L)), 44 | repos = c( 45 | "p3m.dev" = sprintf( 46 | "https://p3m.dev/cran/__linux__/%s/latest", 47 | if (nchar(envvar <- Sys.getenv("VERSION_CODENAME"))) envvar 48 | ), 49 | CRAN = "https://cloud.r-project.org" 50 | ), 51 | pak.sysreqs = TRUE, 52 | 53 | # these settings used to provide prettier ci log output 54 | cli.dynamic = FALSE, 55 | cli.default_num_colors = 256, 56 | crayon.enabled = TRUE 57 | ) 58 | 59 | local({ 60 | tmp <- Sys.getenv("WOODPECKER_TMP") 61 | if (!dir.exists(tmp)) dir.create(tmp, recursive = TRUE) 62 | lib <- file.path("", "woodpecker", "lib") 63 | if (!dir.exists(lib)) dir.create(lib, recursive = TRUE) 64 | .libPaths(c(lib, .libPaths())) 65 | }) 66 | END 67 | 68 | step_setup: &setup 69 | - name: setup 70 | image: ${IMAGE} 71 | pull: true 72 | environment: 73 | <<: *r_envvars 74 | commands: 75 | - <<: *r_site_config 76 | - | 77 | R -q -s --no-save << "END" 78 | pak_repo <- sprintf( 79 | "https://r-lib.github.io/p/pak/stable/%s/%s/%s", 80 | .Platform$pkgType, 81 | R.Version()$os, 82 | R.Version()$arch 83 | ) 84 | 85 | install.packages("pak", repos = pak_repo) 86 | pak::local_install_dev_deps() 87 | END 88 | 89 | step_check: &check 90 | - name: check 91 | image: ${IMAGE} 92 | depends_on: [setup] 93 | pull: true 94 | environment: 95 | <<: *r_envvars 96 | commands: 97 | - <<: *r_site_config 98 | - | 99 | R -q -s --no-save << "END" 100 | pak::local_install_dev_deps() 101 | pak::pkg_install("rcmdcheck") 102 | rcmdcheck::rcmdcheck( 103 | args = c("--no-manual", "--as-cran", "--no-tests", "--timings"), 104 | build_args = c("--no-manual"), 105 | check_dir = "check" 106 | ) 107 | END 108 | 109 | step_test: &test 110 | - name: test 111 | image: ${IMAGE} 112 | depends_on: [setup] 113 | pull: true 114 | environment: 115 | <<: *r_envvars 116 | commands: 117 | - <<: *r_site_config 118 | - | 119 | R -q -s --no-save << "END" 120 | pak::local_install_dev_deps() 121 | pak::pkg_install(c("covr", "xml2")) 122 | 123 | woodpecker_tmp <- normalizePath(Sys.getenv("WOODPECKER_TMP"), winslash = "/") 124 | woodpecker_package <- file.path(woodpecker_tmp, "package") 125 | 126 | cov <- covr::package_coverage( 127 | quiet = FALSE, 128 | clean = FALSE, 129 | install_path = woodpecker_package 130 | ) 131 | 132 | woodpecker_cobertura_xml <- file.path(woodpecker_tmp, "cobertura.xml") 133 | covr::to_cobertura(cov, filename = woodpecker_cobertura_xml) 134 | END 135 | - | 136 | echo '─ test results ────────────────────────────────────────────────────────────────' 137 | find '/woodpecker/tmp/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 138 | 139 | step_pkgdown: &pkgdown 140 | - name: pkgdown 141 | image: ${IMAGE} 142 | depends_on: [setup] 143 | environment: 144 | <<: *r_envvars 145 | commands: 146 | - <<: *r_site_config 147 | - | 148 | R -q -s --no-save << "END" 149 | pak::local_install_dev_deps() 150 | pak::pkg_install(".") 151 | pak::pkg_install("pkgdown") 152 | woodpecker_tmp <- normalizePath(Sys.getenv("WOODPECKER_TMP"), winslash = "/") 153 | woodpecker_pages <- file.path(woodpecker_tmp, "pages") 154 | pkg <- pkgdown::as_pkgdown(".", override = list(destination = woodpecker_pages)) 155 | pkgdown::build_site(pkg, new_process = FALSE, install = FALSE) 156 | END 157 | 158 | step_deploy: &deploy 159 | - name: deploy 160 | image: alpine/git 161 | depends_on: [pkgdown] 162 | when: 163 | - branch: "${CI_REPO_DEFAULT_BRANCH}" 164 | evaluate: "DEPLOY_SSH_KEY != nil" 165 | event: push 166 | environment: 167 | <<: *r_envvars 168 | DEPLOY_SSH_KEY: 169 | # expects woodpecker ci ssh access 170 | # - generated with `ssh-keygen -t ed25519 -f /tmp/key` 171 | # - private key: store as woodpecker ci secret `deploy_ssh_key`, 172 | # optionally set only accessible in `alpine/git` image 173 | # - public key: provide as a Deploy Key in codeberg repository 174 | # settings, granting write access to repository 175 | from_secret: deploy_ssh_key 176 | commands: 177 | - <<: *site_config 178 | # copy author details of last commit 179 | - export GIT_USER_NAME=$$(git log -1 --pretty=format:'%an') 180 | - export GIT_USER_EMAIL=$$(git log -1 --pretty=format:'%ae') 181 | 182 | # set up ssh key, apply known hosts 183 | - eval $(ssh-agent -s) 184 | - echo "$${DEPLOY_SSH_KEY}" | ssh-add - 185 | - mkdir -p ~/.ssh 186 | - ssh-keyscan -H codeberg.org >> ~/.ssh/known_hosts 187 | 188 | # clone repo, create new branch, copy in pages 189 | - git clone --depth 1 $${CI_REPO_CLONE_SSH_URL} $${WOODPECKER_TMP}/deploy 190 | - cd $${WOODPECKER_TMP}/deploy 191 | - git switch --orphan pages 192 | - git pull origin pages || echo "pages branch not found, creating ..." 193 | - git rm --cached --ignore-unmatch -r . 194 | - cp -r $${WOODPECKER_TMP}/pages/* . 195 | 196 | # commit new pages contents & deploy 197 | - git add -A 198 | - git config --global user.name $${GIT_USER_NAME} 199 | - git config --global user.email $${GIT_USER_EMAIL} 200 | - git commit -m "deploy to pages from $${CI_COMMIT_BRANCH} @ $${CI_COMMIT_SHA:0:8}" 201 | - git push origin pages 202 | 203 | steps: 204 | - <<: *setup 205 | - <<: *check 206 | - <<: *test 207 | - <<: *pkgdown 208 | - <<: *deploy 209 | 210 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: options 2 | Title: Simple, Consistent Package Options 3 | Version: 0.3.1 4 | Authors@R: 5 | person( 6 | "Doug", 7 | "Kelkhoff", 8 | email = "doug.kelkhoff@gmail.com", 9 | role = c("aut", "cre") 10 | ) 11 | Description: 12 | Simple mechanisms for defining and interpreting package options. Provides 13 | helpers for interpreting environment variables, global options, defining 14 | default values and more. 15 | License: 16 | MIT + file LICENSE 17 | URL: 18 | https://dgkf.github.io/options/, 19 | https://codeberg.org/dgkf/options 20 | BugReports: 21 | https://codeberg.org/dgkf/options/issues 22 | Imports: 23 | utils 24 | Suggests: 25 | crayon, 26 | knitr, 27 | rmarkdown, 28 | roxygen2, 29 | rcmdcheck, 30 | pkgload, 31 | withr, 32 | testthat (>= 3.0.0) 33 | Config/Needs/website: 34 | pkgdown 35 | Roxygen: 36 | list(markdown = TRUE) 37 | VignetteBuilder: 38 | knitr 39 | Encoding: UTF-8 40 | LazyData: true 41 | RoxygenNote: 7.3.2 42 | Config/testthat/edition: 3 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: options authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 options authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(as.list,options_env) 4 | S3method(as_check_names_fn,"function") 5 | S3method(as_check_names_fn,character) 6 | S3method(conditionCall,options_error) 7 | S3method(define_option,character) 8 | S3method(define_option,option_spec) 9 | S3method(envvar_is,"NULL") 10 | S3method(envvar_is,character) 11 | S3method(envvar_is,default) 12 | S3method(envvar_is,logical) 13 | S3method(envvar_is,numeric) 14 | S3method(format,option_spec) 15 | S3method(format,options_env) 16 | S3method(get_options_env,default) 17 | S3method(get_options_env,options_env) 18 | S3method(get_options_env,options_list) 19 | S3method(opts,"NULL") 20 | S3method(opts,character) 21 | S3method(opts,list) 22 | S3method(print,option_spec) 23 | S3method(print,options_env) 24 | S3method(print,options_list) 25 | export("opt<-") 26 | export(as_params) 27 | export(as_roxygen_docs) 28 | export(define_option) 29 | export(define_options) 30 | export(envvar_choice_of) 31 | export(envvar_eval) 32 | export(envvar_eval_or_raw) 33 | export(envvar_is) 34 | export(envvar_is_false) 35 | export(envvar_is_one_of) 36 | export(envvar_is_set) 37 | export(envvar_is_true) 38 | export(envvar_name_default) 39 | export(envvar_name_generic) 40 | export(envvar_str_split) 41 | export(get_options_env) 42 | export(opt) 43 | export(opt_set) 44 | export(opt_source) 45 | export(option_name_default) 46 | export(option_spec) 47 | export(opts) 48 | export(opts_list) 49 | export(set_envvar_name_fn) 50 | export(set_option_name_fn) 51 | importFrom(utils,capture.output) 52 | importFrom(utils,packageName) 53 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # options 0.3.1 2 | 3 | ## Bug Fixes 4 | 5 | * Fixed `define_options` when using a `NULL` default value. Previously, 6 | the `NULL` value would not be recognized and would silently ignore the 7 | option definition. (@dgkf #34) 8 | 9 | * Fixed `envvar_is.logical` (and thereby, `envvar_is_true`, `envvar_is_false`), 10 | leveraging `as.logical` internally to make the parsing from strings align 11 | more closely to what one may expect in R. Notably, all lowercase values 12 | `true` and `false` will now be recognized as their respective logical values. 13 | (@dgkf #34) 14 | 15 | # options 0.3.0 16 | 17 | * Introduces `opts_list()`, a utility for producing a list of option values with 18 | appropriate global option names that can be used more readily with 19 | `options()` and `withr::with_options()`. (@dgkf #19) 20 | 21 | ## Bug Fixes 22 | 23 | * Fixed `envvar_str_split()` not making us of `delim` parameter. (@dgkf #23) 24 | 25 | # options 0.2.0 26 | 27 | * Fixes `opts()`, which would previously return default values after being 28 | updated. Will now appropriately return values just as they would be fetched 29 | using `opt()`. (@dgkf #17) 30 | 31 | * Exposes `get_options_env()` (currently experimental), for the purpose of 32 | accessing a listing of option names. (@dgkf #17) 33 | 34 | * Adds an optional `option_fn` parameter to `option_spec`, allowing for the 35 | stored option values to be processed, or to produce side-effects when 36 | accessed. (@dgkf #12) 37 | 38 | ## Breaking Changes 39 | 40 | * The result of `opt_source()` when a value is derived from an environment 41 | variable was changed from `"envir"` to `"envvar"` to be more consistent with 42 | the rest of the package's messaging about sources. (@dgkf #12) 43 | 44 | # options 0.1.0 45 | 46 | * Adds various utility functions for modifying options: `opt_set()`, `opt()<-` 47 | and `opt_set_local()`. 48 | 49 | * Trying to retrieve an option that is not yet defined will now default to 50 | throwing a warning. This behavior can be modified using the `on_missing` 51 | argument to functions that fetch option values. 52 | 53 | # options 0.0.2 54 | 55 | * `opts()` slightly refactored to produce more constructive output when no 56 | option names are provided. You can now use `opts(env = package_name)` to 57 | fetch a full named list of option values. (@dgkf #2) 58 | 59 | * Generated `roxygen2` documentation using `as_roxygen_docs()` is now more 60 | consciencious about `R CMD check` requirements, moving `\usage{}` to a new 61 | section titled "Checking Option Values". (@dgkf #2) 62 | 63 | # options 0.0.1 64 | 65 | * `options` split from `dgkf/devutils` 66 | -------------------------------------------------------------------------------- /R/aaa.R: -------------------------------------------------------------------------------- 1 | CONST_OPTIONS_ENV_NAME <- ".options" 2 | CONST_OPTIONS_META <- ".meta" 3 | -------------------------------------------------------------------------------- /R/crayon.R: -------------------------------------------------------------------------------- 1 | #' Optional Crayon Handling 2 | #' 3 | #' Generate a list of styling functions using `crayon`, while safely falling 4 | #' back to non-`crayon` output when `crayon` is unavailable. 5 | #' 6 | #' @return formatted text 7 | #' 8 | #' @keywords internal 9 | options_fmts <- function() { 10 | crayon <- tryCatch(getNamespace("crayon"), error = function(e) list()) 11 | 12 | italic <- crayon$italic %||% identity 13 | bold <- crayon$bold %||% identity 14 | blue <- crayon$blue %||% identity 15 | cyan <- crayon$cyan %||% identity 16 | yellow <- crayon$yellow %||% identity 17 | green <- crayon$green %||% identity 18 | silver <- crayon$silver %||% identity 19 | 20 | fmt <- list() 21 | fmt$name <- function(x) italic(bold(blue(x))) 22 | fmt$desc <- identity 23 | fmt$field_inactive <- identity 24 | fmt$field_active <- green 25 | fmt$optname <- cyan 26 | fmt$fade <- silver 27 | fmt$shorthand <- function(x) italic(blue(x)) 28 | 29 | fmt 30 | } 31 | -------------------------------------------------------------------------------- /R/define_option.R: -------------------------------------------------------------------------------- 1 | #' Defining Options 2 | #' 3 | #' Define options which can be used throughout your package. 4 | #' 5 | #' At their simplest, defining options lets you refer to a global option using a 6 | #' shorthand option name throughout your package, with the added benefit of 7 | #' looking for configurations in global options and environment variables. 8 | #' 9 | #' @param option An option name to use 10 | #' 11 | #' @return the package options environment 12 | #' 13 | #' @examples 14 | #' define_options( 15 | #' "Whether execution should emit console output", 16 | #' quiet = FALSE, 17 | #' "Whether to use detailed console output (showcasing additional 18 | #' configuration parameters)", 19 | #' verbose = TRUE, 20 | #' envvar_fn = envvar_is_true() 21 | #' ) 22 | #' 23 | #' define_option( 24 | #' "deprecations", 25 | #' desc = "Whether deprecation warnings should be suppressed automatically", 26 | #' default = FALSE, 27 | #' option_name = "MypackageDeprecations", 28 | #' envvar_name = "MYPACKAGE_ENVVARS_DEPRECATIONS" 29 | #' ) 30 | #' 31 | #' @name defining_options 32 | #' @rdname defining_options 33 | NULL 34 | 35 | #' @describeIn defining_options 36 | #' 37 | #' Define an option. Unlike [define_options()], this function allows detailed 38 | #' customization of all option behaviors. Accepts either an [option_spec()] 39 | #' object, or an option named followed by arguments to provide to 40 | #' [option_spec()]. 41 | #' 42 | #' @param ... Additional arguments passed to [option_spec()] 43 | #' 44 | #' @export 45 | define_option <- function(option, ...) { 46 | UseMethod("define_option") 47 | } 48 | 49 | #' @inheritParams option_spec 50 | #' @export 51 | define_option.character <- function( 52 | option, 53 | default = bquote(), 54 | ..., 55 | quoted = FALSE, 56 | eager = FALSE, 57 | envir = parent.frame() 58 | ) { 59 | if (!missing(default) && !quoted && !eager) { 60 | default <- match.call()[["default"]] 61 | } 62 | 63 | if (quoted && eager) { 64 | default <- eval(default, envir = envir) 65 | } 66 | 67 | define_option(option_spec( 68 | name = option, 69 | default = default, 70 | ..., 71 | quoted = TRUE, 72 | envir = envir 73 | )) 74 | } 75 | 76 | #' @export 77 | define_option.option_spec <- function(option, ...) { 78 | optenv <- get_options_env(option$envir, inherits = TRUE) 79 | do.call(delayedAssign, list(option$name, option$expr, option$envir, optenv)) 80 | set_option_spec(option$name, option, env = optenv) 81 | optenv 82 | } 83 | 84 | 85 | 86 | #' @describeIn defining_options 87 | #' 88 | #' Define multiple options. This function provides a shorthand syntax for 89 | #' succinctly defining many options. Arguments are defined in groups, each 90 | #' starting with an unnamed description argument. For more details see Section 91 | #' _Non-Standard Evaluation_. 92 | #' 93 | #' @section Non-Standard Evaluation: 94 | #' 95 | #' `define_options()` accepts arguments in a _non-standard_ 96 | #' way, as groups of arguments which each are used to specify an option (See 97 | #' `options_spec()`). Groups of arguments must start with an unnamed argument, 98 | #' which provides the description for the argument, followed immediately by a 99 | #' named argument providing the name of option and default value, followed by 100 | #' any additional arguments to provie to `options_spec()`. 101 | #' 102 | #' The environment in which options are defined is always assumed to be the 103 | #' parent environment. If you'd prefer to specify options in a different 104 | #' environment, this is best done using `define_option()` or 105 | #' `with(, define_options(...))`. 106 | #' 107 | #' Although `define_options()` provides all the functionality of 108 | #' `define_option()` in a succinct shorthand, it is only recommended in cases 109 | #' where the overwhelming majority of your options leverage default behaviors. 110 | #' It is encouraged to use `define_option()` if you repeatedly need more 111 | #' involved definitions to minimize non-standard evaluation bugs. 112 | #' 113 | #' @export 114 | define_options <- function(...) { 115 | eval_env <- parent.frame() 116 | x <- as.list(substitute(...())) 117 | 118 | # always use named arguments, even if no names are used 119 | if (is.null(names(x))) { 120 | names(x) <- rep("", length(x)) 121 | } 122 | 123 | # test against common non-standard eval syntax issues 124 | verify_define_options_syntax(x) 125 | 126 | # split arguments into groupings, building `option_spec` args 127 | specs <- lapply(split(x, cumsum(names(x) == "")), function(group) { 128 | # reassign option name, default from second arg in group 129 | args <- list() 130 | args$name <- names(group[2]) 131 | args["default"] <- list(group[[2]]) 132 | 133 | # build description from first (unnamed) arg in group 134 | args$desc <- reflow_option_desc(eval(group[[1]], envir = eval_env))[[1]] 135 | 136 | # build other arguments from remaining args 137 | other_args <- lapply(group[c(-1, -2)], eval, envir = eval_env) 138 | args[names(other_args)] <- other_args 139 | 140 | do.call(option_spec, args, envir = eval_env) 141 | }) 142 | 143 | for (spec in specs) define_option(spec) 144 | get_options_env(eval_env, inherits = TRUE) 145 | } 146 | 147 | 148 | 149 | verify_define_options_syntax <- function(x) { 150 | no_desc <- names(x)[[1]] != "" 151 | no_arg <- names(x) == "" & vlapply(x, function(i) all(nchar(i) == 0)) 152 | no_named_arg <- names(x) == "" & c(names(x)[-1] == "", TRUE) 153 | arg_desc <- c(names(x)[-length(x)] != "" & names(x)[-1] == "desc", FALSE) 154 | arg_name <- c(names(x)[-length(x)] != "" & names(x)[-1] == "name", FALSE) 155 | 156 | if (!any(no_desc | no_named_arg | arg_desc | arg_name)) { 157 | return(TRUE) 158 | } 159 | 160 | # helper for creating an itemized "issue" message as part of error message 161 | opt_n <- cumsum(names(x) != "" & c(TRUE, names(x)[-length(x)] == "")) 162 | issue <- function(at, ..., verbatim = FALSE) { 163 | if (!any(at)) { 164 | return(NULL) 165 | } 166 | opts <- paste0(unique(opt_n[which(at)]), collapse = ",") 167 | s <- if (sum(at) > 1) "s" else "" 168 | 169 | msg <- paste0(...) 170 | if (!verbatim) msg <- sprintf("option%s (%s) %s", s, opts, msg) 171 | 172 | out <- list(paste0(msg, collapse = "\n")) 173 | names(out) <- opts 174 | out 175 | } 176 | 177 | issues <- Filter(Negate(is.null), c( 178 | issue(no_arg, 179 | verbatim = TRUE, 180 | sprintf("missing argument %s (trailing comma in call)", which(no_arg)[1]) 181 | ), 182 | issue( 183 | no_desc, 184 | "should begin with an unnamed argument, providing a description of the ", 185 | "option's behavior." 186 | ), 187 | issue( 188 | !no_arg & no_named_arg, 189 | "should always follow the description with a named argument to indicate ", 190 | "the option name and default value." 191 | ), 192 | issue( 193 | arg_desc, 194 | "should not provide a redundant `desc` argument." 195 | ), 196 | issue( 197 | arg_name, 198 | "should not provide a redundant `name` argument." 199 | ) 200 | )) 201 | 202 | err("found issues in option definitions", issues, which = -1) 203 | } 204 | 205 | 206 | 207 | #' Reflow multiline strings 208 | #' 209 | #' A small helper function for allowing multiline strings to be collapsed into 210 | #' continuous lines, similar to markdown's paragraph handling. 211 | #' 212 | #' @param x A vector of multiline strings to reflow 213 | #' @return The reflowed strings 214 | #' 215 | #' @keywords internal 216 | reflow_option_desc <- function(x) { 217 | x <- strsplit(x, "\n{2,}\\s*") 218 | x <- lapply(x, paste, collapse = "\n") 219 | x <- lapply(x, gsub, pattern = "^\\s+|\\s+$", replacement = "") 220 | x 221 | } 222 | -------------------------------------------------------------------------------- /R/envvars.R: -------------------------------------------------------------------------------- 1 | fn_with_desc <- function(f, desc) { 2 | attr(f, "desc") <- desc 3 | f 4 | } 5 | 6 | envvar_fn_emit_warning <- function(name) { 7 | warning(sprintf( 8 | paste0( 9 | "Environment variable '%s' produced an error while interpretting its ", 10 | "value. Environment variable will be ignored." 11 | ), 12 | name 13 | )) 14 | } 15 | 16 | 17 | 18 | #' Generator functions for environment variable processors 19 | #' 20 | #' These functions return environment variable processor functions. Arguments to 21 | #' them are used to specify behaviors. 22 | #' 23 | #' @param value A value to test against 24 | #' @param values A list or vector of values to match 25 | #' @param default A default value used when a value cannot be coerced from the 26 | #' environment variable value 27 | #' @param case_sensitive A logical value indicating whether string comparisons 28 | #' should be case-sensitive. 29 | #' @param delim A character value to use as a delimiter to use when splitting 30 | #' the environment variable value 31 | #' @param ... Other arguments unused 32 | #' @return A function to be used for processing an environment variable value 33 | #' 34 | #' @name envvar_fns 35 | #' @rdname envvar_fns 36 | #' 37 | #' @keywords envvar_parsers 38 | #' 39 | NULL 40 | 41 | #' @describeIn envvar_fns 42 | #' Test for equality with handlers for most atomic R types, performing sensible 43 | #' interpretation of environment variable values. 44 | #' @export 45 | envvar_is <- function(value, ...) { 46 | UseMethod("envvar_is", value) 47 | } 48 | 49 | #' @export 50 | envvar_is.default <- function(value, ...) { 51 | fn_with_desc( 52 | function(raw, ...) { 53 | tryCatch( 54 | identical(value, eval(parse(text = raw))), 55 | error = function(e) FALSE 56 | ) 57 | }, 58 | sprintf("TRUE if evaluated value is identical to '%s'", format(value)) 59 | ) 60 | } 61 | 62 | #' @describeIn envvar_fns 63 | #' environment variable has value `"null"` 64 | #' @export 65 | envvar_is.NULL <- function(value, case_sensitive = FALSE, ...) { 66 | fn_with_desc( 67 | function(raw, ...) { 68 | tryCatch( 69 | is.null(eval(parse(text = toupper(raw)))), 70 | error = function(e) FALSE 71 | ) 72 | }, 73 | sprintf( 74 | "TRUE if value is 'null'%s, FALSE otherwise", 75 | if (case_sensitive) "" else " (case insensitive)" 76 | ) 77 | ) 78 | } 79 | 80 | #' @describeIn envvar_fns 81 | #' environment variable is equal to string `value` 82 | #' @export 83 | envvar_is.character <- function(value, case_sensitive = FALSE, ...) { 84 | stopifnot(length(value) == 1) 85 | fn_with_desc( 86 | function(raw, ...) { 87 | if (case_sensitive) isTRUE(value == raw) 88 | else isTRUE(toupper(value) == toupper(raw)) 89 | }, 90 | sprintf( 91 | "TRUE if equal to '%s'%s", 92 | value, 93 | if (case_sensitive) "" else " (case insensitive)" 94 | ) 95 | ) 96 | } 97 | 98 | #' @describeIn envvar_fns 99 | #' environment variable is equal to string representation of numeric `value` 100 | #' @export 101 | envvar_is.numeric <- function(value, ...) { 102 | stopifnot(length(value) == 1) 103 | fn_with_desc( 104 | function(raw, ...) { 105 | tryCatch( 106 | value == as.numeric(raw), 107 | warning = function(w) FALSE 108 | ) 109 | }, 110 | paste0("TRUE if equal to ", value) 111 | ) 112 | } 113 | 114 | #' @describeIn envvar_fns 115 | #' environment variable is equal to string representation of logical `value` 116 | #' @export 117 | envvar_is.logical <- function(value, case_sensitive = FALSE, ...) { 118 | stopifnot(length(value) == 1) 119 | fn_with_desc( 120 | function(raw, ...) { 121 | if (!case_sensitive) { 122 | raw <- toupper(raw) 123 | } 124 | 125 | tryCatch( 126 | identical(value, as.logical(raw)), 127 | error = function(e) FALSE 128 | ) 129 | }, 130 | sprintf( 131 | "TRUE if value is '%s'%s, FALSE otherwise", 132 | format(value), 133 | if (case_sensitive) "" else " (case insensitive)" 134 | ) 135 | ) 136 | } 137 | 138 | #' @describeIn envvar_fns 139 | #' Parse the environment variable value as R code and and evaluate it to 140 | #' produce a return value, emitting an error if the expression fails to parse 141 | #' or evaluate. This option is a sensible default for most R-specific 142 | #' environment variables, but may fail for string literals, and meaningful 143 | #' values that don't conform to R's syntax like `"true`" (see 144 | #' [envvar_is_true()]), `"false"` (see [envvar_is_false()]) or `"null"`. 145 | #' @export 146 | envvar_eval <- function(...) { 147 | fn_with_desc( 148 | function(raw, name, ...) { 149 | parse_error_fmt <- paste0( 150 | "Environment variable '%s' could not be parsed into a valid R ", 151 | "expression" 152 | ) 153 | 154 | tryCatch( 155 | eval(parse(text = raw)), 156 | error = function(e) stop(sprintf(parse_error_fmt, name)) 157 | ) 158 | }, 159 | "as evaluated expression" 160 | ) 161 | } 162 | 163 | #' @describeIn envvar_fns 164 | #' Parse the environment variable value as R code and and evaluate it to 165 | #' produce a return value, or falling back to the raw value as a string if an 166 | #' error occurs. 167 | #' @export 168 | envvar_eval_or_raw <- function(...) { 169 | fn_with_desc( 170 | function(raw, name, ...) { 171 | tryCatch(eval(parse(text = raw)), error = function(e) raw) 172 | }, 173 | "evaluated if possible, raw string otherwise" 174 | ) 175 | } 176 | 177 | #' @describeIn envvar_fns 178 | #' For meaningful string comparisons, check whether the environment variable is 179 | #' equal to some meaningful string. Optionally with case-sensitivity. 180 | #' @export 181 | envvar_is_one_of <- function(values, ...) { 182 | msg <- sprintf( 183 | "TRUE if %s, FALSE otherwise", 184 | if (length(values) == 1) { 185 | paste0("'", values[[1]], "'") 186 | } else { 187 | paste0("one of ", paste0("'", as.character(values), "'", collapse = ", ")) 188 | } 189 | ) 190 | 191 | fn <- function(v) do.call(envvar_is, list(v, ...)) 192 | 193 | fn_with_desc( 194 | function(raw, ...) { 195 | for (v in values) { 196 | if (isTRUE(fn(v)(raw, ...))) return(TRUE) 197 | } 198 | FALSE 199 | }, 200 | msg 201 | ) 202 | } 203 | 204 | #' @describeIn envvar_fns 205 | #' Check whether environment variable can be coerced to match one of `values`, 206 | #' returning the value if it matches or `default` otherwise. 207 | #' @export 208 | envvar_choice_of <- function(values, default = NULL, ...) { 209 | msg <- sprintf( 210 | "%s as value, NULL otherwise", 211 | if (length(values) == 1) { 212 | paste0("'", values[[1]], "'") 213 | } else { 214 | paste0("one of ", paste0("'", as.character(values), "'", collapse = ", ")) 215 | } 216 | ) 217 | 218 | fn <- function(v) do.call(envvar_is, list(v, ...)) 219 | 220 | fn_with_desc( 221 | function(raw, ...) { 222 | for (value in values) if (fn(value)(raw, ...)) return(value) 223 | default 224 | }, 225 | msg 226 | ) 227 | } 228 | 229 | #' @describeIn envvar_fns 230 | #' Test whether the environment variable is "truthy", that is whether it is 231 | #' case-insensitive `"true"` or `1` 232 | #' @export 233 | envvar_is_true <- function(...) { 234 | envvar_is_one_of(list(TRUE, 1), ...) 235 | } 236 | 237 | #' @describeIn envvar_fns 238 | #' Test whether the environment variable is "falsy", that is whether it is 239 | #' case-insensitive `"false"` or `0` 240 | #' @export 241 | envvar_is_false <- function(...) { 242 | envvar_is_one_of(list(FALSE, 0), ...) 243 | } 244 | 245 | #' @describeIn envvar_fns 246 | #' Test whether the environment variable is set. This is somewhat 247 | #' operating-system dependent, as not all operating systems can distinguish 248 | #' between an empty string as a value and an unset environment variable. For 249 | #' details see [Sys.getenv()]'s Details about its `unset` parameter. 250 | #' @export 251 | envvar_is_set <- function(...) { 252 | fn_with_desc( 253 | function(raw, ...) TRUE, 254 | "TRUE if set, FALSE otherwise" 255 | ) 256 | } 257 | 258 | #' @describeIn envvar_fns 259 | #' Interpret the environment variable as a delimited list of strings, such as 260 | #' `PATH` variables. 261 | #' @export 262 | envvar_str_split <- function(delim = ";", ...) { 263 | fn_with_desc( 264 | function(raw, ...) trimws(strsplit(raw, delim)[[1L]]), 265 | sprintf("as character vector, split on '%s' delimiter", delim) 266 | ) 267 | } 268 | -------------------------------------------------------------------------------- /R/errors.R: -------------------------------------------------------------------------------- 1 | #' Raise a package error 2 | #' 3 | #' @param title A title for the error 4 | #' @param issues An optionally named list of issues to associate with the error. 5 | #' When named, the issues are first sorted by issue name. 6 | #' @param which A relative frame to use to build the associated call 7 | #' @return An options_error class 8 | #' 9 | #' @keywords internal 10 | err <- function(title, issues = list(), which = 0) { 11 | which <- max(which, -sys.nframe() + 2) 12 | call <- sys.call(which = which - 1) 13 | w <- getOption("width", 80) 14 | 15 | # order issues by relevant option definition 16 | issues <- Filter(Negate(is.null), issues) 17 | if (is.null(names(issues))) names(issues) <- rep("", length(issues)) 18 | if (length(issues)) issues <- issues[order(names(issues))] 19 | 20 | # apply indentation and wrap lines 21 | issues <- lapply(issues, function(msg) { 22 | msg <- strwrap(width = w - 5, indent = 4, exdent = 6, paste0("* ", msg)) 23 | paste0(msg, collapse = "\n") 24 | }) 25 | 26 | title <- paste0(" ", title) 27 | stop(structure( 28 | list(message = paste0(c("", title, issues), collapse = "\n"), call = call), 29 | class = c("options_error", "error", "condition") 30 | )) 31 | } 32 | 33 | 34 | 35 | #' @export 36 | conditionCall.options_error <- function(c) { 37 | fn <- call("::", quote(options), c$call[[1]]) 38 | call <- as.call(list(fn)) 39 | attributes(call) <- attributes(c$call) 40 | call 41 | } 42 | -------------------------------------------------------------------------------- /R/naming.R: -------------------------------------------------------------------------------- 1 | #' Define Naming Conventions 2 | #' 3 | #' Option naming conventions use sensible defaults so that you can get started 4 | #' quickly with minimal configuration. 5 | #' 6 | #' @section Defaults: 7 | #' 8 | #' Given a package `mypackage` and option `myoption`, the default settings 9 | #' will generate options and environment variables using the convention: 10 | #' 11 | #' option: 12 | #' 13 | #' ``` 14 | #' mypackage.myoption 15 | #' ``` 16 | #' 17 | #' environment variable: 18 | #' 19 | #' ``` 20 | #' R_MYPACKAGE_MYOPTION 21 | #' ``` 22 | #' 23 | #' This convention is intended to track closely with how options and environment 24 | #' variables are handled frequently in the wild. Perhaps in contrast to the 25 | #' community conventions, an `R_` prefix is tacked on to the default environment 26 | #' variables. This prefix helps to differentiate environment variables when 27 | #' similarly named tools exist outside of the R ecosystem. 28 | #' 29 | #' @section Setting Alternative Conventions: 30 | #' 31 | #' If you choose to use alternative naming conventions, you must set the 32 | #' callback function _before_ defining options. This is best achieved by 33 | #' altering these settings in the file where you define your options. 34 | #' 35 | #' If you choose to break up your options across multiple files, then it is best 36 | #' to define the collate order for your R scripts to ensure that the options are 37 | #' consistently configured across operating systems. 38 | #' 39 | #' @param fn A callback function which expects two arguments, the package name 40 | #' and option name, and returns a single character value to use as an 41 | #' environment variable name. 42 | #' @param env An environment in which to search for options settings 43 | #' @return The callback function `fn` 44 | #' 45 | #' @examples 46 | #' set_envvar_name_fn(envvar_name_generic) 47 | #' 48 | #' set_envvar_name_fn(function(package, name) { 49 | #' toupper(paste("ENV", package, name, sep = "_")) 50 | #' }) 51 | #' 52 | #' @seealso naming_formats 53 | #' @name naming 54 | #' 55 | #' @keywords naming 56 | NULL 57 | 58 | 59 | 60 | #' Assert signature for naming functions 61 | #' 62 | #' @param fn A function to inspect 63 | #' @return NULL 64 | #' 65 | #' @keywords internal 66 | assert_naming_fn_signature <- function(fn) { 67 | if (length(formals(fn)) < 2) 68 | err("naming functions must accept at least two arguments", which = -1) 69 | } 70 | 71 | 72 | 73 | #' @describeIn naming 74 | #' Set a callback function to use to format environment variable names. 75 | #' @export 76 | set_envvar_name_fn <- function(fn, env = parent.frame()) { 77 | assert_naming_fn_signature(fn) 78 | optenv <- get_options_env(env) 79 | attr(optenv, "envvar_name_fn") <- fn 80 | } 81 | 82 | #' @describeIn naming 83 | #' Set a callback function to use to format option names. 84 | #' @export 85 | set_option_name_fn <- function(fn, env = parent.frame()) { 86 | assert_naming_fn_signature(fn) 87 | optenv <- get_options_env(env) 88 | attr(optenv, "option_name_fn") <- fn 89 | } 90 | 91 | 92 | 93 | get_option_name_fn <- function(env = parent.frame()) { 94 | optenv <- get_options_env(env, inherits = TRUE) 95 | attr(optenv, "option_name_fn") 96 | } 97 | 98 | get_envvar_name_fn <- function(env = parent.frame()) { 99 | optenv <- get_options_env(env, inherits = TRUE) 100 | attr(optenv, "envvar_name_fn") 101 | } 102 | 103 | 104 | 105 | #' Naming Convention Formatters 106 | #' 107 | #' This family of functions is used internally to generate global option and 108 | #' environment variable names from the package name and internal option name. 109 | #' 110 | #' @param package,option The package name and internal option name used for 111 | #' generating a global R option and environment variable name. As these 112 | #' functions are often provided as values, their arguments rarely need to be 113 | #' provided by package authors directly. 114 | #' @return A character value to use as the global option name or environment 115 | #' variable name 116 | #' 117 | #' @name naming_formats 118 | #' @seealso naming 119 | #' 120 | #' @keywords naming_formats 121 | #' 122 | NULL 123 | 124 | #' @usage option_name_default(package, option) # "package.option" 125 | #' @describeIn naming_formats 126 | #' A default naming convention, producing a global R option name from the 127 | #' package name and internal option name (`mypackage.myoption`) 128 | #' @family naming_formats 129 | #' @export 130 | option_name_default <- function(package, option) { 131 | paste(c(package, option), collapse = ".") 132 | } 133 | 134 | #' @usage envvar_name_default(package, option) # "R_PACKAGE_OPTION" 135 | #' @describeIn naming_formats 136 | #' A default naming convention, producing an environment variable name from the 137 | #' package name and internal option name (`R_MYPACKAGE_MYOPTION`) 138 | #' @family naming_formats 139 | #' @export 140 | envvar_name_default <- function(package, option) { 141 | parts <- c("R", package, option) 142 | paste(gsub("[^A-Z0-9]", "_", toupper(parts)), collapse = "_") 143 | } 144 | 145 | #' @usage envvar_name_generic(package, option) # "PACKAGE_OPTION" 146 | #' @describeIn naming_formats 147 | #' A generic naming convention, producing an environment variable name from the 148 | #' package name and internal option name. Useful when a generic convention might 149 | #' be used to share environment variables with other tools of the same name, or 150 | #' when you're confident that your R package will not conflict with other tools. 151 | #' (`MYPACKAGE_MYOPTION`) 152 | #' @family naming_formats 153 | #' @export 154 | envvar_name_generic <- function(package, option) { 155 | parts <- c(package, option) 156 | paste(gsub("[^A-Z0-9]", "_", toupper(parts)), collapse = "_") 157 | } 158 | 159 | 160 | as_check_names_fn <- function(x) { 161 | UseMethod("as_check_names_fn") 162 | } 163 | 164 | #' @export 165 | as_check_names_fn.character <- function(x) { 166 | switch( 167 | x[[1]], 168 | "warn" = check_names_warn_missing, 169 | "error" = check_names_stop_missing, 170 | "asis" = identity 171 | ) 172 | } 173 | 174 | #' @export 175 | as_check_names_fn.function <- function(x) { 176 | x 177 | } 178 | 179 | check_names_warn_missing <- function(optnames, env = parent.frame()) { 180 | valid <- names(get_options_spec(env)) 181 | if (length(miss <- setdiff(optnames, valid)) > 0) { 182 | warning( 183 | "Option name(s) not found in environment: ", 184 | paste0("'", miss, "'", collapse = ", ") 185 | ) 186 | } 187 | } 188 | 189 | check_names_stop_missing <- function(optnames, env = parent.frame()) { 190 | valid <- names(get_options_spec(env)) 191 | if (length(miss <- setdiff(optnames, valid)) > 0) { 192 | stop( 193 | "Option name(s) not found in environment: ", 194 | paste0("'", miss, "'", collapse = ", ") 195 | ) 196 | } 197 | } 198 | 199 | check_names_asis <- function(optnames, env = parent.frame()) { 200 | } 201 | -------------------------------------------------------------------------------- /R/options_env.R: -------------------------------------------------------------------------------- 1 | #' Options Environment Class 2 | #' 3 | #' The options environment stores primarily, the default values for options. In 4 | #' addition, it stores metadata pertaining to each option in the form of 5 | #' attributes. 6 | #' 7 | #' @section Attributes: 8 | #' - `spec`: A named list of option specifications 9 | #' - `option_name_fn`: A function used to derive default option names for 10 | #' newly defined options. See [set_option_name_fn()]. 11 | #' - `envvar_name_fn`: A function used to derive default environment variable 12 | #' names for newly defined options. See [set_envvar_name_fn()]. 13 | #' 14 | #' @param env An environment in which to search for an options environment 15 | #' @param inherits Whether to search upward through parent environments 16 | #' @param ... Additional arguments unused 17 | #' 18 | #' @name options_env 19 | #' @rdname options_env 20 | #' @family options_env 21 | #' 22 | #' @keywords internal 23 | NULL 24 | 25 | #' Retrieve options environment (experimental) 26 | #' 27 | #' The options environment stores metadata regarding the various options 28 | #' defined in the local scope - often the top environment of a package 29 | #' namespace. 30 | #' 31 | #' @note This function's public interface is still under consideration. It is 32 | #' surfaced to provide access to option names, though the exact mechanism 33 | #' of retrieving these names should be considered experimental. 34 | #' 35 | #' @inheritParams options_env 36 | #' @return An environment containing option specifications and default values, 37 | #' or `ifnotfound` if no environment is found. 38 | #' 39 | #' @export 40 | get_options_env <- function(env, ...) { 41 | UseMethod("get_options_env") 42 | } 43 | 44 | #' @name get_options_env 45 | #' @export 46 | get_options_env.options_env <- function(env, ...) { 47 | env 48 | } 49 | 50 | #' @name get_options_env 51 | #' @export 52 | get_options_env.options_list <- function(env, ...) { 53 | attr(env, "environment") 54 | } 55 | 56 | #' @name get_options_env 57 | #' @param ifnotfound A result to return of no options environment is found. 58 | #' @export 59 | get_options_env.default <- function( 60 | env = parent.frame(), 61 | ..., 62 | inherits = FALSE, 63 | ifnotfound = emptyenv()) { 64 | if (!missing(env) && !options_initialized(env, inherits = inherits)) { 65 | init_options_env(env = env) 66 | } 67 | 68 | opt <- get0(CONST_OPTIONS_ENV_NAME, envir = env, inherits = inherits) 69 | if (!inherits(opt, "options_env")) { 70 | if (missing(env)) { 71 | return(ifnotfound) 72 | } 73 | stop("options object not found in this environment.") 74 | } 75 | 76 | opt 77 | } 78 | 79 | #' @describeIn options_env 80 | #' Test whether options is initialized in environment 81 | options_initialized <- function(env, inherits = FALSE) { 82 | exists(CONST_OPTIONS_ENV_NAME, envir = env, inherits = inherits) 83 | } 84 | 85 | #' @describeIn options_env 86 | #' Initialize an options object 87 | init_options_env <- function(env = parent.frame()) { 88 | optenv <- structure( 89 | new.env(parent = env), 90 | spec = list(), 91 | option_name_fn = option_name_default, 92 | envvar_name_fn = envvar_name_default, 93 | class = c("options_env", "environment") 94 | ) 95 | 96 | assign(CONST_OPTIONS_ENV_NAME, optenv, envir = env) 97 | } 98 | 99 | #' @describeIn options_env 100 | #' Convert into an options list 101 | as_options_list <- function(x, ...) { 102 | UseMethod("as_options_list") 103 | } 104 | 105 | #' @name options_env 106 | as_options_list.list <- function(x, ...) { 107 | structure(x, class = c("options_list", "list")) 108 | } 109 | 110 | #' @name options_env 111 | as_options_list.options_env <- function(x, ...) { 112 | res <- structure(as.list(x), class = c("options_list", "list")) 113 | 114 | for (attr_name in names(attributes(x))) { 115 | if (attr_name %in% names(attributes(res))) next 116 | attr(res, attr_name) <- attr(x, attr_name) 117 | } 118 | 119 | attr(res, "environment") <- x 120 | res 121 | } 122 | 123 | #' @describeIn options_env 124 | #' Get the option's default value 125 | get_option_default_value <- function(x, env = parent.frame()) { 126 | optenv <- get_options_env(env) 127 | 128 | # initialize value by evaluating expression at time of first access 129 | if (!exists(x, envir = optenv, inherits = FALSE)) { 130 | spec <- get_option_spec(x, optenv) 131 | optenv[[x]] <- eval(spec$expr, envir = spec$envir) 132 | } 133 | 134 | optenv[[x]] 135 | } 136 | 137 | #' @describeIn options_env 138 | #' Get all options specifications as named list 139 | get_options_spec <- function(env = parent.frame()) { 140 | optenv <- get_options_env(env) 141 | attr(optenv, "spec") 142 | } 143 | 144 | #' @describeIn options_env 145 | #' Get single option specification 146 | get_option_spec <- function( 147 | name, 148 | env = parent.frame(), 149 | inherits = FALSE, 150 | on_missing = warning) { 151 | optenv <- get_options_env(env, inherits = inherits) 152 | spec <- attr(optenv, "spec") 153 | 154 | if (!is.null(name) && name %in% names(spec)) { 155 | return(spec[[name]]) 156 | } else if (!is.null(on_missing)) { 157 | raise( 158 | on_missing, 159 | msg = paste0("option '", name, "' is not defined in environment") 160 | ) 161 | } 162 | 163 | NULL 164 | } 165 | 166 | #' @describeIn options_env 167 | #' Set single option specification 168 | set_option_spec <- function(name, details, env = parent.frame()) { 169 | optenv <- get_options_env(env) 170 | attr(optenv, "spec")[[name]] <- details 171 | } 172 | 173 | 174 | 175 | #' Format an options environment 176 | #' 177 | #' @param x An option environment ("option_env") class object 178 | #' @param ... Additional arguments unused 179 | #' @param fmt A list of formats to use for formatting individual text elements 180 | #' 181 | #' @return A formatted character value 182 | #' 183 | #' @keywords internal 184 | #' @exportS3Method format options_env 185 | format.options_env <- function(x, ..., fmt = options_fmts()) { 186 | spec <- get_options_spec(x) 187 | values <- as.list(x) 188 | 189 | formatted_spec <- character(length(spec)) 190 | for (i in seq_along(spec)) { 191 | n <- names(spec)[[i]] 192 | formatted_spec[[i]] <- format(spec[[n]], values[[n]], fmt = fmt) 193 | } 194 | 195 | paste0(formatted_spec, collapse = "\n\n") 196 | } 197 | 198 | #' Format an options list 199 | #' 200 | #' @param x An option list ("option_list") class object 201 | #' @inheritParams format.options_env 202 | #' 203 | #' @return A formatted character value 204 | #' 205 | #' @keywords internal 206 | #' @exportS3Method format options_env 207 | format.options_list <- format.options_env 208 | 209 | #' @exportS3Method print options_env 210 | print.options_env <- function(x, ...) { 211 | cat("\n", format(x, ...), "\n\n", sep = "") 212 | } 213 | 214 | #' @exportS3Method print options_list 215 | print.options_list <- print.options_env 216 | 217 | #' @exportS3Method as.list options_env 218 | as.list.options_env <- function(x, ...) { 219 | values <- list() 220 | for (n in names(x)) { 221 | values[[n]] <- if (do.call(missing, list(n), envir = x)) { 222 | bquote() 223 | } else { 224 | x[[n]] 225 | } 226 | } 227 | values 228 | } 229 | -------------------------------------------------------------------------------- /R/options_get.R: -------------------------------------------------------------------------------- 1 | #' Inspecting Option Values 2 | #' 3 | #' @param x,xs An option name, vector of option names, or a named list of new 4 | #' option values 5 | #' @param value A new value for the associated global option 6 | #' @param default A default value if the option is not set 7 | #' @param env An environment, namespace or package name to pull options from 8 | #' @param ... See specific functions to see behavior. 9 | #' @param opts A `list` of values, for use in functions that accept `...` 10 | #' arguments. In rare cases where your argument names conflict with other 11 | #' named arguments to these functions, you can specify them directly using 12 | #' this parameter. 13 | #' @param check_names (experimental) A behavior used when checking option 14 | #' names against specified options. Expects one of `"asis"`, `"warn"` or 15 | #' `"stop"`. 16 | #' 17 | #' @param add,after,scope Passed to [on.exit], with alternative defaults. 18 | #' `scope` is passed to the [on.exit] `envir` parameter to disambiguate it 19 | #' from `env`. 20 | #' 21 | #' @name opt 22 | NULL 23 | 24 | 25 | 26 | #' @describeIn opt 27 | #' 28 | #' Retrieve an option. Additional `...` arguments passed to an optional 29 | #' `option_fn`. See [`option_spec()`] for details. 30 | #' 31 | #' @return For `opt()` and `opts()`; the result of the option (or a list of 32 | #' results), either the value from a global option, the result of processing 33 | #' the environment variable or the default value, depending on which of the 34 | #' alternative sources are defined. 35 | #' 36 | #' @examples 37 | #' define_options("Whether execution should emit console output", quiet = FALSE) 38 | #' opt("quiet") 39 | #' 40 | #' @export 41 | opt <- function(x, default, env = parent.frame(), ...) { 42 | optenv <- get_options_env(as_env(env), inherits = TRUE) 43 | spec <- get_option_spec(x, env = optenv) 44 | 45 | source <- opt_source(spec, env = optenv) 46 | value <- switch(source, 47 | "envvar" = spec$envvar_fn(Sys.getenv(spec$envvar_name), spec$envvar_name), 48 | "option" = getOption(spec$option_name), 49 | "default" = get_option_default_value(x, optenv), 50 | if (missing(default)) { 51 | stop(sprintf("option '%s' not found.", x)) 52 | } else { 53 | default 54 | } 55 | ) 56 | 57 | spec$option_fn( 58 | value, 59 | x = x, 60 | default = default, 61 | env = env, 62 | ..., 63 | source = source 64 | ) 65 | } 66 | 67 | 68 | 69 | #' @describeIn opt 70 | #' 71 | #' Set an option's value. Additional `...` arguments passed to 72 | #' [`get_option_spec()`]. 73 | #' 74 | #' @param value A new value to update the associated global option 75 | #' 76 | #' @return For modifying functions ([opt_set] and [opt<-]: the value of the 77 | #' option prior to modification 78 | #' 79 | #' @export 80 | opt_set <- function(x, value, env = parent.frame(), ...) { 81 | spec <- get_option_spec(x, env = as_env(env), inherits = TRUE, ...) 82 | if (is.null(spec)) { 83 | return(invisible(NULL)) 84 | } 85 | 86 | args <- list(value) 87 | names(args) <- spec$option_name 88 | invisible(do.call(options, args)[[spec$option_name]]) 89 | } 90 | 91 | 92 | 93 | #' @describeIn opt 94 | #' 95 | #' An alias for [`opt_set()`] 96 | #' 97 | #' @export 98 | `opt<-` <- function(x, ..., value) { 99 | opt_set(x = x, value = value, ...) 100 | } 101 | 102 | 103 | 104 | #' @describeIn opt 105 | #' 106 | #' Determine source of option value. Primarily used for diagnosing options 107 | #' behaviors. 108 | #' 109 | #' @return For [opt_source]; the source that is used for a specific option, 110 | #' one of `"option"`, `"envvar"` or `"default"`. 111 | #' 112 | #' @examples 113 | #' define_options("Whether execution should emit console output", quiet = FALSE) 114 | #' opt_source("quiet") 115 | #' 116 | #' Sys.setenv(R_GLOBALENV_QUIET = TRUE) 117 | #' opt_source("quiet") 118 | #' 119 | #' options(globalenv.quiet = FALSE) 120 | #' opt_source("quiet") 121 | #' 122 | #' @export 123 | opt_source <- function(x, env = parent.frame()) { 124 | if (!is_option_spec(x)) { 125 | x <- get_option_spec(x, env = env) 126 | } 127 | 128 | if (is.null(x)) { 129 | return(NA_character_) 130 | } 131 | 132 | # determine whether option is set in various places 133 | opt_sources <- list( 134 | option = function(x) x$option_name %in% names(.Options), 135 | envvar = function(x) !is.na(Sys.getenv(x$envvar_name, unset = NA)), 136 | default = function(x) !(is.name(x$expr) && nchar(x$expr) == 0) 137 | ) 138 | 139 | # TODO: priority possibly configurable per-option in the future 140 | sources <- c("option", "envvar", "default") 141 | 142 | for (origin in sources) { 143 | if (opt_sources[[origin]](x)) { 144 | return(origin) 145 | } 146 | } 147 | 148 | NA_character_ 149 | } 150 | 151 | 152 | 153 | #' @describeIn opt 154 | #' 155 | #' Retrieve multiple options. When no names are provided, return a list 156 | #' containing all options from a given environment. Accepts a character 157 | #' vector of option names or a named list of new values to modify global 158 | #' option values. 159 | #' 160 | #' @examples 161 | #' define_options("Quietly", quiet = TRUE, "Verbosity", verbose = FALSE) 162 | #' 163 | #' # retrieve multiple options 164 | #' opts(c("quiet", "verbose")) 165 | #' 166 | #' # update multiple options, returns unmodified values 167 | #' opts(list(quiet = 42, verbose = TRUE)) 168 | #' 169 | #' # next time we check their values we'll see the modified values 170 | #' opts(c("quiet", "verbose")) 171 | #' 172 | #' @export 173 | opts <- function(xs = NULL, env = parent.frame()) { 174 | UseMethod("opts", xs) 175 | } 176 | 177 | #' @export 178 | opts.NULL <- function(xs, env = parent.frame()) { 179 | env <- get_options_env(as_env(env), inherits = TRUE) 180 | res <- as_options_list(list()) 181 | for (n in names(env)) { 182 | res[[n]] <- opt(n, env = env) 183 | } 184 | res 185 | } 186 | 187 | #' @export 188 | opts.list <- function(xs, env = parent.frame()) { 189 | env <- get_options_env(as_env(env), inherits = TRUE) 190 | 191 | if (list_is_all_named(xs)) { 192 | old <- as_options_list(env)[names(xs)] 193 | 194 | for (i in seq_along(xs)) { 195 | opt_set(names(xs)[[i]], xs[[i]], env) 196 | } 197 | 198 | old 199 | } else if (list_is_all_unnamed(xs)) { 200 | as_options_list(env)[as.character(xs)] 201 | } else { 202 | stop(paste0( 203 | "lists provided to `opts()` must either have no names, or names for ", 204 | "every value." 205 | )) 206 | } 207 | } 208 | 209 | #' @export 210 | opts.character <- function(xs, env = parent.frame()) { 211 | names(xs) <- xs 212 | lapply(xs, opt, env = env) 213 | } 214 | 215 | 216 | 217 | #' @describeIn opt 218 | #' 219 | #' Set an option only in the local frame. Additional `...` arguments passed to 220 | #' [`on.exit()`]. 221 | #' 222 | #' @note 223 | #' Local options are set with [on.exit], which can be prone to error if 224 | #' subsequent calls are not called with `add = TRUE` (masking existing 225 | #' [on.exit] callbacks). A more rigorous alternative might make use of 226 | #' [`withr::defer`]. 227 | #' 228 | #' old <- opt_set("option", value) 229 | #' withr::defer(opt_set("option", old)) 230 | #' 231 | #' If you'd prefer to use this style, see [`opts_list()`], which is designed 232 | #' to work nicely with \code{\link[withr]{withr}}. 233 | #' 234 | opt_set_local <- function( 235 | x, 236 | value, 237 | env = parent.frame(), 238 | ..., 239 | add = TRUE, 240 | after = FALSE, 241 | scope = parent.frame()) { 242 | old <- opt_set(x, value, env = env) 243 | opt_set_call <- as.call(list(quote(opt_set), x, value = old, env = env)) 244 | on_exit_args <- list(opt_set_call, ..., add = add, after = after) 245 | do.call(base::on.exit, on_exit_args, envir = scope) 246 | invisible(old) 247 | } 248 | 249 | 250 | #' @describeIn opt 251 | #' 252 | #' Produce a named list of namespaced option values, for use with [`options()`] 253 | #' and \code{\link[withr]{withr}}. Additional `...` arguments used to provide 254 | #' named option values. 255 | #' 256 | #' @examples 257 | #' define_options("print quietly", quiet = TRUE) 258 | #' 259 | #' print.example <- function(x, ...) if (!opt("quiet")) NextMethod() 260 | #' example <- structure("Hello, World!", class = "example") 261 | #' print(example) 262 | #' 263 | #' # using base R options to manage temporary options 264 | #' orig_opts <- options(opts_list(quiet = FALSE)) 265 | #' print(example) 266 | #' options(orig_opts) 267 | #' 268 | #' @examplesIf length(find.package("withr")) > 0L 269 | #' # using `withr` to manage temporary options 270 | #' withr::with_options(opts_list(quiet = FALSE), print(example)) 271 | #' 272 | #' @export 273 | opts_list <- function( 274 | ..., 275 | env = parent.frame(), 276 | check_names = c("asis", "warn", "error"), 277 | opts = list(...) 278 | ) { 279 | env <- get_options_env(as_env(env), inherits = TRUE) 280 | spec <- get_options_spec(env) 281 | 282 | as_check_names_fn(check_names)(names(opts)) 283 | names(opts) <- vcapply(names(opts), function(name) { 284 | if (name %in% names(spec)) { 285 | spec[[name]]$option_name 286 | } else { 287 | name 288 | } 289 | }) 290 | 291 | opts 292 | } 293 | -------------------------------------------------------------------------------- /R/options_roxygen.R: -------------------------------------------------------------------------------- 1 | #' Generate Standalone `?options` Documentation 2 | #' 3 | #' Produce a comprehensive documentation page outlining all your defined 4 | #' options' behaviors. 5 | #' 6 | #' @param title An optional, customized title (defaults to "Options") 7 | #' @param desc An optional, customized description of behaviors 8 | #' @param env An environemnt in which to find the associated options object 9 | #' @return A character vector of `roxygen2` tag segments 10 | #' 11 | #' @examples 12 | #' #' @eval options::as_roxygen_docs() 13 | #' NULL 14 | #' 15 | #' @family options_roxygen2 16 | #' @keywords roxygen2 17 | #' @importFrom utils packageName 18 | #' @export 19 | as_roxygen_docs <- function( 20 | title = paste(pkgname(env), "Options"), 21 | desc = default_options_rd_desc(), 22 | env = parent.frame()) { 23 | 24 | pkg <- pkgname(env) 25 | optenv <- get_options_env(env, inherits = TRUE) 26 | details <- get_options_spec(optenv) 27 | 28 | c( 29 | sprintf("@title %s", title), 30 | sprintf("@description %s", desc), 31 | "@rdname options", 32 | "@name options", 33 | "@section Checking Option Values:", 34 | "Option values specific to `", pkg, "` can be ", 35 | "accessed by passing the package name to `env`.", 36 | "", 37 | sprintf(" options::opts(env = \"%s\")", pkg), 38 | "", 39 | sprintf(" options::opt(x, default, env = \"%s\")", pkg), 40 | "", 41 | 42 | "@seealso options getOption Sys.setenv Sys.getenv", 43 | "@section Options:", 44 | "\\describe{", 45 | vapply(setdiff(names(details), CONST_OPTIONS_META), function(n) { 46 | sprintf( 47 | "\\item{%s}{\\describe{%s}}\n", n, 48 | paste0( 49 | sep = "\n", 50 | details[[n]]$desc, 51 | sprintf( 52 | "\\item{default: }{\\preformatted{%s}}\n", 53 | paste0( 54 | collapse = "\n", 55 | deparse(eval(bquote(substitute(.(as.symbol(n)), optenv)))) 56 | ) 57 | ), 58 | sprintf("\\item{option: }{%s}\n", details[[n]]$option_name), 59 | sprintf( 60 | "\\item{envvar: }{%s (%s)}\n", 61 | details[[n]]$envvar_name, 62 | attr(details[[n]]$envvar_fn, "desc") %||% "preprocessed" 63 | ) 64 | ) 65 | ) 66 | }, character(1L)), 67 | "}" 68 | ) 69 | } 70 | 71 | 72 | 73 | #' Produce `@param` roxygen sections for options 74 | #' 75 | #' Generate parameter documentation based on option behaviors. Especially useful 76 | #' for ubiquitous function parameters that default to option values. 77 | #' 78 | #' @param ... Character values of options to use. If named arguments are 79 | #' provided, the option description provided as the value is mapped to a 80 | #' parameter of the argument's name. 81 | #' @return A character vector of `roxygen2` `@param` tags 82 | #' 83 | #' @examples 84 | #' options::define_options( 85 | #' "whether messages should be written softly, or in all-caps", 86 | #' quiet = TRUE 87 | #' ) 88 | #' 89 | #' #' Hello, World 90 | #' #' 91 | #' #' @eval options::as_params("softly" = "quiet") 92 | #' #' 93 | #' hello <- function(who, softly = opt("quiet")) { 94 | #' say_what <- paste0("Hello, ", who, "!") 95 | #' if (quiet) say_what else toupper(say_what) 96 | #' } 97 | #' 98 | #' @family options_roxygen2 99 | #' @keywords roxygen2 100 | #' @export 101 | as_params <- function(...) { 102 | env <- parent.frame() 103 | opts <- list(...) 104 | optenv <- get_options_env(env, inherits = TRUE) 105 | details <- get_options_spec(optenv) 106 | 107 | missing_opt_names <- setdiff(opts, names(optenv)) 108 | if (length(missing_opt_names) > 0) { 109 | stop(sprintf( 110 | "options %s not found.", 111 | paste0("'", missing_opt_names, "'", collapse = ", ") 112 | )) 113 | } 114 | 115 | if (length(opts) == 0) { 116 | opts <- setdiff(names(optenv), CONST_OPTIONS_META) 117 | } 118 | 119 | if (is.null(names(opts))) { 120 | names(opts) <- opts 121 | } 122 | 123 | unnamed <- names(opts) == "" 124 | names(opts[unnamed]) <- opts[unnamed] 125 | 126 | format_param <- function(n) { 127 | optname <- opts[[n]] 128 | optdetails <- details[[optname]] 129 | 130 | default <- paste0(deparse(optdetails$expr), collapse = "; ") 131 | 132 | sprintf( 133 | paste0( 134 | "@param %s %s (Defaults to `%s`, overwritable using option '%s' or ", 135 | "environment variable '%s')" 136 | ), 137 | n, 138 | optdetails$desc %||% "From package option", 139 | default, 140 | optdetails$option_name, 141 | optdetails$envvar_name 142 | ) 143 | } 144 | 145 | vapply(names(opts), format_param, character(1L)) 146 | } 147 | 148 | 149 | 150 | default_options_rd_desc <- function() { 151 | paste0( 152 | "Internally used, package-specific options. All options will prioritize ", 153 | "R options() values, and fall back to environment variables if ", 154 | "undefined. If neither the option nor the environment variable is set, ", 155 | "a default value is used." 156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /R/options_spec.R: -------------------------------------------------------------------------------- 1 | #' Specify Option 2 | #' 3 | #' An option specification outlines the various behaviors of an option. It's 4 | #' default value, related global R option, and related environment variable 5 | #' name, as well as a description. This information defines the operating 6 | #' behavior of the option. 7 | #' 8 | #' @details 9 | #' 10 | #' # Processing Functions 11 | #' 12 | #' Parameters `option_fn` and `envvar_fn` allow for customizing the way values 13 | #' are interpreted and processed before being returned by [`opt`] functions. 14 | #' 15 | #' ## `envvar_fn` 16 | #' 17 | #' When a value is retrieved from an environment variable, the string value 18 | #' contained in the environment variable is first processed by `envvar_fn`. 19 | #' 20 | #' An `envvar_fn` accepts only a single positional argument, and should have a 21 | #' signature such as: 22 | #' 23 | #' ```r 24 | #' function(value) 25 | #' ``` 26 | #' 27 | #' ## `option_fn` 28 | #' 29 | #' Regardless of how a value is produced - either retrieved from an environment 30 | #' variable, option, a stored default value or from a default provided to an 31 | #' [`opt`] accessor function - it is then further processed by `option_fn`. 32 | #' 33 | #' The first argument provided to `option_fn` will always be the retrieved 34 | #' value. The remaining parameters in the signature should be considered 35 | #' experimental. In addition to the value, the arguments provided to [`opt()`], 36 | #' as well as an additional `source` parameter from [`opt_source()`] may be 37 | #' used. 38 | #' 39 | #' **Stable** 40 | #' 41 | #' ``` 42 | #' function(value, ...) 43 | #' ``` 44 | #' 45 | #' **Experimental** 46 | #' 47 | #' ``` 48 | #' function(value, x, default, env, ..., source) 49 | #' ``` 50 | #' 51 | #' @param name A string representing the internal name for the option. This is 52 | #' the short form `