Efficient File Management in R with {fs}

Jadey Ryan // October 29, 2025
R-Ladies St. Louis

Jadey Ryan

Context setting

xkcdc omic of person standing behind another person looking at their documents folder on their computer saying 'Oh my god.'. The documents folder has hundreds of untitled documents.

PhD comic with panels showing the back and forth of a person revising a document with their adviser contually marking up each draft, while each draft is suffixed with final, revision, comments, and corrections.

Want to follow along?

Download these two R scripts from this workshop’s GitHub repo

https://github.com/jadeynryan/fs-rladies-sl/tree/main


demo/generate-spooky-folder.R


quarto::qmd_to_r_script("slides.qmd", script = "demo/code-from-slides.R")

demo/code-from-slides.R


Or, you can copy code chunks from these slides into an R script:
https://jadeyryan.quarto.pub/fs-rladies-sl/

What’s wrong with this folder?

source("demo/generate-spooky-folder.R")
demo/spooky-folder
├── Data
│   ├── ghost-viz.R
│   ├── Pumpkin-Stats.CSV
│   ├── skeletonReport.doc
│   ├── skeletonReport.qmd
│   ├── vampireBATS.txt
│   └── WITCH_list.docx
├── images
│   └── witchCauldron.png
├── Misc Files
│   └── BlackCats.png
├── R
│   ├── ghost data (final) v2.CSV
│   ├── haunted_house.xlsx
│   └── untitled 10 31.R
└── REPORTS
    ├── ghost data (final).CSV
    └── ghost data.csv

What’s wrong with this folder?

  • Inconsistent capitalization
  • Inconsistent use of delimiters (e.g., spaces, hyphens, underscores)
  • File extensions don’t match their subfolder
  • Difficult to know which is actually the final version

Woes of inconsistent, poorly named files

  • Hard to find what you need when you need it
  • Can break scripts and analysis pipelines
  • Nightmare for collaborators and your future self
  • Errors from using outdated versions
  • Wastes time and energy

Meet {fs}

  • File system operations (e.g., create, rename, move, delete)
  • Works on multiple operating systems
  • Inspired by Rust’s fs module
  • {fs} pkgdown site

fs R package logo

The basics

{fs} functions

  • Consistent syntax with four main categories
    1. path_ for manipulating and constructing paths
    2. file_ for files
    3. dir_ for directories
    4. link_ for links
  • {fs} function reference

{fs} vs base R

{fs} Base R
Vectorized (accept multiple paths as input) Inconsistently vectorized
Predictable return values that convey a path Sometimes are logical and sometimes have error codes
Consistent verb function names (file_exist() and file_show()) Different naming conventions (e.g., file.exist() and browseURL())
Throws an error if an operation fails Sometimes will only generate a warning and OS dependent error code


{fs} is more consistent and intuitive.

Get started

library("fs")
# recurse = how many levels of subdirectories to compute on. recurse = TRUE means all of them.
dir_tree(recurse = 2)
.
├── demo
│   ├── code-from-slides.R
│   ├── generate-spooky-folder.R
│   └── spooky-folder
│       ├── Data
│       ├── images
│       ├── Misc Files
│       ├── R
│       └── REPORTS
├── fs-rladies-sl.Rproj
├── images
│   ├── bulk-density.jpg
│   ├── documents-xkcd.png
│   ├── final-phd-comics.gif
│   ├── mts.jpg
│   ├── mts.webp
│   ├── rangeland.jpg
│   ├── soil-sampling.jpg
│   └── witchy-jr-cat-fs.png
├── README.md
├── slides.html
├── slides.qmd
├── slides.rmarkdown
├── slides.scss
├── slides_files
│   └── libs
│       ├── clipboard
│       ├── quarto-contrib
│       ├── quarto-html
│       └── revealjs
├── _extensions
│   └── quarto-ext
│       ├── fontawesome
│       └── pointer
└── _publish.yml

Constructing file paths

  • Straight-forward concatenation with the right separator to work across operating systems!
    • Windows: C:\Users\username\Documents\file.txt
    • macOS: /Users/username/Documents/file.txt

Construct a path with the right separator

path <- path("halloween", "images", "spooky-cat", ext = "png")
path
halloween/images/spooky-cat.png

