ZetCode

Ruby Directory and File Operations

last modified October 18, 2026

Ruby provides the Dir, File, FileUtils, and Find classes for working with the file system. This guide shows how to create and list directories, query file metadata, match paths with glob patterns, rename and delete files, and walk a whole tree. For reading and writing file contents, see the Ruby read file and Ruby write file tutorials. All examples below run inside a temporary directory created by the script itself, so no real files are touched, and the directory is removed at the end.

Directory Basics with Dir

The Dir class handles directories. Dir.pwd returns the current working directory, Dir.mkdir creates a single directory, and Dir.exist? tests whether a path is a directory. Dir.entries includes the special . and .. entries, while Dir.children and Dir.each_child omit them. Glob patterns are matched with Dir.glob; ** matches any number of nested directories.

dir_basics.rb
require 'tmpdir'
require 'fileutils'

Dir.mktmpdir('dfo_demo') do |root|
  Dir.chdir(root) do
    puts "pwd is temp root: #{Dir.pwd == File.realpath(root)}"

    Dir.mkdir('docs')
    FileUtils.mkdir_p('src/lib/util')
    File.write('README.md', "hello\n")
    File.write('src/main.rb', "puts 1\n")

    puts "docs exists: #{Dir.exist?('docs')}"
    puts "missing exists: #{Dir.exist?('nope')}"

    puts "entries: #{Dir.entries('.').sort.inspect}"
    puts "children: #{Dir.children('.').sort.inspect}"

    print "each_child: "
    names = []
    Dir.each_child('src') { |name| names << name }
    puts names.sort.inspect

    puts "glob **/*.rb: #{Dir.glob('**/*.rb').sort.inspect}"
    puts "glob *.md: #{Dir.glob('*.md').inspect}"
  end
end

The script changes into the temporary directory with Dir.chdir using a block, so the previous working directory is restored afterwards. Note that Dir.each_child yields only the names inside src, not the nested lib directory's contents.

$ ./dir_basics.rb
pwd is temp root: true
docs exists: true
missing exists: false
entries: [".", "..", "README.md", "docs", "src"]
children: ["README.md", "docs", "src"]
each_child: ["lib", "main.rb"]
glob **/*.rb: ["src/main.rb"]
glob *.md: ["README.md"]

File Tests and Metadata

The File class provides predicate methods such as File.exist?, File.file?, and File.directory?. File.size returns the size in bytes, and File.mtime returns the modification time as a Time object. The path helpers File.extname, File.basename, File.dirname, and File.join split and build paths in a platform-independent way.

file_tests.rb
require 'tmpdir'

Dir.mktmpdir('dfo_demo') do |root|
  path = File.join(root, 'notes.txt')
  File.write(path, "Hello, Ruby!\n")

  puts "exist?: #{File.exist?(path)}"
  puts "file?: #{File.file?(path)}"
  puts "directory?: #{File.directory?(root)}"
  puts "size: #{File.size(path)}"
  puts "mtime class: #{File.mtime(path).class}"
  puts "mtime is Time: #{File.mtime(path).is_a?(Time)}"
  puts "extname: #{File.extname(path)}"
  puts "basename: #{File.basename(path)}"
  puts "basename no ext: #{File.basename(path, '.txt')}"
  puts "dirname is root: #{File.dirname(path) == root}"
  puts "join: #{File.join('a', 'b', 'c.rb')}"
end

File.basename accepts an optional suffix argument that strips an extension. Because the temporary root path varies between runs, the script compares File.dirname with root instead of printing it.

$ ./file_tests.rb
exist?: true
file?: true
directory?: true
size: 13
mtime class: Time
mtime is Time: true
extname: .txt
basename: notes.txt
basename no ext: notes
dirname is root: true
join: a/b/c.rb

Rename, Copy, and Delete

File.rename moves or renames a file, and File.delete removes it. FileUtils.cp copies a file, FileUtils.mkdir_p creates a directory together with any missing parents, and FileUtils.rm_rf removes a directory tree recursively without raising an error when the path is absent.

rename_delete.rb
require 'tmpdir'
require 'fileutils'

Dir.mktmpdir('dfo_demo') do |root|
  Dir.chdir(root) do
    File.write('old.txt', "data\n")
    File.rename('old.txt', 'new.txt')
    puts "after rename: #{Dir.children('.').sort.inspect}"

    FileUtils.cp('new.txt', 'copy.txt')
    puts "after cp: #{Dir.children('.').sort.inspect}"
    puts "copy same content: #{File.read('copy.txt') == File.read('new.txt')}"

    File.delete('copy.txt')
    puts "after delete: #{Dir.children('.').sort.inspect}"

    FileUtils.mkdir_p('a/b/c')
    puts "mkdir_p a/b/c dir?: #{File.directory?('a/b/c')}"

    FileUtils.rm_rf('a')
    puts "after rm_rf a exist?: #{File.exist?('a')}"
    puts "remaining: #{Dir.children('.').sort.inspect}"
  end
end

FileUtils methods are more forgiving than the core File methods: mkdir_p succeeds if the directory already exists, and rm_rf succeeds if the path is missing. Use File.delete when you want an error for a missing file.

$ ./rename_delete.rb
after rename: ["new.txt"]
after cp: ["copy.txt", "new.txt"]
copy same content: true
after delete: ["new.txt"]
mkdir_p a/b/c dir?: true
after rm_rf a exist?: false
remaining: ["new.txt"]

Walking a Tree with Find.find

Find.find recursively visits every file and directory beneath a starting path, yielding each path as it goes. It is a convenient alternative to writing your own recursive traversal. The example builds a small project tree, then prints each entry with its type and, for files, its size.

find_tree.rb
require 'tmpdir'
require 'fileutils'
require 'find'

Dir.mktmpdir('dfo_demo') do |root|
  FileUtils.mkdir_p(File.join(root, 'project/lib'))
  FileUtils.mkdir_p(File.join(root, 'project/spec'))
  File.write(File.join(root, 'project/lib/app.rb'), "class App; end\n")
  File.write(File.join(root, 'project/spec/app_spec.rb'), "# spec\n")
  File.write(File.join(root, 'project/README.md'), "# Project\n")

  Find.find(root) do |path|
    rel = path.sub("#{root}/", '')
    next if rel == root
    if File.directory?(path)
      puts "[DIR]  #{rel}"
    else
      puts "[FILE] #{rel} (#{File.size(path)} bytes)"
    end
  end
end

The traversal order is the order in which the file system returns entries, so it may vary between platforms. The script strips the temporary root from each path so the output stays stable.

$ ./find_tree.rb
[DIR]  project
[FILE] project/README.md (10 bytes)
[DIR]  project/lib
[FILE] project/lib/app.rb (15 bytes)
[DIR]  project/spec
[FILE] project/spec/app_spec.rb (7 bytes)

Recursive Totals with Glob and File.size

Combining Dir.glob with File.size and File.extname is a common pattern for reporting on a set of files. The following script creates a small site directory, lists its files with their sizes, and prints a total.

recursive_size.rb
require 'tmpdir'
require 'fileutils'

Dir.mktmpdir('dfo_demo') do |root|
  Dir.chdir(root) do
    FileUtils.mkdir_p('site/css')
    FileUtils.mkdir_p('site/js')
    File.write('site/index.html', 'x' * 120)
    File.write('site/css/style.css', 'y' * 300)
    File.write('site/js/app.js', 'z' * 75)

    files = Dir.glob('site/**/*').select { |f| File.file?(f) }.sort
    files.each do |f|
      puts "#{f.ljust(20)} #{File.size(f).to_s.rjust(4)} #{File.extname(f)}"
    end

    total = files.sum { |f| File.size(f) }
    puts "total: #{total} bytes in #{files.size} files"
  end
