rcommand-line

withr: suppress message from `deferred_run` when called via `RScript`


Let's assume I have the following script:

msg.R:

library(withr)
local_options(list(whatever = 1L))

suppressMessages(deferred_run())

I invoke it from the command line via

Rscript msg.R

I would have assumed to see no output (thanks to suppressMessage), but in fact I see the message from deferred_run:

No deferred expressions to run

I have 3 questions:

  1. Why do I see the output in the first place? I thought suppressMessages would, well, suppress the message? If I run the code interactively the message is rightfully suppressed.
  2. Why do I see No deferred expressions to run I would have expected to see Ran 1/1 deferred expressions as I am in fact changing the global state (thanks to local_options) and I would have expected that this is reset? (Again when running this code interactively, I would see the run message [after disabling the suppression of course]).
  3. While writing this post, I was wondering whether I should use withr for a script meant to be run from the command line at all? The script is anyways run in its own session, so changing the options should not have a bad effect anyways. So am I using an anti-pattern here?

Solution

  • The deferred events are being triggered twice

    The message is initially perplexing but all is working as intended. The purpose of withr::local_options() is to set options within some scope. When you run it in the global environment, this means you are setting the options for the entire R session. It prints:

    Setting global deferred event(s).
    i These will be run:
      * Automatically, when the R session ends.
      * On demand, if you call `withr::deferred_run()`.
    

    The difference between your interactive example and the Rscript version is that in the latter, your R session ends. Because you've called local_options() at global scope, it checks whether there is anything left to run. This would also occur in interactive mode if you manually quit().

    One way to demonstrate this is happening is with a script like this, where you don't suppress messages:

    withr::local_options(list(whatever = 1L))
    withr::deferred_run()
    cat("***user code ended - script exiting***\n")
    

    If you Rscript this, you'll see the output:

    Ran 1/1 deferred expressions
    ***user code ended - script exiting***
    No deferred expressions to run
    

    The message Ran 1/1 deferred expressions is triggered by withr::deferred_run(). The No deferred expressions to run is triggered by exiting the R session. If you replace the second line with suppressMessages(withr::deferred_run()), you'll get:

    ***user code ended - script exiting***
    No deferred expressions to run
    

    It looks like the suppressMessages() is failing and it's not finding the deferred expressions. But in fact you have manually triggered the deferred expressions, so there are none left to find, and this is the cleanup call which runs when the R session ends, rather than the one that was wrapped in suppressMessages().

    How to fix this

    If you want to avoid triggering a cleanup when the R session ends, rather than setting options in the global environment, you can attach the handlers to a different scope. For example:

    local({
        env <- environment()
        withr::local_options(list(whatever = 1L), .local_envir = env)
        suppressMessages(withr::deferred_run(env))
        # ^^ (Without suppression: Ran 1/1 deferred expressions)
    })
    

    This will run silently.

    Is this an anti-pattern?

    I think so - or at least it's probably not required. When you run withr::local_options(list(whatever = 1L)) at the top level, this means: set options(whatever=1L) and revert the options to their previous state on exit. However, when you run Rscript the options will revert anyway when the session ends, so if you're doing this at the global level, I don't see why it's necessary.