Special fs_path class

class(path)
[1] "fs_path"   "character"

Constructed path is relative to working directory

path_abs(path)
C:/Users/jryan/Documents/R/projects/fs-rladies-sl/halloween/images/spooky-cat.png

Check for existence

Check if spooky-cat.png exists in the halloween folder

file_exists(path)
halloween/images/spooky-cat.png 
                          FALSE 


Create a path to an existing image

real_path <- path("images", "mts.webp")
file_exists(real_path)
images/mts.webp 
           TRUE 


Open the image

file_show(real_path)

Create, move, copy, delete

Action Function Use Case
Create* file_create() Create a new file
Copy* file_copy() Copy a file
Move file_move() Move or rename a file
Delete* file_delete() Delete a file

*These also work with dir_ and link_ prefixes

Create

Create root directory

root <- path("demo", "scary-analysis")
dir_create(root)

Set up sub-directories

subdirs <- c("R", "data/raw", "data/processed", "output", "reports", "images")
dir_create(root, subdirs)

Warning

Must pass vector of names to dir_create. Otherwise, dir_create(root, "R", "data/raw", "data/processed", "output", "reports", "images") will result in this folder path: scary-analysis/R/data/raw/data/processed/output/reports/images/.

Create files

scripts <- path(root,
                "R",
                c("01-load-data.R", "02-wrangle-data.R", "03-model.R", "04-visualization.R"))

file_create(scripts)

Create results

dir_tree("demo/scary-analysis/")
demo/scary-analysis/
├── data
│   ├── processed
│   └── raw
├── images
├── output
├── R
│   ├── 01-load-data.R
│   ├── 02-wrangle-data.R
│   ├── 03-model.R
│   └── 04-visualization.R
└── reports

Copy

dir_copy(path = "demo/scary-analysis", 
         new_path = "demo/other-scary-analysis")

Files and directories exist in both locations with dir_copy()

dir_tree("demo/scary-analysis")
demo/scary-analysis
├── data
│   ├── processed
│   └── raw
├── images
├── output
├── R
│   ├── 01-load-data.R
│   ├── 02-wrangle-data.R
│   ├── 03-model.R
│   └── 04-visualization.R
└── reports
dir_tree("demo/other-scary-analysis")
demo/other-scary-analysis
├── data
│   ├── processed
│   └── raw
├── images
├── output
├── R
│   ├── 01-load-data.R
│   ├── 02-wrangle-data.R
│   ├── 03-model.R
│   └── 04-visualization.R
└── reports

Copy safely

By default, file_copy() will fail if the file already exists.

file_copy("demo/scary-analysis/R/01-load-data.R",
          "demo/other-scary-analysis/R/01-load-data.R")
Error: [EEXIST] Failed to copy 'demo/scary-analysis/R/01-load-data.R' to 'demo/other-scary-analysis/R/01-load-data.R': file already exists


Unless, you use overwrite = TRUE.

file_copy("demo/scary-analysis/R/01-load-data.R",
          "demo/other-scary-analysis/R/01-load-data.R",
          overwrite = TRUE)

Move

Create a new src folder to move scripts into

dir_create("demo/scary-analysis/src")


List all scripts in R folder with dir_ls()

scripts <- dir_ls("demo/scary-analysis/R")


Move the scripts into the new src folder

file_move(path = scripts, new_path = "demo/scary-analysis/src")

Move results

R scripts are now only in the src folder after being moved from the R folder

dir_tree("demo/scary-analysis")
demo/scary-analysis
├── data
│   ├── processed
│   └── raw
├── images
├── output
├── R
├── reports
└── src
    ├── 01-load-data.R
    ├── 02-wrangle-data.R
    ├── 03-model.R
    └── 04-visualization.R

Rename with move

Use file_move() to rename directories and files

file_move(path = "demo/other-scary-analysis/R",
          new_path = "demo/other-scary-analysis/src")

dir_tree("demo/other-scary-analysis")
demo/other-scary-analysis
├── data
│   ├── processed
│   └── raw
├── images
├── output
├── reports
└── src
    ├── 01-load-data.R
    ├── 02-wrangle-data.R
    ├── 03-model.R
    └── 04-visualization.R

