CrackedRuby logo

CrackedRuby

Dir Class

Ruby directory manipulation and traversal using the Dir class for file system navigation and directory operations.

Core Built-in Classes File and IO Classes
2.9.6

Overview

The Dir class provides methods for working with directories and their contents in Ruby. Ruby implements Dir as both a class with instance methods for directory handles and class methods for directory operations. The class wraps system-level directory operations and presents them through a Ruby interface.

Dir operates on directory paths as strings and returns directory contents as arrays of filenames. The class handles platform-specific directory separators and path conventions automatically. Ruby's Dir implementation includes pattern matching capabilities using shell-style glob patterns and regular expressions.

# Open directory handle
dir = Dir.open('/home/user')
dir.each { |filename| puts filename }
dir.close

# Class methods for directory operations
Dir.entries('/home/user')
# => [".", "..", "documents", "downloads", "file.txt"]

# Pattern matching with glob
Dir.glob('/home/user/*.txt')
# => ["/home/user/file.txt", "/home/user/readme.txt"]

The Dir class integrates with Ruby's File class for complete file system operations. Dir handles directory-specific operations while File manages individual file operations. Both classes work with the same path string representations and follow similar error handling patterns.

Directory operations through Dir are synchronous and block execution until completion. The class provides no built-in asynchronous or threaded directory operations, requiring external threading mechanisms for concurrent directory access.

Basic Usage

Dir provides several approaches for accessing directory contents. The most direct method uses Dir.entries to retrieve all filenames in a directory as an array. This method includes the current directory marker . and parent directory marker .. in results.

# Get all entries in directory
entries = Dir.entries('/var/log')
# => [".", "..", "system.log", "error.log", "access.log"]

# Filter out directory markers
files = entries.reject { |name| name.start_with?('.') }
# => ["system.log", "error.log", "access.log"]

The Dir.foreach method iterates through directory contents without loading all names into memory simultaneously. This approach consumes less memory for directories containing many files.

# Iterate through directory entries
Dir.foreach('/usr/bin') do |filename|
  next if filename.start_with?('.')
  puts "Found executable: #{filename}"
end

# Enumerate with index
Dir.foreach('/etc').with_index do |filename, index|
  puts "#{index}: #{filename}" unless filename.start_with?('.')
end

Dir supports glob patterns for selective file matching using shell-style wildcards. The Dir.glob method accepts patterns with *, ?, [], and {} characters for flexible filename matching.

# Basic wildcard patterns
Dir.glob('/home/user/*.rb')
# => ["/home/user/script.rb", "/home/user/app.rb"]

# Multiple patterns with braces
Dir.glob('/var/log/{*.log,*.txt}')
# => ["/var/log/system.log", "/var/log/error.txt"]

# Recursive directory traversal
Dir.glob('/home/user/**/*.rb')
# => ["/home/user/script.rb", "/home/user/lib/helper.rb"]

# Character class matching
Dir.glob('/tmp/file[0-9].dat')
# => ["/tmp/file1.dat", "/tmp/file5.dat"]

Directory instance methods provide stateful access to directory contents. Creating a Dir instance opens a directory handle that maintains position during iteration. Instance methods mirror class methods but operate on the opened directory.

# Directory handle operations
dir = Dir.new('/home/user/documents')

# Read entries one at a time
filename = dir.read
# => "."

# Reset position and iterate
dir.rewind
dir.each { |name| puts name unless name.start_with?('.') }

# Check current position
position = dir.pos
dir.seek(position)

dir.close

The Dir.chdir method changes the current working directory for the Ruby process. This method affects all subsequent relative path operations within the same process.

# Change working directory
original_dir = Dir.pwd
# => "/home/user"

Dir.chdir('/tmp')
Dir.pwd
# => "/tmp"

# Block form automatically restores directory
Dir.chdir('/var/log') do
  puts Dir.pwd  # => "/var/log"
  # Perform operations in /var/log
end

puts Dir.pwd  # => "/home/user" (restored)

Error Handling & Debugging

Directory operations generate specific exceptions when encountering file system errors. The most common exception is Errno::ENOENT when attempting to access non-existent directories. Permission errors raise Errno::EACCES exceptions.

# Handle missing directory
begin
  entries = Dir.entries('/nonexistent/path')
rescue Errno::ENOENT => e
  puts "Directory not found: #{e.message}"
  entries = []
end

# Handle permission errors
begin
  Dir.foreach('/root') { |f| puts f }
rescue Errno::EACCES => e
  puts "Permission denied: #{e.message}"
rescue StandardError => e
  puts "Unexpected error: #{e.class} - #{e.message}"
