24

Is there a way to source() a script in R such that it is attached as a parent to the global environment (.GlobalEnv)?

Currently, when I source a script, all variables and functions of that script appear in my global (interactive) environment. I'd like to include these variables and functions in the search path, but not in .GlobalEnv. That is, I'd like the sourced script to behave like an attached package, which gets attached between the global and base environments (see figure from Advanced R Environments)

enter image description here

Megatron
  • 15,909
  • 12
  • 89
  • 97

5 Answers5

16

The following environment insertion appears to achieve the desired functionality:

Check the current search path:

search()
# [1] ".GlobalEnv"        "package:stats"     "package:graphics"
# [4] "package:grDevices" "package:utils"     "package:datasets"
# [7] "package:methods"   "Autoloads"         "package:base"

Add new environment for sourced packages and use local parameter when source()ing:

myEnv <- new.env()    
source("some_other_script.R", local=myEnv)
attach(myEnv, name="sourced_scripts")

Check the search path:

search()
#  [1] ".GlobalEnv"        "sourced_scripts"   "package:dplyr"
#  [4] "package:stats"     "package:graphics"  "package:grDevices"
#  [7] "package:utils"     "package:datasets"  "package:methods"
# [10] "Autoloads"         "package:base"

Note that we attach() the new environment after sourcing, so that dplyr is attached after our script environment in the search path.

Megatron
  • 15,909
  • 12
  • 89
  • 97
10

I would also like to a solution using sys.source function. Using envir and toplevel.env arguments allows for convenient (IMHO) bypassing of the global environment. As per the linked documentation:

sys.source [p]arses expressions in the given file, and then successively evaluates them in the specified environment.

tstEnv <- new.env()
sys.source(file = "tst.R", envir = tstEnv, toplevel.env = tstEnv)

where tst.R contains:

a <- 1
b <- 1

Results:

ls(envir = .GlobalEnv)
# [1] "tstEnv"
ls(envir = tstEnv)
# [1] "a" "b"
tstEnv$a
# [1] 1
Konrad
  • 17,740
  • 16
  • 106
  • 167
8

The simplest way to source a script as if it was a package (i.e. such that lexical scoping won't result in the use of variables defined in the global environment when calling functions defined in your R script) is to create an environment that that is whose parent is the .BaseNamespaceEnv, and then call source() using that environment.

For example if you have a script like this:

# << my-script.R >>
my_fun <- function(x){x + y}

Then evaluating the following at the console, won't generate an error, as it would if my_fun were defined within it's own package:

source("my-script.R")
y = 2
my_fun(1)
#> 3

However, if you create an environment whose search() path does not include the Global Environment (.GlobalEnv) then you'll get a proper error when you call the function from your script:

# Create the environment:
ENV = new.env(parent = .BaseNamespaceEnv)
# Attache it to the search path so that objects in your environment can be
# found from the global environment (i.e. from the console):
attach(ENV)
# do things:
source("my-script.R",ENV)
y = 2
my_fun(1)
#> Error in .ENV$my_fun(3) : object 'y' not found
Jthorpe
  • 9,756
  • 2
  • 49
  • 64
  • Good answer (I am looking for a solution too), but functions of packages loaded in the sourced file with `library` are still not accessible because they are attached as parent of the environment `ENV` in the search path. Try this code `library(data.table); print(search()); x <- as.data.table(mtcars)` in the "my-script.R" and you will get the error "Error in `[.data.frame`(x, i, j) : object 'cyl' not found" when sourced as you described it. The reason is the "wrong" search path within the sourced file: `[1] ".GlobalEnv" "package:data.table" "ENV" ...` – R Yoda Nov 10 '17 at 23:20
  • 1
    Actually, you would never call `library()` from within an R package. You would list the packages that *your* package depends on in the `imports` section of the `DESCRIPTION` file and R would take care to load those packages before loading your package. The equivalent here would be to call `library(data.table)` and then use `ENV = new.env(parent = as.environment("package:data.table"))`. – Jthorpe Nov 11 '17 at 07:49
  • Yes your are absolutely right, no `library` within a package. The reason why I am looking for a solution is "batch job framework package" I am developing and that sources external code that does the real work and the sourced external code shall be isolated as much as possible in an environment. I only inject the "job context" data and the sourced code does not even know more than the "job context" interface from the framework. I know I could also define function interface that must be implemented but I want make it as easy as possible for the users... – R Yoda Nov 11 '17 at 08:18
  • 2
    It feels like you're reinventing the wheel as there is already a mechanism for running batches of R scripts (i.e. `help("Rscript")`), and IMHO, R is a terrible language for running code in isolation. I'd personally go for scripting R sessions from another language Python, NodeJS, Powershell, or Bash, or whatever language you like that is good at managing sub-processes. – Jthorpe Nov 12 '17 at 04:00
  • 1
    In case it's helpful, [I used this code](https://github.com/jdthorpe/Rmisc/blob/master/R/sandbox.R) for running my own code in isolation a while back, where `sandbox()` is just a weak wrapper around `source()` – Jthorpe Nov 12 '17 at 04:05
  • 1
    Then again, maybe you're looking for something as simple as using `ENV = new.env(parent = .GlobalEnv)` – Jthorpe Nov 12 '17 at 04:19
5

From the source documentation, the local argument can be an environment which determines where the sourced expressions are evaluated.

This suggests that you could create a new environment, run source passing this environment to local, then attach the environment to the search path.

Or you can use attach with what=NULL to create an empty environment, save the return value, and pass that to local in source:

tmp <- attach(what=NULL)
source('test.R', local=tmp)

or as a single line:

source('test.R', local=attach(NULL))
Greg Snow
  • 48,497
  • 6
  • 83
  • 110
  • 3
    An issue with this method is that the sourced script environment is inserted and any libraries attached within the scripts are children - so it can't find the libraries – Megatron Sep 21 '16 at 15:56
0

I'm not sure if my answer is any different from the answers given above, but I use the following code:

if (!exists('.env')) .env <- new.env() # creates an environment in which to store functions
if ('.env' %in% search()) detach(.env) # detaches .env if it already exists; does not "erase" functions previously stored in .env
func <- "filenameWhereSourceCodeIsStored"
source(paste0("C:/Users/JT/R/Functions/", func, ".R"), .env)
attach(.env)
Josh
  • 1,210
  • 12
  • 30