Note

If the goal is to rename a folder, don’t use dir_create() first like we did in the last example. Just use file_move() in one step.

Delete

Delete the empty R folder from the first move example with dir_delete()

dir_delete("demo/scary-analysis/R")
dir_tree("demo/scary-analysis")
demo/scary-analysis
├── data
│   ├── processed
│   └── raw
├── images
├── output
├── reports
└── src
    ├── 01-load-data.R
    ├── 02-wrangle-data.R
    ├── 03-model.R
    └── 04-visualization.R

Clean up a messy folder

Revisit our spooky folder

dir_tree("demo/spooky-folder/")
demo/spooky-folder/
├── Data
│   ├── ghost-viz.R
│   ├── Pumpkin-Stats.CSV
│   ├── skeletonReport.doc
│   ├── skeletonReport.qmd
│   ├── vampireBATS.txt
│   └── WITCH_list.docx
├── images
│   └── witchCauldron.png
├── Misc Files
│   └── BlackCats.png
├── R
│   ├── ghost data (final) v2.CSV
│   ├── haunted_house.xlsx
│   └── untitled 10 31.R
└── REPORTS
    ├── ghost data (final).CSV
    └── ghost data.csv

Naming conventions

What do you use for folders and files? Is this standardized within just your projects? Team? Organization?

Cartoon representations of common cases in coding. A snake screams "SCREAMING_SNAKE_CASE" into the face of a camel (wearing ear muffs) with "camelCase" written along its back. Vegetables on a skewer spell out "kebab-case" (words on a skewer). A mellow, happy looking snake has text "snake_case" along it.

Artwork by Allison Horst

Meet {janitor}

Title text: “janitor::clean_names(): convert all column names to *_case!” Below, a cartoon beaver putting shapes with long, messy column names (pulled from a bin labeled “MESS” and “not so awesome column names”) into a contraption that converts them to lower snake case. The output has stylized text reading “Way more deal-withable column names.” Learn more about clean_names and other *awesome* data cleaning tools in janitor.

Artwork by Allison Horst

{fs} meets {janitor}

files_old <- dir_ls("demo/spooky-folder/", recurse = TRUE)
files_new <- janitor::make_clean_names(files_old, case = "snake")

Note

You could just use {stringr} for cleaning, but {janitor} is more comprehensive so you don’t need to write as much regex.

E.g., myImportantFile would be be cleaned to my_important_file.

See the make_clean_names() docs.

Arguments that might come in handy: case, replace, parsing_option, abbreviations, sep_out.

Always check files before renaming!

Uh oh – the directory separators and extensions were lost!

files_old
demo/spooky-folder/Data
demo/spooky-folder/Data/ghost-viz.R
demo/spooky-folder/Data/Pumpkin-Stats.CSV
demo/spooky-folder/Data/skeletonReport.doc
demo/spooky-folder/Data/skeletonReport.qmd
demo/spooky-folder/Data/vampireBATS.txt
demo/spooky-folder/Data/WITCH_list.docx
demo/spooky-folder/images
demo/spooky-folder/images/witchCauldron.png
demo/spooky-folder/Misc Files
demo/spooky-folder/Misc Files/BlackCats.png
demo/spooky-folder/R
demo/spooky-folder/R/ghost data (final) v2.CSV
demo/spooky-folder/R/haunted_house.xlsx
demo/spooky-folder/R/untitled 10 31.R
demo/spooky-folder/REPORTS
demo/spooky-folder/REPORTS/ghost data (final).CSV
demo/spooky-folder/REPORTS/ghost data.csv
files_new
 [1] "demo_spooky_folder_data"                        
 [2] "demo_spooky_folder_data_ghost_viz_r"            
 [3] "demo_spooky_folder_data_pumpkin_stats_csv"      
 [4] "demo_spooky_folder_data_skeleton_report_doc"    
 [5] "demo_spooky_folder_data_skeleton_report_qmd"    
 [6] "demo_spooky_folder_data_vampire_bats_txt"       
 [7] "demo_spooky_folder_data_witch_list_docx"        
 [8] "demo_spooky_folder_images"                      
 [9] "demo_spooky_folder_images_witch_cauldron_png"   
