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.
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.
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.
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.
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.
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.
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.