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.

OperationFile/Dir (old)Pathname (modern)
Join pathsFile.join(a, b, c)Pathname(a) / b / c
File nameFile.basename(path)path.basename
Parent directoryFile.dirname(path)path.dirname
ExtensionFile.extname(path)path.extname
Exists checkFile.exist?(path)path.exist?
Directory checkFile.directory?(path)path.directory?
Read fileFile.read(path)path.read
Write fileFile.write(path, content)path.write(content)
Recursive globDir.glob("**/*.rb")path.glob("**/*.rb")
Directory iterationDir.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")
# 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 File and Dir methods, but objects with chainable, composable instance methods.
  • The / operator for path compositionPathname("/var/www") / "app" / "config" is far cleaner than File.join with many arguments.
  • glob integrates with Enumerablepath.glob("**/*.rb").select(&:file?).sort is a natural, readable pipeline.
  • expand_path, cleanpath, realpath — three different methods for path normalization: cleanpath is pure string manipulation, expand_path resolves against the working directory, realpath resolves symlinks with filesystem access.
  • mkpath — the shortest way to create a directory with all its parents; better than FileUtils.mkdir_p.
  • __FILE__ and __dir__ — use Pathname(__FILE__).dirname to 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_file or require accept strings, not Pathnames; call .to_s when needed.
  • require "pathname" first — Pathname isn’t automatically available; it must be required before use.

← Previous: Comparable   Next: FileUtils →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact