library(fs)July 2, 2026
The fs package provides consistent, clean (tidy) tools for working with file/folder paths.
paste() / paste0()str_c() / str_flatten()glue()fs uses consistent function naming conventions, making functions easier to remember:
path_*() for manipulating and constructing paths.file_*() for actions concerning files and file paths.dir_*() for actions concerning directories and directory paths.With functions such as paste(), paste0(), str_c(), str_flatten(), and glue(), you still need to take care of the slash separators.
It's not the most difficult thing in the world, and you can use the sep parameter, but it does require a bit of thinking and can get messy when mixing constant strings with string variables, or when accepting user input.
Examples:
[1] "./dir1/dir2/cat/file.abc" "./dir1/dir2/dog/file.abc" "./dir1/dir2/bird/file.abc"
We can create paths with fs using path():
We can optionally use the ext parameter to set an extension, but this may not be ideal for use when accepting user input for a file name:
Things that are good to know:
When creating paths with fs, directory paths never end in trailing slashes:
Repeated slashes are reduced to single slashes:
Double backslashes are normalised to forward slashes:
Condsider the following file path (coercing to a fs_path is optional):
We can obtain the name of the file using path_file():
We can obtain the directory containing the file using path_dir():
We can also get all the components of the file path, as a list, using path_split():
We can check if a file exists using file_exists():
We can check if a directory exists using dir_exists():
Directories can be created using dir_create():
By default, intermediate directories are also created, as opposed to dir.create() where recursive = FALSE by default.
dir_create() also checks existence of the specified directory. If it already exists, nothing happens. In some cases, it may be more convenient to call dir_create() rather than creating a directory conditionally by first checking for exists with dir_exists().
In base-R, to list all files in a directory, we use list.files(). The fs equivalent is dir_ls(). Observe the following differences in behaviour:
[1] "pizza1.xlsx" "pizza2.xlsx" "pizza3.xlsx"
[1] "../data/excel-data/pizza1.xlsx" "../data/excel-data/pizza2.xlsx" "../data/excel-data/pizza3.xlsx"
Rather than listing only the file names by default in list.files(), dir_ls() always lists the file paths relative to the path argument, equivalent to setting full.names = TRUE in list.files().
Similar to list.files(), we can also supply a regular expression to dir_ls() to filter files:
Alternatively, we can supply a globbing pattern, which behaves similarly to a regular expression:
A common task is to read all files in a directory into a single data frame. This can be achieved with a combination of dir_ls(), purrr::map(), and purrr::list_rbind().
The benefit of using dir_ls() over list.files() is that fs_paths are essentially named character vectors.
This makes it easy to include a column of file directories if you also need to include the source of a data file, without needing to manually set the names of the vector of file names.
# A tibble: 6 × 3
file flavour quantity
<chr> <chr> <dbl>
1 ../data/excel-data/pizza1.xlsx cheese 3
2 ../data/excel-data/pizza1.xlsx pepperoni 4
3 ../data/excel-data/pizza2.xlsx veggie 4
4 ../data/excel-data/pizza2.xlsx hawaiian 5
5 ../data/excel-data/pizza3.xlsx deluxe 3
6 ../data/excel-data/pizza3.xlsx special deluxe 2
# A tibble: 6 × 3
file flavour quantity
<chr> <chr> <dbl>
1 ../data/excel-data/pizza1.xlsx cheese 3
2 ../data/excel-data/pizza1.xlsx pepperoni 4
3 ../data/excel-data/pizza2.xlsx veggie 4
4 ../data/excel-data/pizza2.xlsx hawaiian 5
5 ../data/excel-data/pizza3.xlsx deluxe 3
6 ../data/excel-data/pizza3.xlsx special deluxe 2
Sometimes we need to filter paths after receiving them (e.g. user input).
It would be a shame to leave the fs ecosystem and revert to working with raw strings... Thankfully, we don't need to!
We can filter paths using regular expressions with path_filter():
../data/excel-data/pizza1.xlsx ../data/excel-data/pizza2.xlsx ../data/excel-data/pizza3.xlsx
../data/excel-data/pizza1.xlsx
../data/excel-data/pizza1.xlsx ../data/excel-data/pizza3.xlsx
Sometimes we end up with paths with shorthand characters like ., .., and ~. For example, with system environment variables:
To clean these up, in addition to things like backslashes (on Windows) and get the literal path, we can use path_real():
The equivalent in base-R is normalizePath(), but again, due to how inconsistently functions in base-R are named, this one can be difficult to remember:
Another convenient function is file_move() which allows you to move and/or rename files.
This function is especially useful when you want to rename a large number of files and it would be too time consuming or error-prone to rename the files manually. All you need to specify is the original path of the file and its new path.
I typically only use file_move() in an interactive setting... I think it's rare to need to use this non-interactively because it is typically a one-time thing.
../data/excel-data/pizza1.xlsx ../data/excel-data/pizza3.xlsx
"../data/excel-data/pizza4.xlsx" "../data/excel-data/pizza6.xlsx"
To access supplementary files contained in a package (usually under the inst directory), one can use system.file().
However, this only works for packages that are installed on your computer.
To access files for a package that is under active development (and may yet to be installed on your device), we can use the path_package() function:
Now that you are equipped with here and fs, all of your path problems should vanish!