CrackedRuby logo

CrackedRuby

FileUtils

This guide covers FileUtils, Ruby's standard library for file system operations including copying, moving, deleting, and creating files and directories.

Standard Library File Utilities
4.6.1

Overview

FileUtils provides class methods for common file system operations across different operating systems. Ruby implements FileUtils as a module that wraps low-level file operations with consistent behavior and error handling. The module offers both destructive operations that modify the file system immediately and non-destructive preview modes for testing workflows.

FileUtils operations fall into several categories: file copying (cp, cp_r), moving (mv), deletion (rm, rm_rf), directory creation (mkdir, mkdir_p), and permission changes (chmod, chown). Each operation supports options for verbose output, dry-run mode, and error handling behavior.

require 'fileutils'

# Copy a file
FileUtils.cp('source.txt', 'destination.txt')

# Create nested directories
FileUtils.mkdir_p('path/to/nested/directories')

# Remove files and directories
FileUtils.rm_rf('unwanted_directory')

The module defines two main operational modes: normal execution and dry-run mode. Dry-run mode prints operations without executing them, while normal mode performs actual file system changes. FileUtils also provides verbose output options that display each operation as it executes.

Ruby's FileUtils handles cross-platform differences in file operations. Path separators, permission handling, and symbolic link behavior adapt to the underlying operating system. The module raises specific exceptions for different error conditions, allowing applications to handle file operation failures appropriately.

Basic Usage

FileUtils copying operations handle both individual files and directory trees. The cp method copies single files, while cp_r recursively copies entire directory structures. Both methods accept arrays of source paths for batch operations.

require 'fileutils'

# Single file copy
FileUtils.cp('config.yml', 'config.yml.backup')

# Multiple file copy to directory
FileUtils.cp(['file1.txt', 'file2.txt'], 'backup_dir/')

# Recursive directory copy
FileUtils.cp_r('source_directory', 'backup_directory')

# Copy with preservation of timestamps and permissions
FileUtils.cp_r('source', 'dest', preserve: true)

Directory operations create, modify, and remove directory structures. The mkdir method creates single directories, while mkdir_p creates nested directory paths, similar to mkdir -p in Unix systems. Directory removal uses rmdir for empty directories or rm_rf for recursive deletion.

# Create single directory
FileUtils.mkdir('new_directory')

# Create nested directory structure
FileUtils.mkdir_p('deep/nested/directory/structure')

# Remove empty directory
FileUtils.rmdir('empty_directory')

# Remove directory and all contents
FileUtils.rm_rf('directory_with_contents')

Moving and renaming operations use the mv method, which works across file systems when necessary. The method handles both files and directories, automatically detecting whether to perform a rename operation or cross-file-system move.

# Rename file
FileUtils.mv('old_name.txt', 'new_name.txt')

# Move file to directory
FileUtils.mv('file.txt', 'destination_directory/')

# Move multiple files
FileUtils.mv(['file1.txt', 'file2.txt'], 'target_directory/')

# Move directory
FileUtils.mv('source_directory', 'new_location/')

Permission and ownership modifications apply to files and directories through chmod and chown operations. These methods accept both numeric and symbolic permission modes, with support for recursive application to directory trees.

# Change file permissions (numeric mode)
FileUtils.chmod(0644, 'file.txt')

# Change multiple files
FileUtils.chmod(0755, ['script1.sh', 'script2.sh'])

# Recursive permission change
FileUtils.chmod_R(0755, 'executable_directory')

# Change ownership (Unix systems)
FileUtils.chown('user', 'group', 'file.txt')

Error Handling & Debugging

FileUtils operations raise specific exception types for different failure conditions. Understanding these exceptions enables proper error recovery and user feedback. The most common exceptions include Errno::ENOENT for missing files, Errno::EACCES for permission errors, and Errno::ENOSPC for insufficient disk space.

require 'fileutils'

begin
  FileUtils.cp('nonexistent.txt', 'destination.txt')
rescue Errno::ENOENT => e
  puts "Source file not found: #{e.message}"
