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 |