CrackedRuby logo

CrackedRuby

Pathname

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 exist
  • Errno::EACCES - Permission denied
  • Errno::ENOTDIR - Component is not a directory
  • Errno::EISDIR - Path is a directory when file expected
  • Errno::ELOOP - Too many symbolic links
  • ArgumentError - Invalid path argument
  • TypeError - Wrong argument type