Files and Directories in R: A Reproducible Guide

List, find, create, copy, move, and delete files the portable way — with base R and the modern fs package, plus here() for project-relative paths

Programming

A practical reference for working with files and directories in R: build and manipulate paths, list and find files, check existence and file info, and create, copy, move, rename, and delete files and folders — shown in both base R (file.exists(), dir.create(), list.files(), file.path(), …) and the modern fs package. The habit that matters most is portable paths: never setwd(), never a hard-coded absolute path — use here::here() and file.path() so your script runs on someone else’s machine too.

Author
Published

July 18, 2026

Modified

July 18, 2026

TipWhat you’ll learn
  • Build portable paths — why you should never call setwd() or hard-code an absolute path, and what to do instead (here::here() + file.path()).
  • List and find files and folders — by pattern, recursively, with list.files() / list.dirs().
  • Check what exists and read file infofile.exists(), dir.exists(), file.size(), file.info().
  • Create, copy, move, rename, and safely delete files and directories.
  • The same operations in the modern fs package — a consistent, cross-platform alternative to base R’s file functions — with a base ↔︎ fs cheat-sheet.

Reading and writing data in R almost always means touching the file system: finding the CSV you saved yesterday, creating an output folder, cleaning up a directory of results. R has everything you need built in — plus a modern package, fs, that smooths over base R’s rough edges.

The single most important idea is not a function, though — it’s a habit. A path can be absolute (/Users/you/project/data.csv — spelled out from the file-system root) or relative (data/raw/survey.csv — interpreted from your current working directory, the folder R treats as “here”). A project-relative path is a relative path anchored to your project’s root folder rather than to wherever R happens to be sitting. Getting that anchoring right is the difference between a script that runs everywhere and one that runs only on the machine that wrote it. We’ll start there.

Build paths the portable way

The tempting shortcut is to set the working directory at the top of a script:

setwd("/Users/me/analysis/project")   # DON'T — see below
read.csv("data/raw/survey.csv")

This works exactly once — on your machine, in that folder, today. The moment a collaborator opens the script, or you move the project, or it runs in CI, that absolute path is wrong. As the tidyverse’s project-oriented workflow guide puts it bluntly: “The chance of the setwd() command having the desired effect — making the file paths work — for anyone besides its author is 0%.”

Two tools fix this. First, file.path() joins path pieces with the correct separator for whatever operating system you’re on (/ on macOS/Linux, \ on Windows) — so you never hard-code a separator:

# Joins with the right separator for the current OS
file.path("data", "raw", "survey.csv")
[1] "data/raw/survey.csv"

Second, the here package resolves paths from your project root — the folder containing your .Rproj or .git. here::here() returns the same logical location on every machine, because it finds the root at run time instead of trusting the current directory. The here package documentation states it plainly: “The here package creates paths relative to the top-level directory.”

library(here)
# here() finds the project root, then builds a path down from it.
# The result is resolved for YOUR machine, but the CODE is identical
# on everyone's machine — that's the point.
here("data", "raw", "survey.csv")
#> "/Users/you/analysis/project/data/raw/survey.csv"

Use here::here() for paths into your project, and file.path() (or fs::path(), below) for assembling paths generally. Neither depends on setwd(), so your script survives being moved, shared, or run by a machine.

NoteCheck what the machine hands you

When you paste in a path-handling snippet — from an AI assistant, from StackOverflow, or from an old tutorial — the tell-tale anti-pattern is a bare setwd("/Users/…") or a hard-coded absolute path. It runs on the author’s machine and nowhere else. Before you trust it, ask one question: would this resolve on a collaborator’s laptop, or in CI? If it calls setwd() or hard-codes an absolute path, swap in here::here() and file.path(). That check — matching the snippet against the reproducibility assumption, not just against “does it run for me right now” — is the judgment a generated answer won’t make for you.

For the runnable examples below, we’ll work inside a throwaway scratch folder in R’s per-session temporary directory (tempdir()), so nothing here touches your real files:

