# Finding files in project subdirectories

#### Kirill Müller

#### 2017-01-16

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? Ideally, file paths are relative to the project root.

Unfortunately, we cannot always be sure about the current working directory: For instance, in RStudio it’s sometimes:

- the project root (when running R scripts),
- a subdirectory (when building vignettes),
- again the project root (when executing chunks of a vignette).

    basename(getwd())

    ## [1] "vignettes"

In some cases, it’s even outside the project root.

This vignette starts with a very brief summary that helps you get started, followed by a longer description of the features.

## TL;DR

What is your project: An R package?

    rprojroot::is_r_package

    ## Root criterion: contains a file `DESCRIPTION` with contents matching `^Package: `

Or an RStudio project?

    rprojroot::is_rstudio_project

    ## Root criterion: contains a file matching `[.]Rproj$` with contents matching `^Version: ` in the first line

Or something else?

    rprojroot::has_file(".git/index")

    ## Root criterion: contains a file `.git/index`

For now, we assume it’s an R package:

    root <- rprojroot::is_r_package

The `root` object contains a function that helps locating files below the root of your package, regardless of your current working directory. If you are sure that your working directory is somewhere below your project’s root, use the `root$find_file()` function:

    readLines(root$find_file("DESCRIPTION"), 3)

    ## [1] "Package: rprojroot"
    ## [2] "Title: Finding Files in Project Subdirectories"
    ## [3] "Version: 1.2"

You can also construct an accessor to your root using the `root$make_fix_file()` function:

    root_file <- root$make_fix_file()

Note that `root_file()` is a function that works just like `$find_file()` but will find the files even if the current working directory is outside your project:

    withr::with_dir(
      "../..",
      readLines(root_file("DESCRIPTION"), 3)
    )

    ## [1] "Package: rprojroot"
    ## [2] "Title: Finding Files in Project Subdirectories"
    ## [3] "Version: 1.2"

If you know the absolute path of some directory below your project, but cannot be sure of your current working directory, pass that absolute path to `root$make_fix_file()`:

    root_file <- root$make_fix_file("C:\\Users\\User Name\\...")

Get the path of standalone R scripts or vignettes using the `thisfile()` function in the `kimisc` package:

    root_file <- root$make_fix_file(dirname(kimisc::thisfile()))

The remainder of this vignette describes implementation details and advanced features.

## Project root

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 unambiguously 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:

- Start the search in any subdirectory of our project
- Proceed up the directory hierarchy until the root directory has been identified

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 implements the core functionality. 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] "API"              "DESCRIPTION"      "Makefile"
    ##  [4] "NAMESPACE"        "NEWS.md"          "R"
    ##  [7] "README.md"        "_pkgdown.yml"     "appveyor.yml"
    ## [10] "cran-comments.md" "docs"             "inst"
    ## [13] "man"              "readme"           "revdep"
    ## [16] "rprojroot.Rproj"  "tests"            "tic.R"
    ## [19] "vignettes"

    # 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 9 such criteria:

    criteria

    ## $is_rstudio_project
    ## Root criterion: contains a file matching `[.]Rproj$` with contents matching `^Version: ` in the first line
    ##
    ## $is_r_package
    ## Root criterion: contains a file `DESCRIPTION` with contents matching `^Package: `
    ##
    ## $is_remake_project
    ## Root criterion: contains a file `remake.yml`
    ##
    ## $is_projectile_project
    ## Root criterion: contains a file `.projectile`
    ##
    ## $is_git_root
    ## Root criterion: contains a directory `.git`
    ##
    ## $is_svn_root
    ## Root criterion: contains a directory `.svn`
    ##
    ## $is_vcs_root
    ## Root criterion: one of
    ## - contains a directory `.git`
    ## - contains a directory `.svn`
    ##
    ## $is_testthat
    ## Root criterion: directory name is `testthat` (also look in subdirectories: `tests/testthat`, `testthat`)
    ##
    ## $from_wd
    ## Root criterion: from current working directory
    ##
    ## 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: `