end

Glob operations fail silently when encountering permission errors on intermediate directories. The method returns partial results without raising exceptions, making debugging glob failures challenging.

# Glob may return incomplete results due to permission errors
results = Dir.glob('/home/**/secret/*.txt')

# Verify expected directories exist
expected_dirs = ['/home/user1/secret', '/home/user2/secret']
expected_dirs.each do |dir|
  unless Dir.exist?(dir)
    puts "Directory missing or inaccessible: #{dir}"
  end
end

# Test individual directory access
def check_directory_access(path)
  Dir.entries(path)
  true
rescue Errno::EACCES
  false
rescue Errno::ENOENT
  nil  # Directory doesn't exist
end

Directory handle operations require proper cleanup to prevent resource leaks. Unclosed Dir instances consume file descriptors, potentially exhausting system resources in long-running applications.

# Ensure directory handles close properly
def safe_directory_operation(path)
  dir = Dir.open(path)
  yield(dir)
ensure
  dir&.close
end

# Use block form for automatic cleanup
safe_directory_operation('/var/log') do |dir|
  dir.each { |filename| process_file(filename) }
end

# Monitor open file descriptors in development
def count_open_fds
  Dir.entries('/proc/self/fd').length - 2  # Exclude . and ..
rescue Errno::ENOENT
  0  # Not available on non-Linux systems
end

Path encoding issues cause failures when directory names contain non-ASCII characters. Ruby's default external encoding determines how Dir interprets filenames from the file system.

# Handle encoding issues
def safe_directory_read(path)
  Dir.entries(path).map do |filename|
    # Force encoding to UTF-8 and handle invalid bytes
    filename.encode('UTF-8', invalid: :replace, undef: :replace)
  end
rescue Encoding::InvalidByteSequenceError => e
  puts "Encoding error in directory listing: #{e.message}"
  []
end

# Debug encoding issues
Dir.entries('.').each do |filename|
  puts "#{filename}: #{filename.encoding}"
  puts "Valid UTF-8: #{filename.valid_encoding?}"
end

Production Patterns

Directory operations in production applications require careful consideration of performance and resource usage. Large directories containing thousands of files can cause memory and performance issues when loaded entirely into memory.

# Memory-efficient directory processing for large directories
class DirectoryProcessor
  def initialize(path, batch_size: 1000)
    @path = path
    @batch_size = batch_size
  end

  def process_files
    batch = []
    
    Dir.foreach(@path) do |filename|
      next if filename.start_with?('.')
      
      batch << filename
      
      if batch.length >= @batch_size
        yield(batch)
        batch.clear
      end
    end
    
    yield(batch) unless batch.empty?
  end
end

# Usage in production
processor = DirectoryProcessor.new('/var/log/archives', batch_size: 500)
processor.process_files do |batch|
  batch.each { |filename| analyze_log_file(filename) }
  GC.start if batch.length == 500  # Force garbage collection
end

Monitoring directory operations in production requires tracking both performance metrics and error rates. Directory operations can become bottlenecks in applications that frequently scan file systems.

# Production monitoring wrapper
class MonitoredDirectory
  def initialize(path, logger:, metrics:)
    @path = path
    @logger = logger
    @metrics = metrics
  end

  def entries
    start_time = Time.now
    result = Dir.entries(@path)
    duration = Time.now - start_time
    
    @metrics.histogram('dir.entries.duration', duration)
    @metrics.counter('dir.entries.success').increment
    @logger.debug("Dir.entries(#{@path}) completed in #{duration}s")
    
    result
  rescue StandardError => e
    @metrics.counter('dir.entries.error').increment
    @logger.error("Dir.entries(#{@path}) failed: #{e.message}")
    raise
  end

  def glob(pattern)
    full_pattern = File.join(@path, pattern)
    start_time = Time.now
    
    result = Dir.glob(full_pattern)
    duration = Time.now - start_time
    
    @metrics.histogram('dir.glob.duration', duration)
    @metrics.gauge('dir.glob.results', result.length)
    
    result
  end
end

Deployment scripts frequently use Dir operations for file management and directory structure validation. These operations must handle various deployment environments and file system configurations.

