Guilherme Salome
Guilherme Salome

Reputation: 2039

Separate scripts from .GlobalEnv: Source script that source scripts

This question is similar to Source script to separate environment in R, not the global environment, but with a key twist.

Consider a script that sources another script:

# main.R
source("funs.R")
x <- 1
# funs.R
hello <- function() {message("Hi")}

I want to source the script main.R and keep everything in a "local" environment, say env <- new.env(). Normally, one could call source("main.R", local = env) and expect everything to be in the env environment. However, that's not the case here: x is part of env, but the function hello is not! It is in .GlobalEnv.

Question: How can I source a script to a separate environment in R, even if that script itself sources other scripts, and without modifying the other scripts being sourced?

Thanks for helping, and let me know if I can clarify anything.

EDIT 1: Updated question to be explicit that scripts being source cannot be modified (assume they are not under your control).

Upvotes: 3

Views: 326

Answers (3)

Alexis
Alexis

Reputation: 5059

You can use trace to inject code in functions, so you could force all source calls to set local = TRUE. Here I just override it if local is FALSE in case any nested calls to source actually set it to other environments due to special logic of their own.

env <- new.env()

# use !isTRUE if you want to support older R versions (<3.5.0)
tracer <- quote(
  if (isFALSE(local)) {
    local <- TRUE
  }
)

trace(source, tracer, print = FALSE, where = .GlobalEnv)

# if you're doing this inside a function, uncomment the next line
#on.exit(untrace(source, where = .GlobalEnv))

source("main.R", local = env)

As mentioned in the code, if you wrap this logic in a function, consider using on.exit to make sure you untrace even if there are errors.

EDIT: as mentioned in the comments, this could have issues if some of the scripts you will be loading assume there is 1 (global) environment where everything ends. I suppose you could change the tracer to something like

tracer <- quote(
  if (missing(local)) {
    local <- TRUE
  }
)

or maybe

tracer <- quote(
  if (isFALSE(local)) {
    # fetch the specific environment you created
    local <- get("env", .GlobalEnv)
  }
)

The former assumes that if the script didn't specify local at all, it doesn't care about which environment ends up holding everything. The latter assumes that source calls that didn't specify local or set it to FALSE want everything to end up in 1 environment, and modify the logic to use your environment instead of the global one.

Upvotes: 2

jan-glx
jan-glx

Reputation: 9496

The best way to protect yourself from side effects of code you cannot control is isolation. You can use callr to easily execute the scripts isolated in a separate R session:

using environments:

env <- new.env()
env <- as.environment(callr::r(function(env) {
    list2env(env, .GlobalEnv)
    source("main.R")
    as.list(.GlobalEnv)
}, args = list(as.list(env))))
env
#> <environment: 0x0000000018124878>
env$hello()
#> Hi

simpler version sticking to lists:

params <- list()
results <- callr::r(function(params) {
    list2env(params, .GlobalEnv)
    source("main.R")
    as.list(.GlobalEnv)
}, args = list(params))
results
#> $x
#> [1] 1
#> 
#> $hello
#> function () 
#> {
#>   message("Hi")
#> }
results$hello()
#> Hi

The param part is only needed if you actually need to provide input the scripts (not used for you example). Obviously, this will not work for open connections and similar stuff. In that case, you might want to look into callr::r_session.

Upvotes: 1

nicola
nicola

Reputation: 24480

Disclaimer: Very ugly and potentially dangerous, but whatever.

Redefine source:

env<-new.env()
source<-function(...) base::source(..., local = env)
source("main.R")
#just remove your redefinition when you don't need it
rm(source)

Upvotes: 1

Related Questions