rescue Errno::EACCES => e
  puts "Permission denied: #{e.message}"
rescue StandardError => e
  puts "Unexpected error: #{e.class} - #{e.message}"
end

FileUtils provides dry-run capabilities through the :noop option, allowing operations to be tested without file system modifications. This mode prints the operations that would execute, making it valuable for validating complex file processing workflows before execution.

# Test operations without executing
FileUtils.cp_r('source', 'dest', noop: true, verbose: true)
# Output: cp -r source dest

# Verify directory structure before deletion
FileUtils.rm_rf('risky_directory', noop: true, verbose: true)
# Output: rm -rf risky_directory

Verbose mode provides detailed output for debugging file operations. The :verbose option displays each operation as it executes, showing the actual commands performed. This output helps identify which operations succeed or fail in complex workflows.

# Enable verbose output for all operations
FileUtils.cp_r('source', 'dest', verbose: true)
# Output: cp -r source dest

FileUtils.mkdir_p('path/to/directory', verbose: true)  
# Output: mkdir -p path/to/directory

# Combine verbose and dry-run for comprehensive testing
FileUtils.mv(Dir.glob('*.log'), 'logs/', verbose: true, noop: true)
# Output shows all files that would be moved

Permission-related errors require careful handling, especially in multi-user environments or when processing files with varied ownership. FileUtils operations respect existing file permissions, which can cause unexpected failures when copying or modifying files.

def safe_file_copy(source, dest)
  # Check source file exists and is readable
  unless File.readable?(source)
    raise "Cannot read source file: #{source}"
  end
  
  # Check destination directory is writable
  dest_dir = File.dirname(dest)
  unless File.writable?(dest_dir)
    raise "Cannot write to destination directory: #{dest_dir}"
  end
  
  FileUtils.cp(source, dest)
rescue Errno::ENOSPC
  raise "Insufficient disk space for copy operation"
rescue Errno::EMFILE, Errno::ENFILE
  raise "Too many open files - system limit reached"
end

Cross-platform path handling requires attention to path separator differences and case sensitivity. FileUtils handles separator conversion automatically, but applications must consider case sensitivity differences between file systems.

# Path joining that works across platforms
def safe_path_join(*parts)
  File.join(*parts).gsub('\\', '/')  # Normalize separators
end

# Case-sensitive file checking
def file_exists_case_sensitive?(path)
  return false unless File.exist?(path)
  
  # On case-insensitive systems, verify exact case match
  dir = File.dirname(path)
  name = File.basename(path)
  
  Dir.entries(dir).include?(name)
end

Production Patterns

FileUtils operations in production environments require careful consideration of atomicity, error recovery, and resource management. Atomic operations ensure file system consistency during complex workflows, especially when processing critical data or configuration files.

class AtomicFileProcessor
  def self.update_config(config_path, new_content)
    temp_path = "#{config_path}.tmp.#{Process.pid}"
    
    begin
      # Write to temporary file first
      File.write(temp_path, new_content)
      
      # Atomic replace using move operation
      FileUtils.mv(temp_path, config_path)
      
      puts "Configuration updated successfully"
    rescue StandardError => e
      # Clean up temporary file on failure
      FileUtils.rm_f(temp_path)
      raise "Config update failed: #{e.message}"
    end
  end
end

Batch file processing requires progress tracking and error isolation to handle large datasets reliably. Processing files individually with proper error handling prevents single file failures from stopping entire workflows.

class BatchFileProcessor
  def self.process_directory(source_dir, dest_dir, &block)
    processed = 0
    failed = []
    
    Dir.glob(File.join(source_dir, '*')).each do |file_path|
      next unless File.file?(file_path)
      
      begin
        filename = File.basename(file_path)
        dest_path = File.join(dest_dir, filename)
        
        # Ensure destination directory exists
        FileUtils.mkdir_p(dest_dir)
        
        # Process file with provided block
        block.call(file_path, dest_path)
        
        processed += 1
        
        # Progress feedback for long operations
        puts "Processed: #{processed}" if processed % 100 == 0
        
      rescue StandardError => e
        failed << { file: file_path, error: e.message }
        puts "Failed to process #{file_path}: #{e.message}"
      end
    end
    
    { processed: processed, failed: failed }
  end
