The rprojroot package solves a seemingly trivial but annoying problem that occurs sooner or later in any largish project: How to find files in subdirectories? Relative file paths are almost always preferable to absolute paths, but relative to what should they be? Unfortunately, we cannot always be sure about the current working directory: For instance, in RStudio it’s sometimes:

basename(getwd())
## [1] "vignettes"

If we could only get hold of our…

Project root

For an R package, the root is defined by the location of the DESCRIPTION file:

library(rprojroot)

# List all files and directories below the root
dir(find_root("DESCRIPTION"))
##  [1] "DESCRIPTION"     "inst"            "Makefile"       
##  [4] "makeR"           "man"             "NAMESPACE"      
##  [7] "NEWS.md"         "NEWS.md.tmpl"    "R"              
## [10] "readme"          "README.md"       "rprojroot.Rproj"
## [13] "tests"           "vignettes"       "wercker.yml"

The general case

We assume a self-contained project where all files and directories are located below a common root directory. Also, there should be a way to correctly identify this root directory. (Often, the root contains a regular file whose name matches a given pattern, and/or whose contents match another pattern.)

In this case, the following method reliably finds our project root:

The Git version control system (and probably many other tools) use a similar approach: A Git command can be executed from within any subdirectory of a repository.

A simple example

The find_root function is the central function of this package. It returns the path to the first directory that matches the filtering criteria, or throws an error if there is no such directory. Filtering criteria are constructed in a generic fashion using the root_criterion() function, the has_file() function constructs a criterion that checks for the presence of a file with a specific name and specific contents.

library(rprojroot)

# List all files and directories below the root
dir(find_root(has_file("DESCRIPTION")))
##  [1] "DESCRIPTION"     "inst"            "Makefile"       
##  [4] "makeR"           "man"             "NAMESPACE"      
##  [7] "NEWS.md"         "NEWS.md.tmpl"    "R"              
## [10] "readme"          "README.md"       "rprojroot.Rproj"
## [13] "tests"           "vignettes"       "wercker.yml"
# Find a file relative to the root
file.exists(find_root_file("R", "root.R", criterion = has_file("DESCRIPTION")))
## [1] TRUE

Note that the following code produces identical results when building the vignette and when sourcing the chunk in RStudio, provided that the current working directory is the project root or anywhere below.

Criteria

The has_file() function (and the more general root_criterion()) both return an S3 object of class root_criterion:

has_file("DESCRIPTION")
## Root criterion: Contains a file 'DESCRIPTION'

In addition, character values are coerced to has_file criteria by default, this coercion is applied automatically by find_root(). (This feature is used by the introductory example.)

as.root_criterion("DESCRIPTION")
## Root criterion: Contains a file 'DESCRIPTION'

The return value of these functions can be stored and reused; in fact, the package provides 2 such criteria:

criteria
## $is_rstudio_project
## Root criterion: Contains a file matching '^.*\.Rproj$' with contents matching '^Version: ' in the first 1 lines
## 
## $is_r_package
## Root criterion: Contains a file 'DESCRIPTION' with contents matching '^Package: '
## 
## attr(,"class")
## [1] "root_criteria"

Defining new criteria is easy:

has_license <- has_file("LICENSE")
has_license
## Root criterion: Contains a file 'LICENSE'
is_projecttemplate_project <- has_file("config/global.dcf", "^version: ")
is_projecttemplate_project
## Root criterion: Contains a file 'config/global.dcf' with contents matching '^version: '

Shortcuts

To avoid specifying the search criteria for the project root every time, shortcut functions can be created. The find_package_root_file is a shortcut for find_root_file(..., criterion = is_r_package):

# Print first lines of the source for this document
head(readLines(find_package_root_file("vignettes", "rprojroot.Rmd")))
## [1] "---"                                               
## [2] "title: \"Finding files in project subdirectories\""
## [3] "author: \"Kirill Müller\""                         
## [4] "date: \"`r Sys.Date()`\""                          
## [5] "output: rmarkdown::html_vignette"                  
## [6] "vignette: >"

To save typing effort, define a shorter alias:

P <- find_package_root_file

# Use a shorter alias
file.exists(P("vignettes", "rprojroot.Rmd"))
## [1] TRUE

Shortcuts for custom criteria are constructed with the make_find_root_file() function. As our project does not have a file named LICENSE, querying the root results in an error:

# Use the has_license criterion to find the root
R <- make_find_root_file(has_license)

# Our package does not have a LICENSE file, trying to find the root results in an error
R()
## Error: No root directory found. Test criterion:
## Contains a file 'LICENSE'

Fixed root

We can also create a function that computes a path relative to the root at creation time.

# Define a function that computes file paths below the current root
F <- make_fix_root_file(is_r_package)

# Show contents of the NAMESPACE file in our project
readLines(F("NAMESPACE"))
##  [1] "# Generated by roxygen2 (4.1.1): do not edit by hand"
##  [2] ""                                                    
##  [3] "S3method(as.root_criterion,character)"               
##  [4] "S3method(as.root_criterion,default)"                 
##  [5] "S3method(as.root_criterion,root_criterion)"          
##  [6] "S3method(format,root_criterion)"                     
##  [7] "S3method(print,root_criterion)"                      
##  [8] "S3method(str,root_criteria)"                         
##  [9] "export(as.root_criterion)"                           
## [10] "export(criteria)"                                    
## [11] "export(find_package_root_file)"                      
## [12] "export(find_root)"                                   
## [13] "export(find_root_file)"                              
## [14] "export(find_rstudio_root_file)"                      
## [15] "export(has_file)"                                    
## [16] "export(has_file_pattern)"                            
## [17] "export(is.root_criterion)"                           
## [18] "export(is_r_package)"                                
## [19] "export(is_rstudio_project)"                          
## [20] "export(make_find_root_file)"                         
## [21] "export(make_fix_root_file)"                          
## [22] "export(root_criterion)"

This function can be used even if we later change the working directory to somewhere outside the project:

# Print the size of the namespace file, working directory outside the project
local({
  oldwd <- setwd("../..")
  on.exit(setwd(oldwd), add = TRUE)
  file.size(F("NAMESPACE"))
})
## [1] 602

Cascading R profile

Add the following to a local .Rprofile to load the next profile further up the path if it exists.

try(source(
  rprojroot::find_root_file(
    ".Rprofile", criterion = ".Rprofile", path = ".."),
  chdir = TRUE))

Summary

The rprojroot package allows easy access to files below a project root if the project root can be identified easily, e.g. if it is the only directory in the whole hierarchy that contains a specific file. This is a robust solution for finding files in largish projects with a subdirectory hierarchy if the current working directory cannot be assumed fixed. (However, at least initially, the current working directory must be somewhere below the project root.)

Acknowledgement

This package was inspired by the gist “Stop the working directory insanity” by Jennifer Bryan, and by the way Git knows where its files are.