[10] "demo_spooky_folder_misc_files"                  
[11] "demo_spooky_folder_misc_files_black_cats_png"   
[12] "demo_spooky_folder_r"                           
[13] "demo_spooky_folder_r_ghost_data_final_v2_csv"   
[14] "demo_spooky_folder_r_haunted_house_xlsx"        
[15] "demo_spooky_folder_r_untitled_10_31_r"          
[16] "demo_spooky_folder_reports"                     
[17] "demo_spooky_folder_reports_ghost_data_final_csv"
[18] "demo_spooky_folder_reports_ghost_data_csv"      

Split, clean, and reconstruct

To avoid the separators being turned into underscores, let’s split the directory, name, and extension with path_split()

paths <- dir_ls("demo/spooky-folder/", recurse = TRUE)
parts <- path_split(paths)
parts[1:3]
[[1]]
[1] "demo"          "spooky-folder" "Data"         

[[2]]
[1] "demo"          "spooky-folder" "Data"          "ghost-viz.R"  

[[3]]
[1] "demo"              "spooky-folder"     "Data"             
[4] "Pumpkin-Stats.CSV"

Clean with {janitor}

parts_clean <- purrr::map(parts, \(part) janitor::make_clean_names(part, case = "snake"))

Reconstruct

paths_clean <- path_join(parts_clean)

Always check files before renaming!

Uh oh – the directory separators are maintained, but the extensions are still lost!

paths
demo/spooky-folder/Data
demo/spooky-folder/Data/ghost-viz.R
demo/spooky-folder/Data/Pumpkin-Stats.CSV
demo/spooky-folder/Data/skeletonReport.doc
demo/spooky-folder/Data/skeletonReport.qmd
demo/spooky-folder/Data/vampireBATS.txt
demo/spooky-folder/Data/WITCH_list.docx
demo/spooky-folder/images
demo/spooky-folder/images/witchCauldron.png
demo/spooky-folder/Misc Files
demo/spooky-folder/Misc Files/BlackCats.png
demo/spooky-folder/R
demo/spooky-folder/R/ghost data (final) v2.CSV
demo/spooky-folder/R/haunted_house.xlsx
demo/spooky-folder/R/untitled 10 31.R
demo/spooky-folder/REPORTS
demo/spooky-folder/REPORTS/ghost data (final).CSV
demo/spooky-folder/REPORTS/ghost data.csv
paths_clean
demo/spooky_folder/data
demo/spooky_folder/data/ghost_viz_r
demo/spooky_folder/data/pumpkin_stats_csv
demo/spooky_folder/data/skeleton_report_doc
demo/spooky_folder/data/skeleton_report_qmd
demo/spooky_folder/data/vampire_bats_txt
demo/spooky_folder/data/witch_list_docx
demo/spooky_folder/images
demo/spooky_folder/images/witch_cauldron_png
demo/spooky_folder/misc_files
demo/spooky_folder/misc_files/black_cats_png
demo/spooky_folder/r
demo/spooky_folder/r/ghost_data_final_v2_csv
demo/spooky_folder/r/haunted_house_xlsx
demo/spooky_folder/r/untitled_10_31_r
demo/spooky_folder/reports
demo/spooky_folder/reports/ghost_data_final_csv
demo/spooky_folder/reports/ghost_data_csv

{fs} meets {stringr}

Use path_ext() to get the extensions and {stringr} to replace the underscore before the extension with a period

paths_almost_clean <- paths_clean

# Get extensions
exts <- path_ext(paths) |> 
  # Remove blanks
  stringr::str_subset("\\S") |> 
  # Remove duplicates
  stringr::str_unique() |> 
  # Make lowercase
  stringr::str_to_lower()

# Make dynamic regex pattern including extensions
pattern <- paste0("_(?=(", paste(exts, collapse = "|"), ")$)")

# Replace underscore before ext with period
paths_clean <- stringr::str_replace(paths_almost_clean, pattern, ".") |> 
  # Make fs_path again
  path()

Always check files before renaming!

Yay! Let’s just re-capitalize the R folder and .R extensions.