end

# Usage example
result = BatchFileProcessor.process_directory('input/', 'output/') do |source, dest|
  # Custom processing logic
  FileUtils.cp(source, dest)
  FileUtils.chmod(0644, dest)
end

Log rotation and archival workflows benefit from FileUtils operations combined with timestamp-based naming and size management. These patterns handle growing log files and maintain historical data within storage constraints.

class LogRotator
  def initialize(log_file, max_size: 10_000_000, keep_files: 5)
    @log_file = log_file
    @max_size = max_size
    @keep_files = keep_files
  end
  
  def rotate_if_needed
    return unless File.exist?(@log_file)
    return unless File.size(@log_file) > @max_size
    
    timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
    archived_name = "#{@log_file}.#{timestamp}"
    
    begin
      # Move current log to archive
      FileUtils.mv(@log_file, archived_name)
      
      # Create new empty log file
      FileUtils.touch(@log_file)
      FileUtils.chmod(0644, @log_file)
      
      # Remove old archives beyond retention limit
      cleanup_old_archives
      
      puts "Log rotated: #{archived_name}"
    rescue StandardError => e
      puts "Log rotation failed: #{e.message}"
      # Restore original file if move succeeded but cleanup failed
      FileUtils.mv(archived_name, @log_file) if File.exist?(archived_name)
    end
  end
  
  private
  
  def cleanup_old_archives
    pattern = "#{@log_file}.*"
    archives = Dir.glob(pattern).sort
    
    # Remove oldest files beyond retention limit
    while archives.size > @keep_files
      old_file = archives.shift
      FileUtils.rm_f(old_file)
      puts "Removed old archive: #{old_file}"
    end
  end
end

Deployment workflows use FileUtils for asset management, backup creation, and rollback capabilities. These operations require careful ordering and verification to maintain application availability during deployments.

class DeploymentManager
  def initialize(app_path, backup_path)
    @app_path = app_path
    @backup_path = backup_path
  end
  
  def deploy(new_version_path)
    backup_name = "backup_#{Time.now.strftime('%Y%m%d_%H%M%S')}"
    current_backup = File.join(@backup_path, backup_name)
    
    begin
      # Create backup directory
      FileUtils.mkdir_p(@backup_path)
      
      # Backup current version
      if File.exist?(@app_path)
        FileUtils.cp_r(@app_path, current_backup)
        puts "Created backup: #{current_backup}"
      end
      
      # Deploy new version
      FileUtils.rm_rf(@app_path) if File.exist?(@app_path)
      FileUtils.cp_r(new_version_path, @app_path)
      
      # Set proper permissions
      FileUtils.chmod_R(0755, File.join(@app_path, 'bin'))
      FileUtils.chmod_R(0644, File.join(@app_path, 'config'))
      
      puts "Deployment completed successfully"
      
      # Cleanup old backups (keep last 3)
      cleanup_old_backups
      
    rescue StandardError => e
      puts "Deployment failed: #{e.message}"
      rollback(current_backup) if File.exist?(current_backup)
      raise
    end
  end
  
  def rollback(backup_path)
    puts "Rolling back to: #{backup_path}"
    FileUtils.rm_rf(@app_path) if File.exist?(@app_path)
    FileUtils.cp_r(backup_path, @app_path)
    puts "Rollback completed"
  end
  
  private
  
  def cleanup_old_backups
    backups = Dir.glob(File.join(@backup_path, 'backup_*')).sort
    while backups.size > 3
      old_backup = backups.shift
      FileUtils.rm_rf(old_backup)
      puts "Removed old backup: #{old_backup}"
    end
  end
end

Reference

Core Methods

