Pathname #
Before Pathname, Ruby provided File and Dir for filesystem operations — both working with strings as paths. This approach works, but it makes code a series of method calls that can’t be chained and requires lots of manual string manipulation. Pathname changes this perspective: a path isn’t just a string, but an object with methods, chainable, and naturally composable. You no longer write File.join(File.dirname(path), "subdir") — you just write Pathname.new(path).dirname / "subdir". This article covers the whole Pathname API, how it works with FileUtils, and when it’s more appropriate than File and Dir.
Creating Pathnames #
Pathname is available after require "pathname". The most common way to create a Pathname is from a string, but there are several variations.
require "pathname"
# From an absolute path string
home = Pathname.new("/home/user")
config = Pathname.new("/etc/nginx/nginx.conf")
# From a relative path string
relative = Pathname.new("lib/utils")
current = Pathname.new(".")
# Shorthand with the Pathname() kernel method
home = Pathname("/home/user") # same as Pathname.new
# From __FILE__ — the path of the currently executing Ruby file
this_file = Pathname(__FILE__)
this_dir = Pathname(__dir__) # the current file's directory
# Pathname is immutable — every operation produces a new Pathname
original = Pathname.new("/home/user")
modified = original / "documents" # doesn't modify original
# => Pathname("/home/user/documents")
# Converting back and forth with String
path_string = home.to_s # => "/home/user"
path_string = home.to_path # => "/home/user" (for IO compatibility)
Path Navigation and Composition #
The most frequent operations on paths are combining components, extracting parts, and moving between directory levels.
require "pathname"
base = Pathname.new("/var/www/app")
# Combining paths with the / operator
config = base / "config" / "database.yml"
# => Pathname("/var/www/app/config/database.yml")
logs = base / "log" / "production.log"
# => Pathname("/var/www/app/log/production.log")
# join — an alternative to /
base.join("public", "assets", "app.css")
# => Pathname("/var/www/app/public/assets/app.css")
# Extracting parts of a path
config = Pathname.new("/var/www/app/config/database.yml")
config.dirname # => Pathname("/var/www/app/config")
config.basename # => Pathname("database.yml")
config.extname # => ".yml"
config.basename(".yml") # => Pathname("database") (without the extension)
# split — returns [dirname, basename]
config.split
# => [Pathname("/var/www/app/config"), Pathname("database.yml")]
# Navigating to parents
config.parent # => Pathname("/var/www/app/config")
config.parent.parent # => Pathname("/var/www/app")
# Check whether the path is the root
Pathname.new("/").root? # => true
Pathname.new("/home").root? # => false
# Relative paths and expansion
relative = Pathname.new("../config/settings.yml")
absolute = relative.expand_path # resolved against the current working directory
# expand_path with a base directory
relative.expand_path("/var/www/app")
# => Pathname("/var/www/config/settings.yml")
# Absolute <-> relative conversion
absolute = Pathname.new("/var/www/app/public")
base = Pathname.new("/var/www/app")
# relative_path_from — compute the relative path from one path to another
absolute.relative_path_from(base)
# => Pathname("public")
Pathname.new("/var/www/app/config").relative_path_from(base)
# => Pathname("config")
Pathname.new("/var/log").relative_path_from(base)
# => Pathname("../../log")
# cleanpath — clean the path of . and .. without resolving symlinks
Pathname.new("/var/www/../www/app/./config").cleanpath
# => Pathname("/var/www/app/config")
# realpath — resolve symlinks and return an absolute path (file/dir must exist)
# Pathname.new("/etc/nginx").realpath
# => Pathname("/usr/local/etc/nginx") (if /etc/nginx is a symlink)
flowchart TD
A["/var/www/app/config/db.yml"] --> B[dirname\n/var/www/app/config]
A --> C[basename\ndb.yml]
A --> D[extname\n.yml]
A --> E[parent\n/var/www/app/config]
B --> F[parent\n/var/www/app]
F --> G["/ 'log'\n/var/www/app/log"]
G --> H["join 'app.log'\n/var/www/app/log/app.log"]File Attribute Checks #
Pathname provides complete check methods for learning a path’s properties — whether the file exists, whether it’s a directory, its size, and so on.
require "pathname"
path = Pathname.new("/etc/hosts")
# Existence
path.exist? # => true (if the file exists)
path.file? # => true (if a regular file, not a dir or symlink)
path.directory? # => false (not a directory)
path.symlink? # => false (not a symlink)
path.zero? # => false (not an empty file)
path.empty? # => false (alias of zero? for files, checks contents for dirs)
# File information
path.size # => size in bytes
path.size? # => size if > 0, nil if empty (useful for conditionals)
# Path type
path.absolute? # => true (absolute path)
path.relative? # => false
Pathname.new("config/db.yml").absolute? # => false
Pathname.new("config/db.yml").relative? # => true
# Permissions
path.readable? # => true/false
path.writable? # => true/false
path.executable? # => true/false
# Stat — full information
stat = path.stat
stat.size # => size
stat.mtime # => last modification time
stat.ctime # => last status change time
stat.mode # => permission mode
# Check-before-operate pattern
def read_config(path_string)
config = Pathname.new(path_string)
unless config.exist?
raise "Config file not found: #{path_string}"
end
unless config.file?
raise "#{path_string} is not a regular file"
end
unless config.readable?
raise "No read permission: #{path_string}"
end
config.read
end
# Directory checks
def ensure_directory_exists(path)
dir = Pathname.new(path)
dir.mkpath unless dir.directory? # create the directory + parents if missing
dir
end
Reading and Writing Files #
Pathname delegates read/write operations to IO and File, but with a cleaner interface because the path is already bound to the object.
require "pathname"
file = Pathname.new("/tmp/example.txt")
# Writing files
file.write("Hello, Pathname!\n")
file.open("w") { |f| f.write("New content") }
# Appending to a file
file.open("a") { |f| f.puts "Additional line" }
# Reading files
content = file.read # the entire content as a string
lines = file.readlines # an array of strings, one element per line
file.each_line { |l| puts l } # iterate per line
# Reading binary
binary_data = file.binread
# Writing binary
file.binwrite(binary_data)
# Atomic operations — write to a tempfile then rename (crash-safe)
def atomic_write(path, content)
target = Pathname.new(path)
tmp = Pathname.new("#{path}.tmp.#{Process.pid}")
tmp.write(content)
tmp.rename(target)
end
Directory Iteration #
One of Pathname’s strongest features is directory iteration integrated with Enumerable — you can directly use map, select, sort, and all Enumerable methods.
require "pathname"
dir = Pathname.new("/var/www/app")
# Iterate directory contents (one level)
dir.each_child { |path| puts path }
dir.children # => Array of Pathname (without . and ..)
# glob — pattern matching like Dir.glob
dir.glob("**/*.rb") # all .rb files recursively
dir.glob("*.{yml,yaml}") # YAML files at one level
dir.glob("config/*.rb") # .rb files in the config subdirectory
# Example: collect all Ruby files in a project
Pathname.new(".")
.glob("**/*.rb")
.reject { |f| f.to_s.include?("vendor/") }
.sort
.each { |f| puts f }
# find — recursive with full control
require "find"
dir.find do |path|
# path is a Pathname at every level
Find.prune if path.basename.to_s.start_with?(".") # skip hidden files
puts path if path.file? && path.extname == ".log"
end
# Separating files and directories
dir.children.select(&:file?) # files only
dir.children.select(&:directory?) # directories only
dir.children.reject(&:symlink?) # except symlinks
# Real example: Ruby project structure analyzer
def analyze_project(root)
root = Pathname.new(root)
rb_files = root.glob("**/*.rb")
.reject { |f| f.to_s =~ /vendor|node_modules|\.git/ }
{
total_files: rb_files.count,
total_lines: rb_files.sum { |f| f.readlines.count },
largest_file: rb_files.max_by { |f| f.size }&.to_s,
directories: rb_files.map { |f| f.dirname }.uniq.count,
average_size: rb_files.sum(&:size) / [rb_files.count, 1].max
}
end
# Example output:
# {
# total_files: 47,
# total_lines: 2841,
# largest_file: "lib/core/processor.rb",
# directories: 12,
# average_size: 3247
# }
Filesystem Operations #
Pathname provides methods for common filesystem operations, all using the path from the object itself.
require "pathname"
source = Pathname.new("/tmp/source.txt")
destination = Pathname.new("/tmp/destination.txt")
# Creating files and directories
source.write("file contents") # create a file with content
Pathname.new("/tmp/dir/sub").mkpath # create the directory + all parents
# Copy and move — needs FileUtils
require "fileutils"
FileUtils.cp(source, destination) # copy a file
FileUtils.mv(source, destination) # move a file
# Rename — same as mv but within one filesystem
source.rename(destination) # built-in Pathname
# Delete
source.delete # delete a file
source.unlink # alias of delete
# For directories
dir = Pathname.new("/tmp/empty_directory")
dir.rmdir # delete an empty directory
# Creating symlinks
target = Pathname.new("/tmp/link")
target.make_symlink(source) # target points to source
# Following symlinks
link = Pathname.new("/etc/nginx")
link.readlink # => the path the symlink points to (without resolving)
link.realpath # => the absolute path after all symlinks are resolved
# Changing permissions
Pathname.new("/tmp/script.sh").chmod(0755)
Pathname vs File vs Dir #
Understanding when to use each makes code cleaner and more idiomatic.
| Operation | File/Dir (old) | Pathname (modern) |
|---|---|---|
| Join paths | File.join(a, b, c) | Pathname(a) / b / c |
| File name | File.basename(path) | path.basename |
| Parent directory | File.dirname(path) | path.dirname |
| Extension | File.extname(path) | path.extname |
| Exists check | File.exist?(path) | path.exist? |
| Directory check | File.directory?(path) | path.directory? |
| Read file | File.read(path) | path.read |
| Write file | File.write(path, content) | path.write(content) |
| Recursive glob | Dir.glob("**/*.rb") | path.glob("**/*.rb") |
| Directory iteration | Dir.each_child(path) | path.each_child |
# Direct comparison: get all config files under a project directory
# ANTI-PATTERN: old style with File and Dir — not chainable
config_dir = File.join(File.dirname(__FILE__), "..", "config")
config_dir = File.expand_path(config_dir)
files = Dir.glob(File.join(config_dir, "**", "*.yml"))
files = files.select { |f| File.file?(f) }
files = files.sort
# CORRECT: Pathname — expressive, chainable, OOP
config_dir = Pathname(__FILE__).dirname.parent / "config"
files = config_dir
.glob("**/*.yml")
.select(&:file?)
.sort
# End-to-end example: timestamped config backup
def backup_config(config_path)
source = Pathname.new(config_path)
backup_dir = source.dirname / "backups"
backup_dir.mkpath
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
backup_name = "#{source.basename(".#{source.extname.delete(".")}")}_#{timestamp}#{source.extname}"
destination = backup_dir / backup_name
source.open("r") do |input|
destination.open("w") do |output|
output.write(input.read)
end
end
puts "Backup saved to: #{destination}"
destination
end
Usage Patterns in Ruby Projects #
Several patterns frequently found in real Ruby projects.
Paths Relative to the Source File #
# A common pattern in gems and Ruby applications
# Get paths relative to the location of the current .rb file
LIB_DIR = Pathname(__FILE__).dirname
ROOT_DIR = LIB_DIR.parent
CONFIG_DIR = ROOT_DIR / "config"
DATA_DIR = ROOT_DIR / "data"
# Can be used directly
config = CONFIG_DIR / "settings.yml"
require LIB_DIR / "utils" / "helper"
Path-Based Configuration #
class Application
attr_reader :root, :config_dir, :log_dir, :tmp_dir
def initialize(root_path)
@root = Pathname.new(root_path).expand_path
@config_dir = @root / "config"
@log_dir = @root / "log"
@tmp_dir = @root / "tmp"
end
def setup_directories
[@log_dir, @tmp_dir].each(&:mkpath)
end
def config_for(name)
file = @config_dir / "#{name}.yml"
raise "Config not found: #{file}" unless file.exist?
YAML.load_file(file.to_s)
end
def log_file(name)
@log_dir / "#{name}.log"
end
end
app = Application.new("/var/www/myapp")
app.setup_directories
db_config = app.config_for("database")
Recursive File Search #
# Finding files with complex criteria
def find_files(root, **options)
dir = Pathname.new(root)
extension = options[:extension]
max_size = options[:max_size]
modified_after = options[:after]
dir.glob("**/*")
.select(&:file?)
.then { |files| extension ? files.select { |f| f.extname == extension } : files }
.then { |files| max_size ? files.select { |f| f.size <= max_size } : files }
.then { |files| modified_after ? files.select { |f| f.stat.mtime > modified_after } : files }
.sort_by(&:mtime)
end
results = find_files("/var/log",
extension: ".log",
max_size: 10 * 1024 * 1024, # max 10 MB
after: Time.now - 86400 # modified within the last 24 hours
)
Summary #
- Pathname turns paths into objects — not just strings processed with the static
FileandDirmethods, but objects with chainable, composable instance methods.- The
/operator for path composition —Pathname("/var/www") / "app" / "config"is far cleaner thanFile.joinwith many arguments.globintegrates with Enumerable —path.glob("**/*.rb").select(&:file?).sortis a natural, readable pipeline.expand_path,cleanpath,realpath— three different methods for path normalization:cleanpathis pure string manipulation,expand_pathresolves against the working directory,realpathresolves symlinks with filesystem access.mkpath— the shortest way to create a directory with all its parents; better thanFileUtils.mkdir_p.__FILE__and__dir__— usePathname(__FILE__).dirnameto get the current file’s directory; this pattern is very common in Ruby libraries and gems.- Convert to strings before legacy APIs — some methods like
YAML.load_fileorrequireaccept strings, not Pathnames; call.to_swhen needed.require "pathname"first — Pathname isn’t automatically available; it must be required before use.