paths_almost_clean
demo/spooky_folder/data
demo/spooky_folder/data/ghost_viz_r
demo/spooky_folder/data/pumpkin_stats_csv
demo/spooky_folder/data/skeleton_report_doc
demo/spooky_folder/data/skeleton_report_qmd
demo/spooky_folder/data/vampire_bats_txt
demo/spooky_folder/data/witch_list_docx
demo/spooky_folder/images
demo/spooky_folder/images/witch_cauldron_png
demo/spooky_folder/misc_files
demo/spooky_folder/misc_files/black_cats_png
demo/spooky_folder/r
demo/spooky_folder/r/ghost_data_final_v2_csv
demo/spooky_folder/r/haunted_house_xlsx
demo/spooky_folder/r/untitled_10_31_r
demo/spooky_folder/reports
demo/spooky_folder/reports/ghost_data_final_csv
demo/spooky_folder/reports/ghost_data_csv
paths_clean
demo/spooky_folder/data
demo/spooky_folder/data/ghost_viz.r
demo/spooky_folder/data/pumpkin_stats.csv
demo/spooky_folder/data/skeleton_report.doc
demo/spooky_folder/data/skeleton_report.qmd
demo/spooky_folder/data/vampire_bats.txt
demo/spooky_folder/data/witch_list.docx
demo/spooky_folder/images
demo/spooky_folder/images/witch_cauldron.png
demo/spooky_folder/misc_files
demo/spooky_folder/misc_files/black_cats.png
demo/spooky_folder/r
demo/spooky_folder/r/ghost_data_final_v2.csv
demo/spooky_folder/r/haunted_house.xlsx
demo/spooky_folder/r/untitled_10_31.r
demo/spooky_folder/reports
demo/spooky_folder/reports/ghost_data_final.csv
demo/spooky_folder/reports/ghost_data.csv

Make R uppercase again

paths_clean <- paths_clean |> 
  # Capitalize R folder name
  stringr::str_replace("/r(?=/|$)", "/R") |> 
  # Capitalize R extension
  stringr::str_replace("\\.r", ".R")

# Show the updated files containing R
paths_clean |> 
  stringr::str_subset("R")
[1] "demo/spooky_folder/data/ghost_viz.R"         
[2] "demo/spooky_folder/R"                        
[3] "demo/spooky_folder/R/ghost_data_final_v2.csv"
[4] "demo/spooky_folder/R/haunted_house.xlsx"     
[5] "demo/spooky_folder/R/untitled_10_31.R"       

Complete the renaming

But wait… our new folders don’t exist yet!

file_move(path = paths, new_path = paths_clean)
Error: [ENOENT] Failed to move 'demo/spooky-folder/Data' to 'demo/spooky_folder/data': no such file or directory


Warning

{fs} gotcha! file_move() and file_copy() don’t automatically create parent folders!

Create parent folders first

Get the parent folders with path_dir()

parents <- unique(path_dir(paths_clean))
parents
[1] "demo/spooky_folder"            "demo/spooky_folder/data"      
[3] "demo/spooky_folder/images"     "demo/spooky_folder/misc_files"
[5] "demo/spooky_folder/R"          "demo/spooky_folder/reports"   

Create the parent folders if they don’t already exist

parents |> 
  purrr::map(\(x) {
    if (!dir_exists(x)) {
      dir_create(x)
    }
  })
[[1]]
demo/spooky_folder

[[2]]
demo/spooky_folder/data

[[3]]
demo/spooky_folder/images

[[4]]
demo/spooky_folder/misc_files

[[5]]
demo/spooky_folder/R

[[6]]
demo/spooky_folder/reports

Complete the renaming

Filter out folders from current paths and new paths with !is_dir()

paths <- subset(paths, !is_dir(paths))
paths_clean <- subset(paths_clean, !is_dir(paths_clean))

Rename files with file_move()

file_move(paths, paths_clean)

# Optionally, delete the old spooky-folder
# dir_delete("demo/spooky-folder")

See our pretty, cleaned folder

dir_tree("demo/spooky_folder")
demo/spooky_folder
├── data
│   ├── ghost_viz.R
│   ├── pumpkin_stats.csv
│   ├── skeleton_report.doc
│   ├── skeleton_report.qmd
│   ├── vampire_bats.txt
│   └── witch_list.docx
├── images
│   └── witch_cauldron.png
├── misc_files
│   └── black_cats.png
├── R
│   ├── ghost_data_final_v2.csv
│   ├── haunted_house.xlsx
│   └── untitled_10_31.R
└── reports
    ├── ghost_data.csv
    └── ghost_data_final.csv