# A disposable scratch folder — unique to this R session, auto-cleaned on exit.
scratch <- file.path(tempdir(), "files-demo")
unlink(scratch, recursive = TRUE)   # start from a clean slate
dir.create(scratch)

# Seed it with a few files to work with.
writeLines("a,b\n1,2", file.path(scratch, "data.csv"))
writeLines("notes",    file.path(scratch, "README.md"))
writeLines("x",        file.path(scratch, "script.R"))

list.files(scratch)
[1] "data.csv"  "README.md" "script.R" 

list.files() returns just the file names (not the full temp path), which is exactly what we want to display — reproducible, and free of any machine-specific location.

List and find files

list.files() (also spelled dir()) lists the entries in a folder. Give it a pattern — a regular expression — to filter, and recursive = TRUE to descend into subfolders. The base R documentation documents both arguments.

# Only the CSV files (pattern is a regular expression)
list.files(scratch, pattern = "\\.csv$")
[1] "data.csv"
# Add a subfolder, then list everything recursively.
dir.create(file.path(scratch, "raw"))
writeLines("y", file.path(scratch, "raw", "extra.csv"))

list.files(scratch, recursive = TRUE)
[1] "data.csv"      "raw/extra.csv" "README.md"     "script.R"     

Recursive listing returns paths relative to the folder you asked about (raw/extra.csv), which keeps the output portable. To list only directories, use list.dirs():

# full.names = FALSE keeps the names relative and readable
list.dirs(scratch, full.names = FALSE, recursive = TRUE)
[1] ""    "raw"

(The empty string "" is the top folder itself.) The modern equivalent is fs::dir_ls(), which returns a tidy character vector and takes a glob (like *.csv) or regexp filter:

library(fs)
dir_ls(scratch, recurse = TRUE, glob = "*.csv")

Check existence and file info

Before you read or overwrite anything, check it’s there. file.exists() and dir.exists() return simple logicals — perfect for an if () guard. See the base R file.exists and dir.exists help pages.

file.exists(file.path(scratch, "data.csv"))    # is the file there?
[1] TRUE
dir.exists(file.path(scratch, "raw"))          # is the folder there?
[1] TRUE
file.exists(file.path(scratch, "missing.txt")) # a file that isn't
[1] FALSE

For size and metadata, file.size() returns bytes, and file.info() returns a full metadata data frame (size, modification time, permissions, and whether it’s a directory):

file.size(file.path(scratch, "data.csv"))   # bytes
[1] 8
# file.info() returns a metadata data frame; pull the column you need
file.info(file.path(scratch, "data.csv"))$size   # bytes
file.info(file.path(scratch, "data.csv"))$mtime  # last modified
file.info(file.path(scratch, "data.csv"))$isdir  # FALSE for a file

The fs versions are fs::file_exists(), fs::dir_exists(), and fs::file_info() — same idea, vectorised and consistently named.

Create files and directories

dir.create() makes a folder; pass recursive = TRUE to create any missing parent folders in one call (without it, a missing parent is an error). file.create() makes an empty file.

# Make nested folders in one call — parents are created as needed
nested <- file.path(scratch, "results", "figures")
dir.create(nested, recursive = TRUE)
dir.exists(nested)
[1] TRUE
# Make an empty file
file.create(file.path(scratch, "log.txt"))
[1] TRUE
file.exists(file.path(scratch, "log.txt"))
[1] TRUE

The fs equivalent, fs::dir_create(), is recursive by default (no flag needed) and doesn’t warn if the folder already exists — one of the small consistency wins that make fs pleasant:

library(fs)
dir_create(path(scratch, "results", "figures"))  # parents made automatically
file_create(path(scratch, "log.txt"))

Copy, move, and rename

file.copy() duplicates a file; file.rename() both renames and moves (there’s no separate “move” in base R — renaming to a different folder moves it). Both return TRUE on success.

# Copy data.csv to a backup name
file.copy(file.path(scratch, "data.csv"),
          file.path(scratch, "data-backup.csv"))
[1] TRUE
# Rename (= move) script.R to analysis.R
file.rename(file.path(scratch, "script.R"),
            file.path(scratch, "analysis.R"))
[1] TRUE
list.files(scratch, pattern = "\\.(csv|R)$")
[1] "analysis.R"      "data-backup.csv" "data.csv"       

fs splits these into clearly-named verbs — fs::file_copy(), fs::file_move() — so the intent reads off the code:

library(fs)
file_copy(path(scratch, "data.csv"), path(scratch, "data-backup.csv"))
file_move(path(scratch, "script.R"), path(scratch, "analysis.R"))

Delete safely

Deletion is the one operation with no undo, so treat it with care. file.remove() deletes files. unlink() deletes files or folders — but for a folder you must pass recursive = TRUE, or nothing happens. The unlink() documentation is explicit: “If recursive = FALSE directories are not deleted, not even empty ones.”

# Remove a single file
file.remove(file.path(scratch, "log.txt"))
[1] TRUE
file.exists(file.path(scratch, "log.txt"))     # gone
[1] FALSE
# Remove a whole folder tree — recursive = TRUE is required for directories
unlink(file.path(scratch, "results"), recursive = TRUE)
dir.exists(file.path(scratch, "results"))       # gone
[1] FALSE

Because there’s no recycle bin here, check before you delete — guard the call with file.exists() / dir.exists(), and never point unlink(recursive = TRUE) at a path you built by string-pasting without inspecting it first. The fs verbs, fs::file_delete() and fs::dir_delete(), are self-documenting about which one you meant.

base R and fs: a side-by-side map

Base R’s file functions grew over decades, so their names are inconsistent (file.exists() but dir.create(); unlink() for “delete”). fs gives you one consistent, cross-platform, vectorised interface — the fs documentation describes it as “a cross-platform, uniform interface to file system operations.” Every fs function takes and returns tidy fs_path character vectors, uses / on every OS, and names things predictably (dir_* for directories, file_* for files, path_* for path strings). Here’s the translation:

Task Base R fs
Build a path file.path("a", "b") fs::path("a", "b")
Project-relative path here::here("a", "b") here::here("a", "b")
List files list.files(dir, pattern=) fs::dir_ls(dir, glob=)
List directories list.dirs(dir) fs::dir_ls(dir, type="directory")
File exists? file.exists(p) fs::file_exists(p)
Directory exists? dir.exists(p) fs::dir_exists(p)
File size / info file.size(p) / file.info(p) fs::file_info(p)
Create directory dir.create(p, recursive=TRUE) fs::dir_create(p)
Create empty file file.create(p) fs::file_create(p)
Copy a file file.copy(from, to) fs::file_copy(from, to)
Move / rename file.rename(from, to) fs::file_move(from, to)
Delete a file file.remove(p) / unlink(p) fs::file_delete(p)
Delete a directory unlink(p, recursive=TRUE) fs::dir_delete(p)
File extension tools::file_ext(p) fs::path_ext(p)

Base R is always available with zero dependencies — reach for it in packages and scripts you want dependency-light. Reach for fs when you want the consistency, the vectorised behaviour, and paths that look the same on every operating system.

Common issues

setwd() works for you but breaks for everyone else. An absolute path or a setwd() call bakes your machine’s folder layout into the script. Delete it. Anchor paths to the project root with here::here() and build them with file.path() (or fs::path()), so the code is identical on every machine even though the resolved path differs.

Backslash paths fail on other operating systems. Hard-coding "data\\raw\\file.csv" (Windows) breaks on macOS/Linux, and vice-versa. Never type the separator yourself — let file.path() or fs::path() insert the right one for the current OS.

unlink() “didn’t delete” my folder. By default unlink() refuses to remove directories — you must pass recursive = TRUE. (file.remove() only ever removes files, so it silently fails on a folder too.) For a directory, use unlink(path, recursive = TRUE) or the clearer fs::dir_delete().