Method Parameters Returns Description
FileUtils.cp(src, dest, **opts) src (String/Array), dest (String), options (Hash) nil Copy files to destination
FileUtils.cp_r(src, dest, **opts) src (String/Array), dest (String), options (Hash) nil Copy files and directories recursively
FileUtils.mv(src, dest, **opts) src (String/Array), dest (String), options (Hash) nil Move files and directories
FileUtils.rm(list, **opts) list (String/Array), options (Hash) nil Remove files
FileUtils.rm_r(list, **opts) list (String/Array), options (Hash) nil Remove files and directories recursively
FileUtils.rm_rf(list, **opts) list (String/Array), options (Hash) nil Force remove files and directories recursively
FileUtils.rm_f(list, **opts) list (String/Array), options (Hash) nil Force remove files (ignore missing)

Directory Operations

Method Parameters Returns Description
FileUtils.mkdir(list, **opts) list (String/Array), options (Hash) nil Create directories
FileUtils.mkdir_p(list, **opts) list (String/Array), options (Hash) nil Create directories and parent directories
FileUtils.rmdir(list, **opts) list (String/Array), options (Hash) nil Remove empty directories
FileUtils.pwd None String Return current working directory
FileUtils.cd(dir, **opts) { } dir (String), options (Hash), block Block return value Change directory, optionally with block

Permission and Ownership

Method Parameters Returns Description
FileUtils.chmod(mode, list, **opts) mode (Integer), list (String/Array), options (Hash) nil Change file permissions
FileUtils.chmod_R(mode, list, **opts) mode (Integer), list (String/Array), options (Hash) nil Change file permissions recursively
FileUtils.chown(user, group, list, **opts) user (String/Integer), group (String/Integer), list (String/Array), options (Hash) nil Change file ownership
FileUtils.chown_R(user, group, list, **opts) user (String/Integer), group (String/Integer), list (String/Array), options (Hash) nil Change file ownership recursively

File Creation and Links

Method Parameters Returns Description
FileUtils.touch(list, **opts) list (String/Array), options (Hash) nil Create empty files or update timestamps
FileUtils.ln(src, dest, **opts) src (String), dest (String), options (Hash) nil Create hard link
FileUtils.ln_s(src, dest, **opts) src (String), dest (String), options (Hash) nil Create symbolic link
FileUtils.ln_sf(src, dest, **opts) src (String), dest (String), options (Hash) nil Create symbolic link (force overwrite)

Comparison and Information

Method Parameters Returns Description
FileUtils.cmp(file1, file2) file1 (String), file2 (String) Boolean Compare file contents
FileUtils.identical?(file1, file2) file1 (String), file2 (String) Boolean Check if files are identical
FileUtils.uptodate?(file, deps) file (String), deps (Array) Boolean Check if file is newer than dependencies

Common Options

Option Type Description Compatible Methods
:noop Boolean Dry-run mode, no actual operations All methods
:verbose Boolean Print operations to stdout All methods
:force Boolean Ignore errors and continue rm, rm_r, rm_rf, cp, cp_r
:preserve Boolean Preserve timestamps and permissions cp, cp_r
:mode Integer Set permissions for created directories mkdir, mkdir_p
:mtime Time Set modification time touch

Permission Mode Values

Octal Binary Permissions Description
0644 110100100 rw-r--r-- Owner read/write, group/other read
0755 111101101 rwxr-xr-x Owner full, group/other read/execute
0600 110000000 rw------- Owner read/write only
0700 111000000 rwx------ Owner full access only
0666 110110110 rw-rw-rw- All users read/write
0777 111111111 rwxrwxrwx All users full access

Exception Hierarchy

Exception Class Description Common Causes
Errno::ENOENT No such file or directory Missing source files, invalid paths
Errno::EACCES Permission denied Insufficient permissions, read-only files
Errno::ENOSPC No space left on device Disk full, quota exceeded
Errno::EEXIST File exists Attempting to create existing file/directory
Errno::EISDIR Is a directory Using file operation on directory
Errno::ENOTDIR Not a directory Using directory operation on file
Errno::EMFILE Too many open files System file descriptor limit reached