You can also combine criteria via the `|` operator:

    is_r_package | is_rstudio_project

    ## Root criterion: one of
    ## - contains a file `DESCRIPTION` with contents matching `^Package: `
    ## - contains a file matching `[.]Rproj$` with contents matching `^Version: ` in the first line

### 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

Each criterion actually contains a function that allows finding a file below the root specified by this criterion. 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 <- has_license$find_file
    R

    ## function (..., path = ".")
    ## {
    ##     find_root_file(..., criterion = criterion, path = path)
    ## }
    ## <environment: 0x55ca0ef0cf10>

    # Our package does not have a LICENSE file, trying to find the root results in an error
    R()

    ## Error: No root directory found in /tmp/Rtmp76Lko7/Rbuild36db711fd839/rprojroot/vignettes or its parent directories. Root 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 <- is_r_package$make_fix_file()
    F

    ## function (...)
    ## {
    ##     file.path("/tmp/Rtmp76Lko7/Rbuild36db711fd839/rprojroot",
    ##         ...)
    ## }
    ## <environment: 0x55ca0cd18718>

    # Show contents of the NAMESPACE file in our project
    readLines(F("NAMESPACE"))

    ##  [1] "# Generated by roxygen2: do not edit by hand"
    ##  [2] ""
    ##  [3] "S3method(\"|\",root_criterion)"
    ##  [4] "S3method(as.root_criterion,character)"
    ##  [5] "S3method(as.root_criterion,default)"
    ##  [6] "S3method(as.root_criterion,root_criterion)"
    ##  [7] "S3method(format,root_criterion)"
    ##  [8] "S3method(print,root_criterion)"
    ##  [9] "S3method(str,root_criteria)"
    ## [10] "export(as.root_criterion)"
    ## [11] "export(criteria)"
    ## [12] "export(find_package_root_file)"
    ## [13] "export(find_remake_root_file)"
    ## [14] "export(find_root)"
    ## [15] "export(find_root_file)"
    ## [16] "export(find_rstudio_root_file)"
    ## [17] "export(find_testthat_root_file)"
    ## [18] "export(from_wd)"
    ## [19] "export(get_root_desc)"
    ## [20] "export(has_dir)"
    ## [21] "export(has_dirname)"
    ## [22] "export(has_file)"
    ## [23] "export(has_file_pattern)"
    ## [24] "export(is.root_criterion)"
    ## [25] "export(is_git_root)"
    ## [26] "export(is_projectile_project)"
    ## [27] "export(is_r_package)"
    ## [28] "export(is_remake_project)"
    ## [29] "export(is_rstudio_project)"
    ## [30] "export(is_svn_root)"
    ## [31] "export(is_testthat)"
    ## [32] "export(is_vcs_root)"
    ## [33] "export(root_criterion)"
    ## [34] "import(backports)"
    ## [35] "importFrom(utils,str)"

This is a more robust alternative to `$find_file()`, because it fixes the project directory when `$make_fix_file()` is called, instead of searching for it every time. (For that reason it is also slightly faster, but I doubt this matters in practice.)

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
    withr::with_dir(
      "../..",
      file.size(F("NAMESPACE"))
    )

    ## [1] 880

The `make_fix_file()` member function also accepts an optional `path` argument, in case you know your project’s root but the current working directory is somewhere outside. Take a look at the `thisfile()` function in the `kimisc` package for getting the path to the current script or `knitr` document.

## `testthat` files

Tests run with `testthat` commonly use files that live below the `tests/testthat` directory. Ideally, this should work in the following situation:

- During package development (working directory: package root)
- When testing with `devtools::test()` (working directory: `tests/testthat`)
- When running `R CMD check` (working directory: a renamed recursive copy of `tests`)

The `is_testthat` criterion allows robust lookup of test files.

    is_testthat

    ## Root criterion: directory name is `testthat` (also look in subdirectories: `tests/testthat`, `testthat`)

The example code below lists all files in the hierarchy test directory. It uses two project root lookups in total, so that it also works when rendering the vignette (sigh):

    dir(is_testthat$find_file("hierarchy", path = is_r_package$find_file()))

    ## [1] "DESCRIPTION"     "a"               "b"               "c"
    ## [5] "hierarchy.Rproj"

## 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.