Wait… we’re not done yet!

We have nice, consistent names… but the organization is still very wrong!

Use globs to get files based on extension

data <- dir_ls("demo/spooky_folder", recurse = TRUE, glob = "*.csv|*.xlsx")
images <- dir_ls("demo/spooky_folder", recurse = TRUE, glob = "*.png")
misc_files <- dir_ls("demo/spooky_folder", recurse = TRUE, glob = "*.txt")
r <- dir_ls("demo/spooky_folder", recurse = TRUE, glob = "*.R")
reports <- dir_ls("demo/spooky_folder", recurse = TRUE, glob = "*.doc|*.docx|*.qmd")

Move files into appropriate folders

file_move(data, "demo/spooky_folder/data")
file_move(images, "demo/spooky_folder/images")
file_move(misc_files, "demo/spooky_folder/misc_files")
file_move(r, "demo/spooky_folder/R")
file_move(reports, "demo/spooky_folder/reports")

What we started with

source("demo/generate-spooky-folder.R")
demo/spooky-folder
├── Data
│   ├── ghost-viz.R
│   ├── Pumpkin-Stats.CSV
│   ├── skeletonReport.doc
│   ├── skeletonReport.qmd
│   ├── vampireBATS.txt
│   └── WITCH_list.docx
├── images
│   └── witchCauldron.png
├── Misc Files
│   └── BlackCats.png
├── R
│   ├── ghost data (final) v2.CSV
│   ├── haunted_house.xlsx
│   └── untitled 10 31.R
└── REPORTS
    ├── ghost data (final).CSV
    └── ghost data.csv

Our cleaned folder

dir_tree("demo/spooky_folder")
demo/spooky_folder
├── data
│   ├── ghost_data.csv
│   ├── ghost_data_final.csv
│   ├── ghost_data_final_v2.csv
│   ├── haunted_house.xlsx
│   └── pumpkin_stats.csv
├── images
│   ├── black_cats.png
│   └── witch_cauldron.png
├── misc_files
│   └── vampire_bats.txt
├── R
│   ├── ghost_viz.R
│   └── untitled_10_31.R
└── reports
    ├── skeleton_report.doc
    ├── skeleton_report.qmd
    └── witch_list.docx

Cleaning steps

  1. Review folder with dir_tree() to inspect structure and naming inconsistencies
  2. Plan new conventions (e.g., snake_case)
  3. Initial manual edits (e.g., untitled.R)
  4. Generate new names with {janitor} and/or {stringr}
  5. ⚠️Review first before renaming!⚠️
  6. Create new parent folders with dir_create()
  7. Filter out folders from old and new path vectors
  8. Move files with file_move()
  9. Review again with dir_tree()
  10. Any final manual edits

Use functions to repeat this process for multiple folders

Clean paths function

clean_paths <- function(folder, recurse = TRUE) {
  # Split paths into parts
  paths <- dir_ls(folder, recurse = recurse)
  parts <- path_split(paths)

  # Clean to snake_case
  parts_clean <- purrr::map(
    parts,
    \(part) janitor::make_clean_names(part, case = "snake")
  )

  # Reconstruct paths
  paths_clean <- path_join(parts_clean)

  # Get extensions
  exts <- path_ext(paths) |>
    # Remove blanks
    stringr::str_subset("\\S") |>
    # Remove duplicates
    stringr::str_unique() |>
    # Make lowercase
    stringr::str_to_lower()

  # Make dynamic regex pattern including extensions
  pattern <- paste0("_(?=(", paste(exts, collapse = "|"), ")$)")

  # Replace underscore before ext with period and make R uppercase
  paths_clean <- paths_clean |> 
    stringr::str_replace(pattern, ".") |>
    # Capitalize R folder name
    stringr::str_replace("/r(?=/|$)", "/R") |>
    # Capitalize R extension
    stringr::str_replace("\\.r", ".R") |>
    # Make fs_path again
    path()

  return(paths_clean)
}

clean_paths() results

