r-lib / cli

Tools for making beautiful & useful command line interfaces
https://cli.r-lib.org/
Other
625 stars 66 forks source link

Suggestion: `cli_abort_if_not()` #672

Open wurli opened 4 months ago

wurli commented 4 months ago

If I'm using cli in a package, often I avoid stopifnot() because the unformatted error messages feel jarring compared with those produced by cli_abort() and friends. I'm guessing cli has no stopifnot() equivalent because {rlang} doesn't have one either, but I would really like to see something like this added as I often just end up implementing it myself:

cli_abort_if_not <- function(..., .call = .envir, .envir = parent.frame(), .frame = .envir) {
  for (i in seq_len(...length())) {
    if (!all(...elt(i))) {
      cli::cli_abort(
        ...names()[i], 
        .call = .call, 
        .envir = .envir, 
        .frame = .frame
      )
    }
  }
  invisible(NULL)
}

cli_abort_if_not(
  "No problem" = TRUE,
  "Some {.emph important} issue" = FALSE,
  "Another issue" = FALSE
)
#> Error:
#> ! Some important issue
#> Backtrace:
#>     ▆
#>  1. └─global cli_abort_if_not(...)
#>  2.   └─cli::cli_abort(...)
#>  3.     └─rlang::abort(...)

Created on 2024-02-21 with reprex v2.0.2

Many thanks for all the work on this amazing package!

JosiahParry commented 4 months ago

I agree. I would love to see something like this made available in cli. stopifnot() simplifies the process of doing multiple validations for function arguments. The current way to achieve the same functionality with cli is to create multiple if statements which can become quite unruly.

Here for example is some cli I wrote today that migrates off of stopifnot()

if (!rlang::inherits_any(x, "data.frame")) {
  cli::cli_abort(
    "Expected {.cls data.frame} found {.obj_type_friendly {x}}",
    call = call
  )
} else if (!rlang::is_scalar_character(name)) {
  cli::cli_abort(
    "{.arg name} must be a scalar character vector.",
    call = call
  )
} else if (!rlang::is_scalar_character(title)) {
  cli::cli_abort(
    "{.arg title} must be a scalar character vector.",
    call = call
  )
} else if (!nzchar(name)) {
  cli::cli_abort(
    "{.arg name} must not be empty.",
    call = call
  )
} else if (!nzchar(title)) {
  cli::cli_abort(
    "{.arg name} must not be empty.",
    call = call
  )
}

If a cli_stopifnot() were made available it could look something like this instead which I think would be very nice!

cli_stopifnot(
  "Expected {.cls data.frame} found {class(x)}" = rlang::inherits_any(x, "data.frame"), 
  "{.arg name} must be a scalar character vector." = rlang::is_scalar_character(name),
  "{.arg title} must be a scalar character vector." = rlang::is_scalar_character(title),
  "{.arg name} must not be empty." = nzchar(name),
  "{.arg name} must not be empty." = nzchar(title),
  call = call
)
wurli commented 4 months ago

Worth noting that rlang has an open (albeit fairly old) issue about whether to implement something like this.

gaborcsardi commented 4 months ago

This is a good idea, and we might have something like this at some point, but not in the short term, unfortunately.