Frequently asked questions

Use list.files("path/to/folder"), which returns the file names in that folder. Add pattern = "\\.csv$" to filter with a regular expression, recursive = TRUE to descend into subfolders, and full.names = TRUE if you want complete paths rather than bare names. The modern equivalent is fs::dir_ls("path/to/folder"), which accepts a glob = "*.csv" filter and returns a tidy path vector.

Use dir.create("path/to/folder"). To create any missing parent folders in the same call, add recursive = TRUE — otherwise a missing parent is an error. fs::dir_create("path/to/folder") does the same thing, is recursive by default, and doesn’t warn if the folder already exists.

Use file.exists("path/to/file"), which returns TRUE or FALSE — ideal for guarding a read or write with an if (). For a folder, use dir.exists("path/to/folder"). The fs versions are fs::file_exists() and fs::dir_exists().

No — avoid it. setwd() hard-codes your machine’s folder layout into the script, so it works only on your computer, in that folder, right now; it breaks for collaborators, after a move, and in CI. Instead, keep your work in a project (an RStudio .Rproj or a Git repo) and build paths with here::here(), which resolves them relative to the project root on any machine.

They do the same jobs, but fs is more consistent. Base R’s functions grew over time and are unevenly named (file.exists() vs dir.create(); unlink() for “delete”), return different value types, and can behave differently across operating systems. The fs package gives one uniform, vectorised, cross-platform interface — predictable dir_* / file_* / path_* names, / separators everywhere, and tidy path vectors. Base R needs no dependencies; fs needs the package but is nicer for interactive and pipeline code.

Test your understanding

You want a snippet that empties and recreates an output/ folder at your project root, ready for a fresh run — deleting it if it’s there, then making it again. Write it so it runs unchanged on a collaborator’s machine (no setwd(), no absolute path).

TipHint

You need three pieces: a project-relative path (not an absolute one), a recursive delete that works on a directory, and a create call. Which function refuses to remove a folder unless you tell it to recurse?

TipSolution
library(here)
out <- here("output")                 # project-relative — portable
unlink(out, recursive = TRUE)         # remove the folder if it exists
dir.create(out)                        # recreate it, empty
  • here("output") anchors the path to the project root, so the code is identical on every machine.
  • unlink(..., recursive = TRUE) is required to delete a directory — plain unlink() (or file.remove()) won’t remove a folder. unlink() on a path that doesn’t exist is harmless, so this is safe even on the first run.
  • dir.create(out) remakes the now-empty folder.

The fs one-liner is fs::dir_create(here("output")) after fs::dir_delete() — or just re-create, since dir_create() is happy if it already exists.

Quick check. You call unlink(file.path(scratch, "results")) (without recursive = TRUE) on a folder. What happens?

Nothing — the folder is left in place. unlink() refuses to delete directories unless you pass recursive = TRUE; without it, “not even empty ones” are removed. Add recursive = TRUE (or use fs::dir_delete()) to actually remove a directory tree.

Conclusion

Working with files in R is two things: the functions, and the habit. The functions are small and learnable — list.files(), file.exists(), dir.create(recursive = TRUE), file.copy(), file.rename(), unlink(recursive = TRUE) — with the modern fs package offering a consistent, cross-platform version of each. The habit is what makes your code reproducible: never setwd(), never a hard-coded absolute path — anchor everything with here::here() and assemble it with file.path() (or fs::path()). Do that, and a script you wrote today still runs on a collaborator’s laptop, in CI, and on your own machine a year from now.

Citation

BibTeX citation:
@online{kassambara2026,
  author = {Kassambara, Alboukadel},
  title = {Files and {Directories} in {R:} {A} {Reproducible} {Guide}},
  date = {2026-07-18},
  url = {https://www.datanovia.com/blog/files-and-directories-in-r},
  langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2026. “Files and Directories in R: A Reproducible Guide.” July 18. https://www.datanovia.com/blog/files-and-directories-in-r.