Overview
Pathname provides an object-oriented approach to file path manipulation in Ruby. Rather than working with string-based paths directly, Pathname wraps path strings and offers methods for common operations like joining, normalization, and file system queries.
The Pathname class sits between raw string paths and Ruby's File and Dir classes. It handles platform-specific path separators automatically and provides a chainable interface for building complex path operations. When you create a Pathname object, it stores the path internally and delegates actual file system operations to File and Dir when needed.
Ruby's Pathname class addresses several problems with string-based path manipulation. String concatenation for paths can introduce errors with trailing slashes and separator inconsistencies across platforms. Pathname handles these details internally and provides methods that return new Pathname objects, allowing for method chaining.
require 'pathname'
# Creating pathname objects
path = Pathname.new('/usr/local/bin')
relative_path = Pathname.new('config/database.yml')
# Path information
path.absolute? # => true
path.directory? # => true (if directory exists)
path.basename # => #<Pathname:bin>
The class integrates with Ruby's File and Dir classes by forwarding method calls when appropriate. Methods like read
, write
, and size
delegate to File, while entries
and mkdir
delegate to Dir. This design maintains the object-oriented interface while leveraging existing file system functionality.
Pathname objects are immutable. Operations that modify paths return new Pathname instances rather than changing the existing object. This approach prevents accidental path corruption and makes the API safer for concurrent usage.
base = Pathname.new('/app')
config = base.join('config', 'settings.yml')
# base remains unchanged, config is new Pathname object
# Method chaining
log_file = Pathname.new('/var/log')
.join('application.log')
.expand_path
# => #<Pathname:/var/log/application.log>
Basic Usage
Creating Pathname objects requires passing a string path to the constructor. The string can represent absolute or relative paths, and Pathname preserves the original format until explicitly converted.
# Different path types
absolute = Pathname.new('/home/user/documents')
relative = Pathname.new('lib/utilities.rb')
current_dir = Pathname.new('.')
parent_dir = Pathname.new('..')
# Path properties
absolute.absolute? # => true
relative.relative? # => true
current_dir.directory? # => true (if current directory exists)
Path joining represents the most common Pathname operation. The join
method accepts multiple arguments and handles separator insertion automatically. This eliminates errors from manual string concatenation and separator management.
# Joining paths
base = Pathname.new('/usr/local')
bin_path = base.join('bin', 'ruby')
# => #<Pathname:/usr/local/bin/ruby>
# Complex joining
config_root = Pathname.new('config')
database_config = config_root.join('environments', 'production.yml')
cache_config = config_root.join('cache', 'redis.conf')
# Handles trailing separators correctly
path_with_slash = Pathname.new('/app/')
joined = path_with_slash.join('models', 'user.rb')
# => #<Pathname:/app/models/user.rb>
Path decomposition methods extract components from existing paths. These methods return Pathname objects for further manipulation or strings for direct use.
file_path = Pathname.new('/var/log/application.log.gz')
# Component extraction
file_path.dirname # => #<Pathname:/var/log>
file_path.basename # => #<Pathname:application.log.gz>
file_path.basename('.gz') # => #<Pathname:application.log>
file_path.extname # => ".gz"
# Path traversal
file_path.parent # => #<Pathname:/var/log>
file_path.parent.parent # => #<Pathname:/var>
File system queries determine path properties without loading file contents. These methods delegate to File and Dir classes but maintain the Pathname interface.
path = Pathname.new('config/database.yml')
# Existence checks
path.exist? # => true/false
path.file? # => true if regular file
path.directory? # => true if directory
path.symlink? # => true if symbolic link
# Permission checks
path.readable? # => true if readable
path.writable? # => true if writable
path.executable? # => true if executable
# Size and timestamps
path.size # => file size in bytes
path.mtime # => modification time
path.ctime # => creation time
Path resolution converts relative paths to absolute paths and resolves symbolic links. The expand_path
method handles ..
and .
components and prepends the current working directory to relative paths.
# Path expansion
relative = Pathname.new('../config/app.yml')
absolute = relative.expand_path
# => #<Pathname:/home/user/project/config/app.yml>
# From specific directory
expanded = relative.expand_path('/opt/myapp')
# => #<Pathname:/opt/config/app.yml>
# Real path (resolves symlinks)
link_path = Pathname.new('current_version') # symbolic link
real_path = link_path.realpath
# => #<Pathname:/opt/myapp/releases/20240315>
Reading and writing files through Pathname objects provides a convenient interface that handles path conversion internally. These methods delegate to File class methods but accept the same arguments.
config_path = Pathname.new('config/settings.json')
# Reading files
content = config_path.read
lines = config_path.readlines
# Writing files
config_path.write('{"debug": true}')
config_path.write('log entry', mode: 'a') # append mode
# File operations
config_path.delete # remove file
backup = Pathname.new('config/settings.json.bak')
config_path.rename(backup) # move file
Error Handling & Debugging
File system operations through Pathname objects raise the same exceptions as their File and Dir counterparts. Understanding these exceptions helps build robust applications that handle missing files, permission issues, and other file system problems.
The most common exception is Errno::ENOENT
, raised when attempting operations on non-existent files or directories. This occurs with read operations, file queries on missing files, and directory traversal through non-existent paths.
missing_file = Pathname.new('non_existent.txt')
begin
content = missing_file.read
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
# Handle missing file - create default, prompt user, etc.
end
# Safer existence checking
if missing_file.exist?
content = missing_file.read
else
# Handle missing file case
content = create_default_content
end
Permission errors manifest as Errno::EACCES
exceptions when the process lacks required file system permissions. These errors occur during read, write, or execute operations on restricted files.
restricted_file = Pathname.new('/etc/passwd')
begin
restricted_file.write('malicious content')
rescue Errno::EACCES => e
puts "Permission denied: #{e.message}"
# Log security attempt, notify admin, etc.
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
end
# Check permissions before operations
if restricted_file.readable?
content = restricted_file.read
else
puts "Cannot read #{restricted_file}"
end
Directory operations introduce additional error conditions. Creating directories can fail due to parent directory absence, while directory removal fails when directories contain files.
deep_path = Pathname.new('/tmp/app/cache/sessions')
begin
deep_path.mkdir
rescue Errno::ENOENT => e
# Parent directory doesn't exist
deep_path.parent.mkpath # Create parent directories
deep_path.mkdir # Try again
rescue Errno::EEXIST => e
# Directory already exists - usually safe to ignore
puts "Directory exists: #{deep_path}"
end
# Safe directory creation
deep_path.mkpath # Creates all parent directories if needed
Path resolution errors occur when symbolic links point to non-existent targets or when circular references exist. The realpath
method raises exceptions for broken links, while expand_path
handles most path normalization safely.
broken_link = Pathname.new('broken_symlink')
begin
real_path = broken_link.realpath
rescue Errno::ENOENT => e
puts "Broken symlink: #{e.message}"
# Clean up broken link or recreate target
broken_link.delete if broken_link.symlink?
end
# Check link status
if broken_link.symlink?
begin
target = broken_link.readlink
puts "Link points to: #{target}"
rescue Errno::ENOENT
puts "Broken symlink detected"
end
end
Debugging path issues often involves examining the actual path strings that Pathname objects contain. The to_s
method reveals the internal string representation, while path decomposition methods help identify problematic components.
problematic_path = Pathname.new('//double//slashes/../weird/./path')
puts "Original: #{problematic_path.to_s}"
puts "Normalized: #{problematic_path.cleanpath.to_s}"
puts "Absolute: #{problematic_path.expand_path.to_s}"
# Component analysis
puts "Directory: #{problematic_path.dirname}"
puts "Basename: #{problematic_path.basename}"
puts "Parts: #{problematic_path.each_filename.to_a}"
File encoding issues can cause problems when reading files with non-ASCII content. Pathname delegates to File.read, which uses the default external encoding unless specified.
utf8_file = Pathname.new('unicode_content.txt')
begin
content = utf8_file.read
rescue EncodingError => e
puts "Encoding error: #{e.message}"
# Try with explicit encoding
content = utf8_file.read(encoding: 'utf-8')
end
# Specify encoding upfront
content = utf8_file.read(encoding: 'utf-8', invalid: :replace)
Production Patterns
Production applications require robust file handling patterns that account for concurrent access, error recovery, and system administration concerns. Pathname provides the foundation for these patterns while integrating with application frameworks and deployment tools.
Configuration file management represents a critical production pattern. Applications typically load configuration from multiple files with different precedence levels and environment-specific overrides.
class ConfigLoader
def initialize(app_root)
@app_root = Pathname.new(app_root)
@config_dir = @app_root.join('config')
end
def load_config(environment = 'production')
config = {}
# Load base configuration
base_config = @config_dir.join('application.yml')
config.merge!(YAML.load_file(base_config)) if base_config.exist?
# Load environment-specific config
env_config = @config_dir.join("#{environment}.yml")
config.merge!(YAML.load_file(env_config)) if env_config.exist?
# Load local overrides (not in version control)
local_config = @config_dir.join('local.yml')
config.merge!(YAML.load_file(local_config)) if local_config.exist?
config
end
def config_files_status
[@config_dir.join('application.yml'),
@config_dir.join('production.yml'),
@config_dir.join('local.yml')].map do |path|
{
path: path.to_s,
exists: path.exist?,
readable: path.readable?,
modified: path.exist? ? path.mtime : nil
}
end
end
end
Log file management requires handling file rotation, directory creation, and permission management. Production applications often write to multiple log files with different retention policies.
class LogManager
def initialize(log_root)
@log_root = Pathname.new(log_root)
ensure_log_directory
end
def application_log_path(date = Date.today)
@log_root.join('application', "#{date.strftime('%Y-%m-%d')}.log")
end
def error_log_path
@log_root.join('errors', 'error.log')
end
def rotate_logs(days_to_keep = 30)
cutoff_date = Date.today - days_to_keep
@log_root.join('application').children.select(&:file?).each do |log_file|
if extract_date_from_filename(log_file) < cutoff_date
log_file.delete
end
end
end
def ensure_log_permissions
[@log_root.join('application'), @log_root.join('errors')].each do |dir|
dir.chmod(0755) if dir.exist?
dir.children.select(&:file?).each { |f| f.chmod(0644) }
end
end
private
def ensure_log_directory
@log_root.mkpath unless @log_root.exist?
[@log_root.join('application'), @log_root.join('errors')].each(&:mkpath)
end
def extract_date_from_filename(path)
match = path.basename.to_s.match(/(\d{4}-\d{2}-\d{2})/)
match ? Date.parse(match[1]) : Date.today
end
end
Asset and static file serving in web applications requires path validation to prevent directory traversal attacks and efficient file serving with appropriate headers.
class StaticFileServer
def initialize(public_root)
@public_root = Pathname.new(public_root).expand_path
@cache_control = 'public, max-age=31536000' # 1 year
end
def serve_file(request_path)
# Sanitize and resolve path
clean_path = sanitize_path(request_path)
file_path = @public_root.join(clean_path)
# Security check - ensure file is within public root
unless file_within_root?(file_path)
raise SecurityError, "Path outside public directory: #{request_path}"
end
# File existence and type check
return nil unless file_path.file?
{
path: file_path,
content_type: determine_content_type(file_path),
cache_control: cache_control_for_file(file_path),
last_modified: file_path.mtime,
size: file_path.size
}
end
def precompile_assets(source_dir, target_dir)
source_root = Pathname.new(source_dir)
target_root = Pathname.new(target_dir)
source_root.find do |source_file|
next unless source_file.file?
relative_path = source_file.relative_path_from(source_root)
target_file = target_root.join(relative_path)
target_file.parent.mkpath
if needs_compilation?(source_file)
compile_asset(source_file, target_file)
else
source_file.copy(target_file)
end
end
end
private
def sanitize_path(path)
# Remove query parameters and fragments
clean = path.split('?').first.split('#').first
# Decode URL encoding
clean = URI.decode_www_form_component(clean)
# Remove leading slash and resolve path
Pathname.new(clean.sub(/^\//, '')).cleanpath.to_s
end
def file_within_root?(file_path)
file_path.expand_path.to_s.start_with?(@public_root.to_s)
end
def determine_content_type(file_path)
case file_path.extname.downcase
when '.html' then 'text/html'
when '.css' then 'text/css'
when '.js' then 'application/javascript'
when '.json' then 'application/json'
when '.png' then 'image/png'
when '.jpg', '.jpeg' then 'image/jpeg'
else 'application/octet-stream'
end
end
def cache_control_for_file(file_path)
if file_path.to_s.include?('assets') &&
file_path.basename.to_s.match?(/\w{8,}\.(css|js|png|jpg)$/)
@cache_control # Fingerprinted assets get long cache
else
'public, max-age=3600' # Regular files get shorter cache
end
end
def needs_compilation?(source_file)
%w[.scss .sass .coffee .ts].include?(source_file.extname)
end
def compile_asset(source, target)
# Asset compilation logic would go here
# This is framework/tool specific
end
end
Database backup and migration patterns often involve file system operations for backup storage and schema migration tracking.
class DatabaseBackupManager
def initialize(backup_root, database_config)
@backup_root = Pathname.new(backup_root)
@database_config = database_config
ensure_backup_structure
end
def create_backup(backup_name = nil)
backup_name ||= "backup_#{Time.now.strftime('%Y%m%d_%H%M%S')}"
backup_file = @backup_root.join('daily', "#{backup_name}.sql.gz")
backup_file.parent.mkpath
# Create database dump (simplified)
dump_command = build_dump_command(backup_file)
success = system(dump_command)
if success
create_backup_manifest(backup_file)
cleanup_old_backups
backup_file
else
backup_file.delete if backup_file.exist?
raise "Backup failed: #{dump_command}"
end
end
def restore_backup(backup_file_path)
backup_file = Pathname.new(backup_file_path)
unless backup_file.exist? && backup_file.readable?
raise "Cannot read backup file: #{backup_file}"
end
restore_command = build_restore_command(backup_file)
success = system(restore_command)
unless success
raise "Restore failed: #{restore_command}"
end
true
end
def list_backups
@backup_root.join('daily').children
.select { |f| f.file? && f.extname == '.gz' }
.sort_by(&:mtime)
.reverse
.map { |f| backup_info(f) }
end
private
def ensure_backup_structure
%w[daily weekly monthly].each do |period|
@backup_root.join(period).mkpath
end
end
def build_dump_command(backup_file)
"pg_dump #{@database_config['url']} | gzip > #{backup_file}"
end
def build_restore_command(backup_file)
"gunzip -c #{backup_file} | psql #{@database_config['url']}"
end
def create_backup_manifest(backup_file)
manifest_file = backup_file.sub_ext('.manifest.json')
manifest = {
backup_file: backup_file.basename.to_s,
created_at: Time.now.iso8601,
database: @database_config['database'],
size_bytes: backup_file.size
}
manifest_file.write(JSON.pretty_generate(manifest))
end
def cleanup_old_backups
cutoff_date = 30.days.ago
@backup_root.join('daily').children.select(&:file?).each do |backup_file|
if backup_file.mtime < cutoff_date
backup_file.delete
manifest_file = backup_file.sub_ext('.manifest.json')
manifest_file.delete if manifest_file.exist?
end
end
end
def backup_info(backup_file)
manifest_file = backup_file.sub_ext('.manifest.json')
if manifest_file.exist?
JSON.parse(manifest_file.read)
else
{
backup_file: backup_file.basename.to_s,
created_at: backup_file.mtime.iso8601,
size_bytes: backup_file.size
}
end
end
end
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Pathname.new(path) |
path (String) |
Pathname |
Creates new Pathname object |
#to_s |
None | String |
Returns path as string |
#to_path |
None | String |
Alias for #to_s , used by File methods |
#==(other) |
other (Object) |
Boolean |
Path equality comparison |
#<=>(other) |
other (Pathname) |
Integer |
Path comparison for sorting |
#hash |
None | Integer |
Hash value for use as hash key |
Path Information
Method | Parameters | Returns | Description |
---|---|---|---|
#absolute? |
None | Boolean |
True if path is absolute |
#relative? |
None | Boolean |
True if path is relative |
#root? |
None | Boolean |
True if path is root directory |
#basename(suffix=nil) |
suffix (String, optional) |
Pathname |
Final path component |
#dirname |
None | Pathname |
Directory portion of path |
#extname |
None | String |
File extension including dot |
#parent |
None | Pathname |
Parent directory |
Path Manipulation
Method | Parameters | Returns | Description |
---|---|---|---|
#join(*paths) |
*paths (String/Pathname) |
Pathname |
Joins paths with separator |
#+(path) |
path (String/Pathname) |
Pathname |
Alias for #join |
#expand_path(base=nil) |
base (String, optional) |
Pathname |
Converts to absolute path |
#realpath(base=nil) |
base (String, optional) |
Pathname |
Resolves symlinks to real path |
#cleanpath |
None | Pathname |
Removes redundant components |
#relative_path_from(base) |
base (Pathname) |
Pathname |
Path relative to base directory |
File System Queries
Method | Parameters | Returns | Description |
---|---|---|---|
#exist? |
None | Boolean |
True if path exists |
#file? |
None | Boolean |
True if regular file |
#directory? |
None | Boolean |
True if directory |
#symlink? |
None | Boolean |
True if symbolic link |
#readable? |
None | Boolean |
True if readable by process |
#writable? |
None | Boolean |
True if writable by process |
#executable? |
None | Boolean |
True if executable by process |
#size |
None | Integer |
File size in bytes |
#zero? |
None | Boolean |
True if file exists and is empty |
File System Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#mkdir(mode=0777) |
mode (Integer) |
Integer |
Creates directory |
#mkpath(mode=0777) |
mode (Integer) |
nil |
Creates directory and parents |
#rmdir |
None | Integer |
Removes empty directory |
#rmtree |
None | Integer |
Removes directory and contents |
#delete |
None | Integer |
Deletes file |
#unlink |
None | Integer |
Alias for #delete |
#rename(new_name) |
new_name (String/Pathname) |
Integer |
Renames file or directory |
File Content Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#read(*args) |
Variable args | String |
Reads entire file content |
#binread(*args) |
Variable args | String |
Reads file in binary mode |
#readlines(*args) |
Variable args | Array<String> |
Reads file as array of lines |
#write(content, *args) |
content (String), options |
Integer |
Writes content to file |
#binwrite(content, *args) |
content (String), options |
Integer |
Writes binary content to file |
#copy(dest) |
dest (String/Pathname) |
nil |
Copies file to destination |
Directory Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#opendir(&block) |
Block (optional) | Dir or result |
Opens directory for reading |
#entries |
None | Array<Pathname> |
Lists directory contents |
#children(with_directory=true) |
with_directory (Boolean) |
Array<Pathname> |
Lists directory contents excluding . and .. |
#each_child(&block) |
Block | Enumerator |
Iterates over child paths |
#find(&block) |
Block | Enumerator |
Recursively finds all paths |
Time and Permission Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#atime |
None | Time |
Last access time |
#mtime |
None | Time |
Last modification time |
#ctime |
None | Time |
Creation time |
#chmod(mode) |
mode (Integer) |
Integer |
Changes file permissions |
#lchmod(mode) |
mode (Integer) |
Integer |
Changes link permissions |
#chown(owner, group) |
owner , group (Integer) |
Integer |
Changes file ownership |
#lchown(owner, group) |
owner , group (Integer) |
Integer |
Changes link ownership |
Path Iteration
Method | Parameters | Returns | Description |
---|---|---|---|
#each_filename(&block) |
Block | Enumerator |
Iterates over path components |
#ascend(&block) |
Block | Enumerator |
Iterates from path to root |
#descend(&block) |
Block | Enumerator |
Iterates from root to path |
Link Operations
Method | Parameters | Returns | Description |
---|---|---|---|
#readlink |
None | Pathname |
Reads symbolic link target |
#make_link(target) |
target (String/Pathname) |
Integer |
Creates hard link |
#make_symlink(target) |
target (String/Pathname) |
Integer |
Creates symbolic link |
Constants and Exceptions
Constant | Value | Description |
---|---|---|
Pathname::SEPARATOR_PAT |
/[#{Regexp.quote(File::SEPARATOR)}#{Regexp.quote(File::ALT_SEPARATOR)}]/ |
Pattern matching path separators |
Common Exceptions:
Errno::ENOENT
- File or directory not foundErrno::EACCES
- Permission deniedErrno::EEXIST
- File or directory already existsErrno::ENOTDIR
- Not a directoryErrno::EISDIR
- Is a directory (when file expected)ArgumentError
- Invalid path or argumentsSystemCallError
- System call failures
File Mode Constants (for chmod operations):
0644
- Read/write owner, read group/other0755
- Read/write/execute owner, read/execute group/other0666
- Read/write all users0777
- Read/write/execute all users