Overview
Pathname provides an object-oriented interface for working with file system paths in Ruby. Unlike string-based path manipulation, Pathname wraps file system paths in a dedicated class that provides methods for path construction, traversal, and file system operations.
The Pathname class acts as a wrapper around file system paths, converting them from strings into objects that respond to path-specific methods. Ruby's implementation treats paths as immutable objects - operations return new Pathname instances rather than modifying existing ones.
require 'pathname'
path = Pathname.new('/usr/local/bin')
# => #<Pathname:/usr/local/bin>
path.directory?
# => true (if directory exists)
parent = path.parent
# => #<Pathname:/usr/local>
Pathname bridges the gap between string paths and File/Dir operations by providing a consistent interface that works across different operating systems. The class handles path separators automatically and provides methods for both path manipulation and file system queries.
# Cross-platform path construction
config_path = Pathname.new('config') / 'database.yml'
# => #<Pathname:config/database.yml> on Unix
# => #<Pathname:config\database.yml> on Windows
# File system queries
config_path.exist?
config_path.readable?
config_path.size
The class includes Enumerable, allowing iteration over path components, and provides conversion methods to work with other Ruby file system classes.
Basic Usage
Creating Pathname objects accepts string paths or other Pathname instances. The constructor normalizes paths but does not validate file system existence.
require 'pathname'
# String path
home = Pathname.new('/home/user')
# Relative path
current = Pathname.new('.')
# From another Pathname
backup = Pathname.new(home)
Path construction uses the /
operator to join path components, automatically handling separators for the current platform.
base = Pathname.new('/var')
logs = base / 'log' / 'application.log'
# => #<Pathname:/var/log/application.log>
# Multiple components at once
config = base / 'app' / 'config' / 'settings.yml'
Common path operations include accessing parent directories, extracting filenames, and manipulating extensions.
path = Pathname.new('/home/user/documents/report.pdf')
path.dirname # => #<Pathname:/home/user/documents>
path.basename # => #<Pathname:report.pdf>
path.extname # => ".pdf"
# Basename without extension
path.basename('.pdf') # => #<Pathname:report>
# Parent traversal
path.parent # => #<Pathname:/home/user/documents>
path.parent.parent # => #<Pathname:/home/user>
File system queries test path properties without requiring explicit File or Dir method calls.
path = Pathname.new('config.yml')
path.exist? # File or directory exists
path.file? # Is a regular file
path.directory? # Is a directory
path.readable? # Has read permissions
path.writable? # Has write permissions
path.executable? # Has execute permissions
Converting between Pathname and strings happens through explicit method calls rather than automatic coercion.
path = Pathname.new('/tmp/data.txt')
# Convert to string
path.to_s # => "/tmp/data.txt"
path.to_path # => "/tmp/data.txt" (alias)
# Use with File operations
File.read(path)
File.open(path, 'w') { |f| f.write('data') }
Advanced Usage
Pathname supports complex path manipulations through method chaining and relative path resolution. The +
operator joins paths without separator normalization, while /
provides intelligent joining.
base = Pathname.new('/app')
relative = Pathname.new('config/../logs/app.log')
# Resolve relative components
resolved = base / relative
puts resolved.cleanpath # => /app/logs/app.log
# Expand to absolute path
expanded = relative.expand_path(base)
# => /app/logs/app.log
Path enumeration treats path components as enumerable elements, supporting standard iteration patterns.
path = Pathname.new('/home/user/projects/myapp')
# Iterate over components
path.each_filename { |component| puts component }
# home
# user
# projects
# myapp
# Collect components
components = path.each_filename.to_a
# => ["home", "user", "projects", "myapp"]
# Path ascend iteration
path.ascend { |p| puts p }
# /home/user/projects/myapp
# /home/user/projects
# /home/user
# /home
# /
Directory traversal provides recursive file system exploration with pattern matching and filtering capabilities.
project_root = Pathname.new('/app')
# Find all Ruby files recursively
ruby_files = []
project_root.find do |path|
if path.extname == '.rb'
ruby_files << path
end
end
# Skip directories during traversal
project_root.find do |path|
if path.basename.to_s == '.git'
Find.prune # Skip this directory tree
elsif path.file? && path.extname == '.log'
puts "Log file: #{path}"
end
end
Pathname integrates with glob patterns for complex file matching across directory hierarchies.
base = Pathname.new('/var/log')
# Glob with Pathname
log_files = Pathname.glob(base / '**' / '*.log')
# Multiple patterns
config_files = Pathname.glob([
base / 'app' / '*.conf',
base / 'nginx' / '*.conf'
])
# Block form for processing
Pathname.glob('/tmp/**/*.tmp') do |tmp_file|
tmp_file.delete if tmp_file.mtime < Time.now - 3600
end
Relative path calculation determines relationships between paths, supporting navigation between different locations.
from = Pathname.new('/app/public/assets')
to = Pathname.new('/app/config/database.yml')
relative_path = to.relative_path_from(from)
# => #<Pathname:../../config/database.yml>
# Verify relationship
absolute_from_relative = from / relative_path
puts absolute_from_relative.cleanpath == to # => true
Error Handling & Debugging
Pathname operations can raise various exceptions depending on file system state and permissions. Common exceptions include Errno::ENOENT for missing files and Errno::EACCES for permission issues.
path = Pathname.new('/nonexistent/file.txt')
begin
content = path.read
rescue Errno::ENOENT => e
puts "File not found: #{path}"
puts "Error: #{e.message}"
rescue Errno::EACCES => e
puts "Permission denied: #{path}"
puts "Error: #{e.message}"
rescue => e
puts "Unexpected error: #{e.class} - #{e.message}"
end
Path validation prevents common errors by checking existence and permissions before operations.
def safe_read_file(path_string)
path = Pathname.new(path_string)
unless path.exist?
raise ArgumentError, "Path does not exist: #{path}"
end
unless path.file?
raise ArgumentError, "Path is not a file: #{path}"
end
unless path.readable?
raise ArgumentError, "File is not readable: #{path}"
end
path.read
end
# Usage with error handling
begin
content = safe_read_file('/etc/passwd')
process_content(content)
rescue ArgumentError => e
puts "Validation error: #{e.message}"
end
Cross-platform path issues often arise from hardcoded separators or case sensitivity assumptions. Debugging these requires understanding path normalization behavior.
# Problematic hardcoded separator
bad_path = Pathname.new('/home/user\documents') # Mixed separators
# Debug path components
puts "Original: #{bad_path}"
puts "Cleanpath: #{bad_path.cleanpath}"
puts "Components: #{bad_path.each_filename.to_a}"
# Better cross-platform approach
good_path = Pathname.new('home') / 'user' / 'documents'
puts "Cross-platform: #{good_path}"
Directory traversal errors commonly occur when paths become invalid during iteration or when permissions change.
root_path = Pathname.new('/app/uploads')
begin
root_path.find do |path|
begin
# Operations that might fail per file
if path.file?
stats = path.stat
puts "#{path}: #{stats.size} bytes"
end
rescue Errno::EACCES
puts "Skipping inaccessible: #{path}"
next # Continue with next file
rescue Errno::ELOOP
puts "Skipping symlink loop: #{path}"
Find.prune # Skip this branch
end
end
rescue Errno::ENOENT
puts "Root directory disappeared: #{root_path}"
rescue => e
puts "Traversal failed: #{e.class} - #{e.message}"
end
Production Patterns
Rails applications commonly use Pathname for managing application paths and asset locations. Pathname provides consistent path handling across different deployment environments.
class ApplicationPaths
def self.root
@root ||= Pathname.new(Rails.root)
end
def self.config
root / 'config'
end
def self.uploads
root / 'public' / 'uploads'
end
def self.temp
root / 'tmp'
end
def self.log_file(name)
root / 'log' / "#{name}.log"
end
end
# Usage in production
config_path = ApplicationPaths.config / 'database.yml'
if config_path.exist?
database_config = YAML.load_file(config_path)
end
File upload handling benefits from Pathname's path manipulation and validation capabilities.
class FileUploadHandler
UPLOAD_ROOT = Pathname.new('/var/uploads')
ALLOWED_EXTENSIONS = ['.jpg', '.png', '.pdf', '.doc'].freeze
def store_upload(uploaded_file, user_id)
# Create user-specific directory
user_dir = UPLOAD_ROOT / user_id.to_s
user_dir.mkdir unless user_dir.exist?
# Generate safe filename
original_name = Pathname.new(uploaded_file.original_filename)
extension = original_name.extname.downcase
unless ALLOWED_EXTENSIONS.include?(extension)
raise ArgumentError, "File type not allowed: #{extension}"
end
# Create unique filename
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
safe_basename = original_name.basename(extension).to_s.gsub(/[^a-zA-Z0-9_-]/, '_')
final_path = user_dir / "#{timestamp}_#{safe_basename}#{extension}"
# Ensure no conflicts
counter = 1
while final_path.exist?
final_path = user_dir / "#{timestamp}_#{safe_basename}_#{counter}#{extension}"
counter += 1
end
# Store file
File.open(final_path, 'wb') do |f|
f.write(uploaded_file.read)
end
final_path
end
end
Log rotation and cleanup tasks use Pathname for file management operations.
class LogRotator
def self.rotate_logs(log_dir_path, max_age_days: 30, max_size_mb: 100)
log_dir = Pathname.new(log_dir_path)
cutoff_time = Time.now - (max_age_days * 24 * 3600)
max_size_bytes = max_size_mb * 1024 * 1024
log_dir.children.select(&:file?).each do |log_file|
next unless log_file.extname == '.log'
# Remove old files
if log_file.mtime < cutoff_time
log_file.delete
puts "Deleted old log: #{log_file}"
next
end
# Compress large files
if log_file.size > max_size_bytes
compressed_name = log_file.sub_ext('.log.gz')
system("gzip", "-c", log_file.to_s) do |gzip_out|
File.write(compressed_name, gzip_out)
end
log_file.delete
puts "Compressed large log: #{log_file} -> #{compressed_name}"
end
end
end
end
Configuration management systems use Pathname for environment-specific file loading.
class ConfigLoader
CONFIG_ROOT = Pathname.new('/etc/myapp')
def self.load_environment_config(env = 'production')
env_config = CONFIG_ROOT / "#{env}.yml"
default_config = CONFIG_ROOT / 'default.yml'
config = {}
# Load default configuration
if default_config.exist?
config = YAML.load_file(default_config) || {}
end
# Override with environment-specific config
if env_config.exist?
env_settings = YAML.load_file(env_config) || {}
config = deep_merge(config, env_settings)
end
# Load local overrides
local_config = CONFIG_ROOT / 'local.yml'
if local_config.exist?
local_settings = YAML.load_file(local_config) || {}
config = deep_merge(config, local_settings)
end
config
end
private
def self.deep_merge(hash1, hash2)
hash1.merge(hash2) do |key, oldval, newval|
oldval.is_a?(Hash) && newval.is_a?(Hash) ? deep_merge(oldval, newval) : newval
end
end
end
Common Pitfalls
Pathname objects are not strings, causing type errors when code expects string operations. Explicit conversion prevents these issues.
# WRONG - Pathname doesn't support string methods directly
path = Pathname.new('/app/config.yml')
path.include?('config') # NoMethodError
# CORRECT - Convert to string first
path.to_s.include?('config') # => true
# WRONG - String interpolation without conversion
puts "Loading #{path}" # Works but inconsistent
puts "Path: " + path # TypeError
# CORRECT - Explicit string conversion
puts "Path: #{path.to_s}"
puts "Path: " + path.to_s
Path separator assumptions break cross-platform compatibility. Ruby handles separators automatically, but hardcoded separators cause problems.
# WRONG - Hardcoded Unix separators
config_path = Pathname.new('/etc') + '/myapp/config.yml'
# WRONG - Manual string concatenation
data_dir = Pathname.new('/var') + '/data'
# CORRECT - Use path joining
config_path = Pathname.new('/etc') / 'myapp' / 'config.yml'
data_dir = Pathname.new('/var') / 'data'
# CORRECT - Cross-platform base paths
if RUBY_PLATFORM =~ /win32/
base = Pathname.new('C:/')
else
base = Pathname.new('/')
end
Relative path confusion occurs when current directory changes during execution. Always work with absolute paths or establish a consistent working directory.
# PROBLEMATIC - Relative paths affected by directory changes
config = Pathname.new('config.yml')
Dir.chdir('/tmp')
config.read # Looks in /tmp/config.yml, not original location
# BETTER - Establish absolute path early
original_dir = Pathname.pwd
config = original_dir / 'config.yml'
Dir.chdir('/tmp')
config.read # Still reads from original location
# BEST - Use expand_path for absolute resolution
config = Pathname.new('config.yml').expand_path
Dir.chdir('/tmp')
config.read # Absolute path unaffected by directory changes
File system race conditions occur when testing existence separately from operations. File status can change between checks and usage.
# WRONG - Race condition between check and operation
if path.exist?
content = path.read # File might be deleted between exist? and read
end
# BETTER - Handle exceptions instead of pre-checking
begin
content = path.read
rescue Errno::ENOENT
puts "File not found: #{path}"
end
# WRONG - Multiple separate checks
if path.exist? && path.readable? && path.file?
# File status might change during these checks
data = path.read
end
# BETTER - Single operation with error handling
begin
data = path.read
# File existence, readability, and file type all verified by successful read
rescue Errno::ENOENT
puts "File does not exist"
rescue Errno::EACCES
puts "Permission denied"
rescue Errno::EISDIR
puts "Path is a directory, not a file"
end
Memory issues arise when processing large directory trees without controlling iteration depth or filtering.
# PROBLEMATIC - Loads entire large directory into memory
all_files = Pathname.glob('/var/**/*') # Millions of files
# BETTER - Process files in batches
Pathname.glob('/var/**/*') do |file_path|
process_file(file_path)
# File processed immediately, not stored in memory
end
# BETTER - Limit traversal depth
large_dir = Pathname.new('/var')
large_dir.find do |path|
if path.directory? && path.each_filename.count > 3
Find.prune # Skip deep nested directories
elsif path.file?
process_file(path)
end
end
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Pathname.new(path) |
path (String/Pathname) |
Pathname |
Creates new Pathname object |
Pathname.pwd |
None | Pathname |
Returns current working directory |
Pathname.glob(pattern) |
pattern (String/Array) |
Array<Pathname> |
Returns paths matching glob pattern |
#to_s |
None | String |
Converts to string representation |
#to_path |
None | String |
Alias for to_s, used by File methods |
Path Construction
Method | Parameters | Returns | Description |
---|---|---|---|
#/(component) |
component (String/Pathname) |
Pathname |
Joins paths with separator |
#+(path) |
path (String/Pathname) |
Pathname |
Concatenates paths without separator |
#join(*components) |
*components (String/Pathname) |
Pathname |
Joins multiple path components |
#expand_path(dir = nil) |
dir (String/Pathname/nil) |
Pathname |
Returns absolute path |
#cleanpath |
None | Pathname |
Removes redundant components |
Path Components
Method | Parameters | Returns | Description |
---|---|---|---|
#basename(suffix = nil) |
suffix (String/nil) |
Pathname |
Returns final path component |
#dirname |
None | Pathname |
Returns directory portion |
#extname |
None | String |
Returns file extension |
#parent |
None | Pathname |
Returns parent directory |
#root? |
None | Boolean |
Tests if path is root directory |
#absolute? |
None | Boolean |
Tests if path is absolute |
#relative? |
None | Boolean |
Tests if path is relative |
File System Queries
Method | Parameters | Returns | Description |
---|---|---|---|
#exist? |
None | Boolean |
Tests if path exists |
#file? |
None | Boolean |
Tests if path is regular file |
#directory? |
None | Boolean |
Tests if path is directory |
#executable? |
None | Boolean |
Tests if path is executable |
#readable? |
None | Boolean |
Tests if path is readable |
#writable? |
None | Boolean |
Tests if path is writable |
#symlink? |
None | Boolean |
Tests if path is symbolic link |
#socket? |
None | Boolean |
Tests if path is socket |
#blockdev? |
None | Boolean |
Tests if path is block device |
#chardev? |
None | Boolean |
Tests if path is character device |
File Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#read(*args) |
*args (various) |
String |
Reads entire file content |
#binread(*args) |
*args (various) |
String |
Reads file in binary mode |
#readlines(*args) |
*args (various) |
Array<String> |
Reads file as array of lines |
#write(string, **opts) |
string (String), options |
Integer |
Writes string to file |
#binwrite(string, **opts) |
string (String), options |
Integer |
Writes binary string to file |
#size |
None | Integer |
Returns file size in bytes |
#stat |
None | File::Stat |
Returns file statistics |
#lstat |
None | File::Stat |
Returns link statistics |
Directory Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#children(with_dir = true) |
with_dir (Boolean) |
Array<Pathname> |
Returns directory contents |
#entries |
None | Array<Pathname> |
Returns all directory entries |
#each_child(&block) |
block (Proc) |
Enumerator/nil |
Iterates over children |
#mkdir(mode = 0777) |
mode (Integer) |
Integer |
Creates directory |
#rmdir |
None | Integer |
Removes empty directory |
#find(&block) |
block (Proc) |
Enumerator/nil |
Recursively traverses directory tree |
Path Traversal
Method | Parameters | Returns | Description |
---|---|---|---|
#each_filename(&block) |
block (Proc) |
Enumerator/nil |
Iterates over path components |
#ascend(&block) |
block (Proc) |
Enumerator/nil |
Iterates from path to root |
#descend(&block) |
block (Proc) |
Enumerator/nil |
Iterates from root to path |
#relative_path_from(base) |
base (Pathname) |
Pathname |
Returns relative path from base |
File System Modification
Method | Parameters | Returns | Description |
---|---|---|---|
#delete |
None | Integer |
Deletes file |
#unlink |
None | Integer |
Alias for delete |
#rename(new_name) |
new_name (Pathname/String) |
Integer |
Renames file/directory |
#chmod(mode) |
mode (Integer) |
Integer |
Changes file permissions |
#chown(uid, gid) |
uid (Integer), gid (Integer) |
Integer |
Changes file ownership |
#utime(atime, mtime) |
atime (Time), mtime (Time) |
Integer |
Updates access/modification times |
Common Constants
Constant | Value | Description |
---|---|---|
File::SEPARATOR |
"/" (Unix), "\\" (Windows) |
Platform path separator |
File::ALT_SEPARATOR |
nil (Unix), "/" (Windows) |
Alternative separator |
File::PATH_SEPARATOR |
":" (Unix), ";" (Windows) |
PATH environment separator |
Exception Hierarchy
Common exceptions raised by Pathname operations:
Errno::ENOENT
- File or directory does not existErrno::EACCES
- Permission deniedErrno::ENOTDIR
- Component is not a directoryErrno::EISDIR
- Path is a directory when file expectedErrno::ELOOP
- Too many symbolic linksArgumentError
- Invalid path argumentTypeError
- Wrong argument type