# Start fresh
source("demo/generate-spooky-folder.R")
paths <- dir_ls("demo/spooky-folder", recurse = TRUE)

# Run function
paths_clean <- clean_paths("demo/spooky-folder")
paths_clean
demo/spooky_folder/data
demo/spooky_folder/data/ghost_viz.R
demo/spooky_folder/data/pumpkin_stats.csv
demo/spooky_folder/data/skeleton_report.doc
demo/spooky_folder/data/skeleton_report.qmd
demo/spooky_folder/data/vampire_bats.txt
demo/spooky_folder/data/witch_list.docx
demo/spooky_folder/images
demo/spooky_folder/images/witch_cauldron.png
demo/spooky_folder/misc_files
demo/spooky_folder/misc_files/black_cats.png
demo/spooky_folder/R
demo/spooky_folder/R/ghost_data_final_v2.csv
demo/spooky_folder/R/haunted_house.xlsx
demo/spooky_folder/R/untitled_10_31.R
demo/spooky_folder/reports
demo/spooky_folder/reports/ghost_data_final.csv
demo/spooky_folder/reports/ghost_data.csv

Rename files function

rename_files <- function(old_paths, new_paths) {
  # Get parent folders
  parents <- unique(path_dir(new_paths))
  
  # Create parent folders
  parents |> 
    purrr::map(\(x) {
      if (!dir_exists(x)) {
        dir_create(x)
      }
    })
  
  # Filter out folders from files
  old_paths <- subset(old_paths, !is_dir(old_paths))
  new_paths <- subset(new_paths, !is_dir(new_paths))

    # Rename files
  file_move(old_paths, new_paths)
  
  # See results
  dir_tree(path_common(new_paths))
}

Note

path_common() returns the path common to all the paths passed as input.

rename_files() results

rename_files(paths, paths_clean)
demo/spooky_folder
├── data
│   ├── ghost_data.csv
│   ├── ghost_data_final.csv
│   ├── ghost_data_final_v2.csv
│   ├── ghost_viz.R
│   ├── haunted_house.xlsx
│   ├── pumpkin_stats.csv
│   ├── skeleton_report.doc
│   ├── skeleton_report.qmd
│   ├── vampire_bats.txt
│   └── witch_list.docx
├── images
│   ├── black_cats.png
│   └── witch_cauldron.png
├── misc_files
│   ├── black_cats.png
│   └── vampire_bats.txt
├── R
│   ├── ghost_data_final_v2.csv
│   ├── ghost_viz.R
│   ├── haunted_house.xlsx
│   └── untitled_10_31.R
└── reports
    ├── ghost_data.csv
    ├── ghost_data_final.csv
    ├── skeleton_report.doc
    ├── skeleton_report.qmd
    └── witch_list.docx

Organize files function

organize_files <- function(folder) {
  # List subdirectories and extensions
  data <- dir_ls(folder, recurse = TRUE, glob = "*.csv|*.xlsx")
  images <- dir_ls(folder, recurse = TRUE, glob = "*.png")
  misc_files <- dir_ls(folder, recurse = TRUE, glob = "*.txt")
  r <- dir_ls(folder, recurse = TRUE, glob = "*.R")
  reports <- dir_ls(folder, recurse = TRUE, glob = "*.doc|*.docx|*.qmd")

  # Move files
  file_move(data, path(stringr::str_glue("{folder}/data")))
  file_move(images, path(stringr::str_glue("{folder}/images")))
  file_move(misc_files, path(stringr::str_glue("{folder}/misc_files")))
  file_move(r, path(stringr::str_glue("{folder}/R")))
  file_move(reports, path(stringr::str_glue("{folder}/reports")))

  # See results
  dir_tree(folder)
}

organize_files() results

organize_files("demo/spooky_folder")
demo/spooky_folder
├── data
│   ├── ghost_data.csv
│   ├── ghost_data_final.csv
│   ├── ghost_data_final_v2.csv
│   ├── haunted_house.xlsx
│   └── pumpkin_stats.csv
├── images
│   ├── black_cats.png
│   └── witch_cauldron.png
├── misc_files
│   └── vampire_bats.txt
├── R
│   ├── ghost_viz.R
│   └── untitled_10_31.R
└── reports
    ├── skeleton_report.doc
    ├── skeleton_report.qmd
    └── witch_list.docx

