In R, how to find a file located in any parent directory upper than working directory?

Viewed 149

When I'm located in a given working directory, how can I find a file located any several levels up?

I want to search the file by its name, and I don't know whether the file is located just one directory up, or more levels up. Therefore, I'm looking for a way to go one level up in each time and search for the file. Once it's found, I want to get the absolute full path to the file.

I'm trying to use the {fs} package to this end, but so far don't really know how.

Example

library(fs)

set.seed(2021)

## create a folder in location temp
tmp <- dir_create(file_temp())
tmp
#> /tmp/Rtmpw2JtlE/file31f26c45d04

## create a txt file in `tmp` path
file_create(path(tmp, "my-file.txt"))

## verify the file now exists in `tmp`
dir_ls(tmp)
#> /tmp/Rtmpw2JtlE/file31f26c45d04/my-file.txt

## create a subdirectory within `tmp`, calling it `my_dir`
dir_create(tmp, "my_dir")

## create a subdirectory within `my_dir`, calling it `another_dir`
dir_create(path(tmp, "my_dir_1"), "another_dir")

## create a subdirectory within `another_dir`, calling it `another_dir_2`
dir_create(path(tmp, "my_dir_1"), "another_dir", "another_dir_2")

## now we set the working directory to be at tmp/my_dir_1/another_dir/another_dir_2/
setwd(path(tmp, "my_dir_1", "another_dir", "another_dir_2"))

## verify that the working directory has been set correctly
getwd()
#> [1] "/tmp/Rtmpw2JtlE/file31f26c45d04/my_dir_1/another_dir/another_dir_2"

What I did in the code above was to create the following tree:

tmp
├── my-file.txt
├── my_dir
└── my_dir_1
    └── another_dir
        └── another_dir_2  ## now this is the working directory 

Given that my working directory is another_dir_2, and that the target file is my-file.txt, how could I find the file without knowing that it's 3 levels up? That is, I'd like to first check one level up (i.e., at another_dir), and if my-file.txt is there, return the full path to file. If it's not found at another_dir, go another level up and search there, and so on, until it's found.

Desired Solution

Something like:

find_up("my-file.txt")

## which will return:
[1] "/tmp/Rtmpw2JtlE/file31f26c45d04/my-file.txt"

Any idea how to accomplish this with base R or fs?

Thanks!

5 Answers

I would leverage list.files and all of the arguments that come with as well as setting a max-height argument and whether you stop at the first match or continue until you reach the max height:

find_up(pattern = "my-file.txt")
# [1] "my-file.txt"
find_up(pattern = "my-file.txt", full.names = TRUE)
# [1] "/private/var/folders/mm/vnxjlghn6c79lr0_hd9119tc0000gn/T/Rtmp6UyBzM/file1bed1d2d02f2/my-file.txt"
find_up(pattern = 'my-file1.txt')
# character(0)

find_up(pattern = 'dir')
# [1] "another_dir_2"
find_up(pattern = 'dir', first = FALSE)
# [1] "another_dir_2" "another_dir"   "my_dir"        "my_dir_1"


find_up <- function(path = getwd(), pattern, maxheight = Inf, first = TRUE, ...) {
  level <- 0L
  lf <- NULL
  while (level <= maxheight) {
    lf <- c(lf, list.files(path, pattern, ...))
    if (first && length(lf) || path == dirname(path))
      break
    level <- level + 1L
    path <- dirname(path)
  }
  lf
}

You could write a recursive function. Using Base R functions

find_up <- function(file, path=getwd()){
  d <- dirname(path)
  if(file.exists(here <- file.path(path, file))) here
  else  if(grepl('/', sub('/+$', '', d)))  find_up(file, d)
  else message('file does not exist')
}

find_up("my-file.txt")
find_up('my-file1.txt')

I think the following will do the job:

find_up <- function(filename = "my-file.txt", startingDirectory = getwd()) {
  
  directoryCount <- length(strsplit(startingDirectory,"/")[[1]])
  
  directoriesToCheck <- Reduce(f = function(upperDirectory, notNeeded) {
    dirname(upperDirectory)
  }, x = 1:(directoryCount-1), init = startingDirectory, accumulate = TRUE)
  
  for(directory in directoriesToCheck) {
    currentFiles <- list.files(directory, full.names = TRUE)
    foundIndex <- grep(paste0(filename,"$"),currentFiles)
    if(length(foundIndex)) return(currentFiles[foundIndex])
  }
  
  return(NULL)
}

Something like this?

find_up <- function(file, dir = getwd()) {
  
  if (file %in% list.files(dir)) {
    return(file.path(dir, file))
  }
  
  split_dirs <- strsplit(dir, "/")[[1]]
  
  if (length(split_dirs) == 1) {
    message("No such file exists in directory or parent directories")
    return(invisible(NULL))
  }
  
  find_up(file, paste(head(split_dirs, -1), collapse = "/"))
  
}

find_up("my-file.txt")
#> [1] "[...]/RtmpuG36zw/file207051991704/my-file.txt"

If you want to go high up one level, normalizePath(file.path(path, '..')) would do the trick.

so here's my solution. I don't know how anyone check whether it's root or not, but I am checking it with path == normalizePath(file.path(path, '..'))

find_up = function(fnpatt, path=getwd()) {
  # cat(path, '\n')
  found <- list.files(path, fnpatt, full.names=TRUE)
  if (length(found) > 0) {
    return(found)
  } else {
    path2 = normalizePath(file.path(path, '..'))
    if (path==path2) {
      stop('Nothing found')
    } else {
      return(find_up(fn, path2))
    }
  }
}
Related