# Deployment directory operations
class DeploymentManager
  def initialize(base_path, logger)
    @base_path = base_path
    @logger = logger
  end

  def ensure_directory_structure
    required_dirs = %w[logs tmp config uploads]
    
    required_dirs.each do |dir_name|
      dir_path = File.join(@base_path, dir_name)
      
      unless Dir.exist?(dir_path)
        @logger.info("Creating directory: #{dir_path}")
        Dir.mkdir(dir_path)
      end
      
      validate_directory_permissions(dir_path)
    end
  end

  def cleanup_old_releases(keep_count: 5)
    releases_path = File.join(@base_path, 'releases')
    return unless Dir.exist?(releases_path)
    
    # Get release directories sorted by creation time
    releases = Dir.entries(releases_path)
                 .reject { |name| name.start_with?('.') }
                 .map { |name| File.join(releases_path, name) }
                 .select { |path| Dir.exist?(path) }
                 .sort_by { |path| File.ctime(path) }
    
    # Remove old releases
    releases[0...-keep_count].each do |old_release|
      @logger.info("Removing old release: #{old_release}")
      FileUtils.rm_rf(old_release)
    end
  end

  private

  def validate_directory_permissions(path)
    stat = File.stat(path)
    mode = stat.mode & 0777
    
    unless mode == 0755 || mode == 0775
      @logger.warn("Unexpected permissions on #{path}: #{mode.to_s(8)}")
    end
  rescue Errno::ENOENT
    @logger.error("Directory validation failed - path does not exist: #{path}")
  end
end

Caching directory contents improves performance in applications that repeatedly access the same directories. Cache invalidation strategies must account for file system changes.

# Directory content caching
class CachedDirectory
  def initialize(path, ttl: 300)  # 5 minute TTL
    @path = path
    @ttl = ttl
    @cache = {}
    @mutex = Mutex.new
  end

  def entries
    @mutex.synchronize do
      cache_key = :entries
      cached_data = @cache[cache_key]
      
      if cached_data && (Time.now - cached_data[:timestamp]) < @ttl
        return cached_data[:data]
      end
      
      fresh_data = Dir.entries(@path)
      @cache[cache_key] = {
        data: fresh_data,
        timestamp: Time.now
      }
      
      fresh_data
    end
  end

  def invalidate
    @mutex.synchronize { @cache.clear }
  end
end

Reference

Class Methods

Method Parameters Returns Description
Dir.[] pattern (String) Array<String> Equivalent to Dir.glob(pattern)
Dir.chdir path (String), block (optional) String or nil Changes working directory, returns previous directory
Dir.chroot path (String) nil Changes root directory (requires privileges)
Dir.delete path (String) nil Removes empty directory
Dir.entries path (String) Array<String> Returns all directory entries including . and ..
Dir.exist? path (String) Boolean Tests whether directory exists
Dir.foreach path (String), block (optional) nil or Enumerator Iterates through directory entries
Dir.getwd None String Returns current working directory
Dir.glob pattern (String/Array), flags (Integer, optional) Array<String> Returns filenames matching patterns
Dir.home user (String, optional) String Returns home directory path
Dir.mkdir path (String), mode (Integer, optional) nil Creates directory with optional permissions
Dir.open path (String), block (optional) Dir or result Opens directory handle
Dir.pwd None String Alias for Dir.getwd
Dir.rmdir path (String) nil Alias for Dir.delete
Dir.tmpdir None String Returns system temporary directory

Instance Methods

Method Parameters Returns Description
#close None nil Closes directory handle
#each block (optional) nil or Enumerator Iterates through directory entries
#path None String Returns directory path
#pos None Integer Returns current position in directory
#pos= position (Integer) Integer Sets position in directory
#read None String or nil Reads next directory entry
#rewind None Dir Resets position to beginning
#seek position (Integer) Dir Sets position in directory
#tell None Integer Alias for #pos

Glob Patterns

Pattern Matches Example
* Any characters except / *.rb matches file.rb
** Any characters including / **/*.rb matches dir/file.rb
? Single character file?.txt matches file1.txt
[abc] Single character from set file[123].txt matches file2.txt
[a-z] Single character from range [a-z]*.rb matches app.rb
{a,b} Either pattern a or b *.{rb,py} matches both .rb and .py

Glob Flags

Flag Value Description
File::FNM_CASEFOLD 8 Case-insensitive matching
File::FNM_DOTMATCH 4 * and ? match files starting with .
File::FNM_NOESCAPE 1 Backslashes don't escape special characters
File::FNM_PATHNAME 2 * and ? don't match / characters

Common Exceptions

Exception Cause Resolution
Errno::ENOENT Directory doesn't exist Verify path exists, create if needed
Errno::EACCES Permission denied Check file permissions, run as appropriate user
Errno::ENOTDIR Path is not a directory Verify path points to directory, not file
Errno::ENOTEMPTY Directory not empty (delete) Remove contents before deleting directory
SystemCallError General system error Check system resources, file system status