Full cleaning function workflow

# Start fresh
source("demo/generate-spooky-folder.R")

# Get paths to clean
paths <- dir_ls("demo/spooky-folder", recurse = TRUE)

# Run function
paths_clean <- clean_paths("demo/spooky-folder")
paths_clean

# Review paths_clean before running the next functions!
rename_files(paths, paths_clean)
organize_files("demo/spooky_folder")

Bonus content

Query folder and file information

Use file_info() to get a tibble

files <- file_info(dir_ls("demo", recurse = TRUE))
files
# A tibble: 51 × 18
   path                type     size permissions modification_time   user  group
   <fs::path>          <fct> <fs::b> <fs::perms> <dttm>              <chr> <chr>
 1 …code-from-slides.R file    7.75K rw-         2025-10-29 13:36:14 <NA>  <NA> 
 2 …te-spooky-folder.R file      888 rw-         2025-10-29 14:51:06 <NA>  <NA> 
 3 …her-scary-analysis dire…       0 rw-         2025-10-29 15:43:29 <NA>  <NA> 
 4 …cary-analysis/data dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
 5 …sis/data/processed dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
 6 …-analysis/data/raw dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
 7 …ry-analysis/images dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
 8 …ry-analysis/output dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
 9 …y-analysis/reports dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
10 …scary-analysis/src dire…       0 rw-         2025-10-29 15:43:28 <NA>  <NA> 
# ℹ 41 more rows
# ℹ 11 more variables: device_id <dbl>, hard_links <dbl>,
#   special_device_id <dbl>, inode <dbl>, block_size <dbl>, blocks <dbl>,
#   flags <int>, generation <dbl>, access_time <dttm>, change_time <dttm>,
#   birth_time <dttm>

Use that tibble within a tidyverse workflow

files |> 
  dplyr::filter(type == "directory" | stringr::str_detect(path, ".R")) |> 
  dplyr::select(path, type, size, birth_time, modification_time)
# A tibble: 40 × 5
   path                    type     size birth_time          modification_time  
   <fs::path>              <fct> <fs::b> <dttm>              <dttm>             
 1 demo/code-from-slides.R file    7.75K 2025-10-28 15:13:31 2025-10-29 13:36:14
 2 …nerate-spooky-folder.R file      888 2025-10-05 19:05:07 2025-10-29 14:51:06
 3 …o/other-scary-analysis dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:29
 4 …er-scary-analysis/data dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 5 …nalysis/data/processed dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 6 …cary-analysis/data/raw dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 7 …-scary-analysis/images dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 8 …-scary-analysis/output dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 9 …scary-analysis/reports dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
10 …her-scary-analysis/src dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
# ℹ 30 more rows

Or a base R workflow

files[
  files$type == "directory" | grepl("\\.R$", files$path),
  c("path", "type", "size", "birth_time", "modification_time")
  ]
# A tibble: 40 × 5
   path                    type     size birth_time          modification_time  
   <fs::path>              <fct> <fs::b> <dttm>              <dttm>             
 1 demo/code-from-slides.R file    7.75K 2025-10-28 15:13:31 2025-10-29 13:36:14
 2 …nerate-spooky-folder.R file      888 2025-10-05 19:05:07 2025-10-29 14:51:06
 3 …o/other-scary-analysis dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:29
 4 …er-scary-analysis/data dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 5 …nalysis/data/processed dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 6 …cary-analysis/data/raw dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 7 …-scary-analysis/images dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 8 …-scary-analysis/output dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
 9 …scary-analysis/reports dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
10 …her-scary-analysis/src dire…       0 2025-10-29 15:43:28 2025-10-29 15:43:28
# ℹ 30 more rows

Code snippets

Use code snippets in an RStudio project to set up folder scaffolding!

  1. Create new RStudio Project

  2. Go to Tools > Edit Code Snippets... > R

  3. Paste the following (with this exact tabbing):

snippet project
    fs::dir_create(
        c("data/raw", "data/processed", "images", "R", "reports", "output")
    )
  1. To use, type project in an R script or in the Console and then press Shift+Tab.

Resources

Thank you!