Search code examples
rerror-handlingrlang

Preserve whitespace in cli::cli_abort messages from R package cli?


I am using cli::cli_abort to handle errors.

As context for an error message, I am trying to print the contents of a matrix. I can achieve this via capture.output, though when the message is processed by cli::cli_abort the whitespace gets collapsed and you can no longer see the matrix columns easily.

The following reproducible example (hopefully!) illustrates:

do_something_with_matrix <- function(x) {

  # provide a way to format the matrix as a string vector
  format_mtx <- function(x, method = c("collapse", "preserve")) {
    method <- match.arg(method)
    mtx_lines <- if (method == "collapse") {
      c("Matrix values [Note: spaces get collapsed, hard-to-read columns]:",
        capture.output(x))
    } else {
      out_txt <- gsub(" ", "_", capture.output(x))
      c("Matrix values [Note: spaces replaced with '_' to prevent collapsing]:",
        out_txt)
    }
    names(mtx_lines) <- rep("i", length(mtx_lines))
    mtx_lines
  }

  # check the input
  if (isTRUE(any(x < 0))) {

    # display the matrix, replacing spaces with '_' to stop them collapsing
    cli::cli_inform(c(
      format_mtx(x, "preserve")
    ))

    # abort since input is invalid
    cli::cli_abort(c(
      "{.var x} must contain only positive values",
      "x" = "You've supplied a matrix with one or more negative values.",
      # display the matrix with spaces as-is (they will be collapsed)
      format_mtx(x)
    ))
  }

  # do <whatever> with x [...]
}

# --- --- ---
# Example usage

# create a matrix
mtx_data <- matrix(c( 6.62, 3.33, 1.94,
                      -4.1, 3.52,  0.5,
                      1.94,  0.5, 1.95),
                   3, 3)

do_something_with_matrix(mtx_data)
# ℹ Matrix values [Note: spaces replaced with '_' to prevent collapsing]:
# ℹ _____[,1]__[,2]_[,3]
# ℹ [1,]_6.62_-4.10_1.94
# ℹ [2,]_3.33__3.52_0.50
# ℹ [3,]_1.94__0.50_1.95
# Error in `do_something_with_matrix()`:
# ! `x` must contain only positive values
# ✖ You've supplied a matrix with one or more negative values.
# ℹ Matrix values [Note: spaces get collapsed, hard-to-read columns]:
# ℹ [,1] [,2] [,3]
# ℹ [1,] 6.62 -4.10 1.94
# ℹ [2,] 3.33 3.52 0.50
# ℹ [3,] 1.94 0.50 1.95
# Run `rlang::last_error()` to see where the error occurred.

I have searched for a way to do this but not yet managed to come up with anything. I wondered if the use_cli_format argument to rlang::abort might help, but looking at the code for cli::cli_abort shows that this arg is set to TRUE so I can't change it (anyway I'm not sure if that could help):

> cli::cli_abort
# function (message, ..., .envir = parent.frame(), call = .envir) 
# {
#   message[] <- vcapply(message, format_inline, .envir = .envir)
#   rlang::abort(message, ..., call = call, use_cli_format = TRUE)
# }
# <bytecode: 0x5562464367c8>
# <environment: namespace:cli>

Is there a way to preserve the spaces/columns in the printed matrix values? Or would there be an altogether better way to display the user-supplied matrix in the event of an error?


Solution

  • Since posting this question I realised that a way to achieve what I wanted to do would be to simply call rlang::abort directly.

    I have tried to check if there is any recommendation against doing this when using the cli package but if so I have not yet found any info to indicate it. So I am posting a possible solution to my original problem, using rlang::abort and setting use_cli_format = FALSE:

    library(rlang)
    library(cli)
    
    do_something_with_matrix <- function(x) {
      
      # Format the matrix as a named string vector where the names indicate info
      # items for the abort message
      format_mtx <- function(x) {
        mtx_lines <- capture.output(print(x))
        names(mtx_lines) <- rep("i", length(mtx_lines))
        mtx_lines
      }
      
      # Check the input
      if (isTRUE(any(x < 0))) {
        # Abort since input is invalid
        rlang::abort(c(
          cli::cli_fmt(
            cli::cli_text("Matrix {.var x} must contain only positive values")
          ),
          "x" = "You've supplied a matrix with one or more negative values.",
          "i" = "Your matrix:",
          format_mtx(x)
        ), use_cli_format = FALSE)
      }
      
      # Do <whatever> with x [...]
    }
    
    # --- --- ---
    # Example usage
    
    # Create a matrix containing an invalid value
    mtx_data <- matrix(c( 6.62, 3.33, 1.94,
                          -4.1, 3.52,  0.5,
                          1.94,  0.5, 1.95),
                       3, 3)
    
    do_something_with_matrix(mtx_data)
    # => Produces output with readable matrix columns:
    #
    # ! Matrix `x` must contain only positive values
    # ✖ You've supplied a matrix with one or more negative values.
    # ℹ Your matrix:
    # ℹ      [,1]  [,2] [,3]
    # ℹ [1,] 6.62 -4.10 1.94
    # ℹ [2,] 3.33  3.52 0.50
    # ℹ [3,] 1.94  0.50 1.95
    # Run `rlang::last_trace()` to see where the error occurred.