end

The select call filters out the directories that the **/* pattern also matches. Sorting the list makes the output deterministic.

$ ./recursive_size.rb
site/css/style.css    300 .css
site/index.html       120 .html
site/js/app.js         75 .js
total: 495 bytes in 3 files

Glob Pattern Matching

Dir.glob supports several wildcards: * matches within a single path segment, **/ matches across directories, and braces {a,b} match alternatives. Combining them lets you select exactly the files you need.

glob_patterns.rb
require 'tmpdir'
require 'fileutils'

Dir.mktmpdir('dfo_demo') do |root|
  Dir.chdir(root) do
    FileUtils.mkdir_p(%w[app/models lib test])
    %w[app/main.rb app/models/user.rb lib/helper.rb test/user_test.rb notes.txt].each do |f|
      File.write(f, "# #{f}\n")
    end

    puts "**/*.rb: #{Dir.glob('**/*.rb').sort.inspect}"
    puts "app/*.rb: #{Dir.glob('app/*.rb').sort.inspect}"
    puts "**/*_test.rb: #{Dir.glob('**/*_test.rb').inspect}"
    puts "{lib,test}/*: #{Dir.glob('{lib,test}/*').sort.inspect}"
    puts "**/*.{rb,txt} count: #{Dir.glob('**/*.{rb,txt}').size}"
  end
end

Note that * does not descend into subdirectories, so app/*.rb returns only app/main.rb, while **/*.rb finds app/models/user.rb too.

$ ./glob_patterns.rb
**/*.rb: ["app/main.rb", "app/models/user.rb", "lib/helper.rb", "test/user_test.rb"]
app/*.rb: ["app/main.rb"]
**/*_test.rb: ["test/user_test.rb"]
{lib,test}/*: ["lib/helper.rb", "test/user_test.rb"]
**/*.{rb,txt} count: 5

Source References

Learn more from these resources: Ruby Dir Documentation, Ruby File Documentation, FileUtils Documentation, and Find Documentation.

List all Ruby tutorials.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.