Guard & File Watching

Automate development workflows with Guard file system monitoring. Set up automated testing, compilation, and task execution on file changes.

Beginner 1 min read

Guard & File Watching in Ruby

Guard is a powerful file system monitoring tool that automatically executes actions when files change. Learn to set up automated testing, compilation, and development workflows that respond to file system events in real-time.

Introduction to Guard

Guard watches file system changes and automatically runs specified actions, making development workflows more efficient by eliminating manual task execution.

Guard Installation and Setup

# Install Guard
gem install guard

# Add to Gemfile
gem 'guard', group: :development

# Common Guard plugins
gem 'guard-rspec', group: :development      # For RSpec tests
gem 'guard-minitest', group: :development   # For Minitest
gem 'guard-rubocop', group: :development    # For code style checking
gem 'guard-bundler', group: :development    # For Gemfile changes
gem 'guard-sass', group: :development       # For Sass compilation
gem 'guard-livereload', group: :development # For browser reload

# Install Guard in your project
bundle install

# Initialize Guard (creates Guardfile)
bundle exec guard init

# Initialize specific plugins
bundle exec guard init rspec
bundle exec guard init minitest
bundle exec guard init rubocop

Basic Guardfile Configuration

The Guardfile is where you configure Guard's behavior, defining what files to watch and what actions to take when they change.

Guardfile Structure and Syntax

# Guardfile - main configuration file

# Global Guard options
guard_options = {
  all_after_pass: true,     # Run all tests after any test passes
  all_on_start: true,       # Run all tests when Guard starts
  keep_failed: true,        # Keep running failed tests until they pass
  cmd: 'bundle exec rspec'  # Command to run
}

# Watch Ruby files and run corresponding specs
guard :rspec, guard_options do
  # Watch spec files themselves
  watch(%r{^spec/.+_spec\.rb$})

  # Watch Ruby files and run corresponding specs
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
  watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }

  # Watch specific files and run all specs
  watch('spec/spec_helper.rb') { 'spec' }
  watch('config/routes.rb')    { 'spec/routing' }

  # Rails-specific watchers
  watch(%r{^app/controllers/(.+)_(controller)\.rb$}) do |m|
    [
      "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb",
      "spec/acceptance/#{m[1]}_spec.rb",
      "spec/routing/#{m[1]}_routing_spec.rb"
    ]
  end

  watch(%r{^app/models/(.+)\.rb$}) do |m|
    ["spec/models/#{m[1]}_spec.rb"]
  end
end

# Custom Guard block for general file watching
guard :shell do
  watch(%r{^config/(.+)\.yml$}) do |m|
    puts "Configuration file #{m[0]} changed!"
    `echo "Config updated: #{m[1]}" >> logs/config_changes.log`
  end

  watch(%r{^README\.md$}) do
    puts "README updated - regenerating documentation"
    `yard doc`
  end
end

# Multiple environments
guard :rspec, cmd: 'RAILS_ENV=test bundle exec rspec', all_on_start: false do
  # Test-specific configuration
end

guard :rspec, cmd: 'RAILS_ENV=development bundle exec rspec', all_on_start: false do
  # Development-specific configuration
end

Advanced Guard Configurations

Implement sophisticated watching patterns and conditional execution for complex development workflows.

Conditional Guards and Complex Patterns

# Advanced Guardfile with multiple Guard types

# Environment-based configuration
if ENV['GUARD_ENV'] == 'ci'
  # CI-specific configuration
  guard :rspec, cmd: 'bundle exec rspec --format documentation' do
    watch(%r{^spec/.+_spec\.rb$})
    watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
  end
else
  # Development configuration with notifications
  notification :notifysend, transient: true, timeout: 5

  guard :rspec, cmd: 'bundle exec rspec --format progress' do
    watch(%r{^spec/.+_spec\.rb$})
    watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }

    # React to test results
    callback(:run_all_begin) { puts "Running all specs..." }
    callback(:run_all_end) { |result| puts "All specs completed: #{result}" }
  end
end

# Guard for code quality
guard :rubocop, all_on_start: false, keep_failed: false do
  watch(%r{.+\.rb$})
  watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
end

# Guard for asset compilation
guard :sass,
      input: 'app/assets/stylesheets',
      output: 'public/stylesheets',
      style: :compressed,
      sourcemap: :file do

  watch(%r{^app/assets/stylesheets/(.+\.s[ac]ss)$})
end

# Guard for JavaScript/TypeScript
guard :shell do
  watch(%r{^app/assets/javascripts/(.+\.(js|ts|coffee))$}) do |m|
    puts "Compiling JavaScript: #{m[0]}"
    `npm run build`
  end
end

# Database migration watcher
guard :shell do
  watch(%r{^db/migrate/(.+)\.rb$}) do |m|
    puts "New migration detected: #{m[1]}"
    choice = ""
    while !['y', 'n'].include?(choice.downcase)
      print "Run migration now? (y/n): "
      choice = $stdin.gets.chomp
    end

    if choice.downcase == 'y'
      system('bundle exec rake db:migrate')
      puts "Migration completed"
    end
  end
end

# Guard with custom notifications
guard :shell do
  watch(%r{^Gemfile$}) do
    puts "Gemfile changed - running bundle install"
    result = system('bundle install')

    if result
      notification = "Bundle install completed successfully"
      system("notify-send 'Guard' '#{notification}'") if RUBY_PLATFORM.include?('linux')
    else
      puts "Bundle install failed!"
    end
  end
end

# Grouped guards for different concerns
group :tests do
  guard :rspec do
    # RSpec configuration
  end

  guard :minitest do
    # Minitest configuration
  end
end

group :quality do
  guard :rubocop do
    # RuboCop configuration
  end

  guard :reek do
    # Code smell detection
  end
end

group :assets do
  guard :sass do
    # Sass compilation
  end

  guard :coffeescript do
    # CoffeeScript compilation
  end
end

Custom Guard Plugins

Create custom Guard plugins for specialized file watching and automation needs specific to your project.

Building Custom Guard Plugins

# lib/guard/custom_deploy.rb
require 'guard/compat/plugin'

module Guard
  class CustomDeploy < Plugin
    def initialize(options = {})
      super
      @options = {
        environment: 'staging',
        cmd: 'cap deploy',
        run_all: false
      }.merge(options)
    end

    def start
      UI.info "Guard::CustomDeploy is running"
      UI.info "Watching for deployment triggers..."
    end

    def run_all
      return unless @options[:run_all]
      deploy_all
    end

    def run_on_additions(paths)
      deploy_if_needed(paths)
    end

    def run_on_modifications(paths)
      deploy_if_needed(paths)
    end

    def run_on_removals(paths)
      UI.info "Files removed: #{paths.join(', ')}"
    end

    private

    def deploy_if_needed(paths)
      return unless deployment_files_changed?(paths)

      UI.info "Deployment files changed: #{paths.join(', ')}"
      confirm_and_deploy
    end

    def deployment_files_changed?(paths)
      deployment_patterns = [
        /^config\/deploy/,
        /^Capfile$/,
        /^Gemfile$/,
        /^VERSION$/
      ]

      paths.any? { |path| deployment_patterns.any? { |pattern| path =~ pattern } }
    end

    def confirm_and_deploy
      UI.info "Deployment files changed!"

      return unless UI.agree("Deploy to #{@options[:environment]}? (y/n)")

      deploy_all
    end

    def deploy_all
      UI.info "Starting deployment to #{@options[:environment]}..."

      cmd = "#{@options[:cmd]} #{@options[:environment]}"
      result = system(cmd)

      if result
        UI.info "✅ Deployment to #{@options[:environment]} completed successfully"
        Notifier.notify("Deployment completed", title: "Guard::CustomDeploy")
      else
        UI.error "❌ Deployment to #{@options[:environment]} failed"
        Notifier.notify("Deployment failed", title: "Guard::CustomDeploy", type: :failed)
      end
    end
  end
end

# Usage in Guardfile
guard :custom_deploy, environment: 'staging', cmd: 'cap' do
  watch(%r{^config/deploy/})
  watch('Capfile')
  watch('Gemfile')
  watch('VERSION')
end

File System Event Handling

Understand how Guard interacts with file system events and implement efficient watching strategies.

Low-Level File System Monitoring

# Using Listen gem directly (Guard's underlying technology)
require 'listen'

# Basic file listener
listener = Listen.to('app', 'lib', 'spec') do |modified, added, removed|
  puts "Modified: #{modified}" unless modified.empty?
  puts "Added: #{added}" unless added.empty?
  puts "Removed: #{removed}" unless removed.empty?

  # Custom logic based on file types
  ruby_files = (modified + added).select { |file| file.end_with?('.rb') }
  unless ruby_files.empty?
    puts "Ruby files changed: #{ruby_files}"
    system('bundle exec rubocop #{ruby_files.join(" ")}')
  end

  # Handle specific file patterns
  test_files = ruby_files.select { |file| file.include?('_test.rb') || file.include?('_spec.rb') }
  unless test_files.empty?
    puts "Test files changed, running tests..."
    system('bundle exec rake test')
  end
end

# Configure listener options
listener = Listen.to('app') do |modified, added, removed|
  # Handle changes
end

# Listener configuration
listener.ignore(/\.git/)                    # Ignore .git directory
listener.ignore(/node_modules/)             # Ignore node_modules
listener.only(/\.rb$/)                      # Only watch .rb files
listener.latency(0.5)                       # Set polling interval
listener.wait_for_delay(2)                  # Batch changes for 2 seconds

listener.start  # Non-blocking
sleep           # Keep the script running

# File watcher with custom logic
class CustomFileWatcher
  def initialize(paths, options = {})
    @paths = Array(paths)
    @options = {
      latency: 1.0,
      ignore: [/\.git/, /\.log$/],
      only: [/\.rb$/]
    }.merge(options)
  end

  def start
    @listener = Listen.to(*@paths, @options) do |modified, added, removed|
      handle_changes(modified, added, removed)
    end

    @listener.start
    puts "File watcher started for: #{@paths.join(', ')}"
  end

  def stop
    @listener&.stop
    puts "File watcher stopped"
  end

  private

  def handle_changes(modified, added, removed)
    all_changed = modified + added

    # Group files by type
    changes_by_type = all_changed.group_by { |file| categorize_file(file) }

    changes_by_type.each do |type, files|
      case type
      when :ruby_lib
        run_related_tests(files)
        check_code_quality(files)
      when :ruby_test
        run_specific_tests(files)
      when :config
        reload_configuration
      when :assets
        recompile_assets(files)
      end
    end

    handle_removals(removed) unless removed.empty?
  end

  def categorize_file(file)
    case file
    when /^lib\/.*\.rb$/
      :ruby_lib
    when /_spec\.rb$|_test\.rb$/
      :ruby_test
    when /^config\//
      :config
    when /^app\/assets\//
      :assets
    else
      :other
    end
  end

  def run_related_tests(files)
    test_files = files.map { |f| f.gsub(/^lib\//, 'spec/').gsub(/\.rb$/, '_spec.rb') }
    existing_tests = test_files.select { |f| File.exist?(f) }

    unless existing_tests.empty?
      puts "Running related tests: #{existing_tests.join(', ')}"
      system("bundle exec rspec #{existing_tests.join(' ')}")
    end
  end

  def run_specific_tests(files)
    puts "Running specific tests: #{files.join(', ')}"
    system("bundle exec rspec #{files.join(' ')}")
  end

  def check_code_quality(files)
    puts "Checking code quality for: #{files.join(', ')}"
    system("bundle exec rubocop #{files.join(' ')}")
  end

  def reload_configuration
    puts "Configuration changed - consider restarting application"
  end

  def recompile_assets(files)
    puts "Assets changed: #{files.join(', ')}"
    system('bundle exec rake assets:precompile')
  end

  def handle_removals(files)
    puts "Files removed: #{files.join(', ')}"
    # Could trigger cleanup tasks, update indices, etc.
  end
end

# Usage
watcher = CustomFileWatcher.new(['app', 'lib', 'spec'])
watcher.start

Integration with Development Workflows

Integrate Guard with popular development tools and workflows for a seamless development experience.

Complete Development Workflow Integration

# Comprehensive Guardfile for Rails application
# Clear terminal on Guard start
clearing :on

# Notification settings
notification :notifysend if RUBY_PLATFORM.include?('linux')
notification :terminal_notifier if RUBY_PLATFORM.include?('darwin')

# Interactor for Guard commands
interactor :simple

# Global Guard settings
scope group: :red_green_refactor

# Testing with RSpec
group :red_green_refactor do
  guard :rspec,
        cmd: 'bundle exec rspec',
        all_after_pass: true,
        all_on_start: false,
        failed_mode: :keep,
        run_all: { cmd: 'bundle exec rspec --profile' } do

    # Watch spec files
    watch(%r{^spec/.+_spec\.rb$})
    watch('spec/spec_helper.rb') { 'spec' }
    watch('spec/rails_helper.rb') { 'spec' }

    # Rails app watchers
    watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
    watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
    watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }

    # Rails controllers
    watch(%r{^app/controllers/(.+)_(controller)\.rb$}) do |m|
      [
        "spec/routing/#{m[1]}_routing_spec.rb",
        "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb",
        "spec/acceptance/#{m[1]}_spec.rb"
      ]
    end

    # Rails models
    watch(%r{^app/models/(.+)\.rb$}) do |m|
      ["spec/models/#{m[1]}_spec.rb"]
    end

    # Rails views
    watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) do |m|
      "spec/features/#{m[1]}_spec.rb"
    end
  end
end

# Code quality
group :quality do
  guard :rubocop,
        all_on_start: false,
        keep_failed: false,
        notification: true do
    watch(%r{.+\.rb$})
    watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
  end

  # Custom guard for complexity analysis
  guard :shell do
    watch(%r{^app/(.+)\.rb$}) do |m|
      complexity_check(m[0])
    end

    def complexity_check(file)
      puts "Analyzing complexity for #{file}"
      result = `flog #{file} 2>/dev/null`

      unless result.empty?
        complexity = result.lines.first.match(/(\d+\.\d+)/)&.captures&.first&.to_f
        if complexity && complexity > 25
          puts "⚠️  High complexity detected in #{file}: #{complexity}"
          system("notify-send 'High Complexity' 'Check #{file}' -u critical") if RUBY_PLATFORM.include?('linux')
        end
      end
    end
  end
end

# Asset pipeline
group :assets do
  guard :sass,
        input: 'app/assets/stylesheets',
        output: 'app/assets/stylesheets',
        style: :expanded,
        sourcemap: :file do
    watch(%r{^app/assets/stylesheets/(.+\.s[ac]ss)$})
  end

  guard :coffeescript,
        input: 'app/assets/javascripts',
        output: 'app/assets/javascripts',
        bare: false,
        shallow: false do
    watch(%r{^app/assets/javascripts/(.*\.(?:coffee|litcoffee|coffee\.md))$})
  end
end

# Database and configuration
group :maintenance do
  guard :bundler do
    watch('Gemfile')
  end

  guard :shell do
    # Database migrations
    watch(%r{^db/migrate/(.+)\.rb$}) do |m|
      puts "New migration: #{m[1]}"

      if UI.agree("Run migration? (y/n)")
        puts "Running migration..."
        result = system('bundle exec rake db:migrate')

        if result
          system('bundle exec rake db:test:prepare')
          puts "✅ Migration completed and test database updated"
        else
          puts "❌ Migration failed"
        end
      end
    end

    # Configuration changes
    watch(%r{^config/(.+)\.yml$}) do |m|
      puts "Configuration changed: #{m[1]}"
      puts "Consider restarting the application server"
    end

    # Routes changes
    watch('config/routes.rb') do
      puts "Routes changed - running route check"
      system('bundle exec rake routes > tmp/routes.txt')
      puts "Routes saved to tmp/routes.txt"
    end
  end
end

# Development server integration
guard :shell do
  watch('Gemfile.lock') do
    puts "Dependencies changed - restart your server if needed"
  end

  watch('config/application.rb') do
    puts "Application configuration changed - server restart required"
  end

  watch(%r{^config/environments/}) do |m|
    puts "Environment configuration changed: #{m[0]}"
    puts "Server restart recommended"
  end
end

# Custom development helpers
guard :shell do
  # API documentation generation
  watch(%r{^app/controllers/api/}) do
    puts "API controller changed - regenerating documentation"
    system('bundle exec rake api:docs:generate')
  end

  # Model documentation
  watch(%r{^app/models/(.+)\.rb$}) do |m|
    puts "Model changed: #{m[1]} - updating schema documentation"
    system('bundle exec annotate --models')
  end

  # Security checks
  watch('Gemfile.lock') do
    puts "Checking for security vulnerabilities..."
    system('bundle audit check --update')
  end
end

Performance Optimization and Tips

Optimize Guard performance for large projects and configure efficient file watching strategies.

Performance Best Practices

# Performance-optimized Guardfile
# Optimize Guard for large projects

# Use groups to control what runs when
scope group: :primary

# Ignore unnecessary directories
ignore %r{^coverage/}
ignore %r{^log/}
ignore %r{^tmp/}
ignore %r{^vendor/}
ignore %r{^node_modules/}
ignore %r{^\.git/}

# Optimize RSpec for speed
guard :rspec,
      cmd: 'bundle exec rspec',
      all_after_pass: false,      # Don't run all tests after each pass
      all_on_start: false,        # Don't run all tests on start
      failed_mode: :keep,         # Keep failed tests
      run_all: { cmd: 'bundle exec rspec --fail-fast' } do  # Fail fast in run_all

  # Use more specific watchers to reduce unnecessary runs
  watch(%r{^spec/.+_spec\.rb$})

  # Only watch lib files that have corresponding specs
  watch(%r{^lib/(.+)\.rb$}) do |m|
    spec_file = "spec/lib/#{m[1]}_spec.rb"
    spec_file if File.exist?(spec_file)
  end

  # Batch related files
  watch(%r{^app/models/(.+)\.rb$}) do |m|
    [
      "spec/models/#{m[1]}_spec.rb",
      "spec/factories/#{m[1].pluralize}.rb"
    ].select { |f| File.exist?(f) }
  end
end

# Efficient asset watching
group :assets, halt_on_fail: true do
  # Only watch specific asset types
  guard :sass,
        input: 'app/assets/stylesheets',
        output: 'public/assets',
        style: :compressed do

    # Only watch main SCSS files, not partials
    watch(%r{^app/assets/stylesheets/(?!.*/_)(.+\.scss)$})
  end
end

# Conditional guards based on project size
if Dir['spec/**/*_spec.rb'].size > 1000
  # Large test suite - more conservative approach
  guard :rspec,
        all_after_pass: false,
        all_on_start: false do
    # Limited watchers for large projects
    watch(%r{^spec/.+_spec\.rb$})
  end
else
  # Small test suite - more aggressive approach
  guard :rspec,
        all_after_pass: true,
        all_on_start: true do
    # Full watchers for small projects
    watch(%r{^spec/.+_spec\.rb$})
    watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
    watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
  end
end

# Efficient file system monitoring configuration
def configure_listener
  Listen.to('.', latency: 1, wait_for_delay: 5) do |modified, added, removed|
    # Batch process changes
    all_changes = modified + added + removed

    # Group by type for efficient processing
    by_extension = all_changes.group_by { |f| File.extname(f) }

    # Process in batches
    by_extension.each do |ext, files|
      case ext
      when '.rb'
        process_ruby_files(files)
      when '.scss', '.sass'
        process_style_files(files)
      when '.js', '.coffee'
        process_script_files(files)
      end
    end
  end
end

# Memory usage optimization
class OptimizedGuard
  def self.setup
    # Garbage collection optimization
    GC.start(full_mark: true, immediate_sweep: true)

    # Reduce memory usage
    ObjectSpace.each_object(String, &:freeze)

    # Monitor memory usage
    Thread.new do
      loop do
        sleep 60
        memory_usage = `ps -o rss= -p #{Process.pid}`.to_i / 1024
        puts "Memory usage: #{memory_usage}MB" if memory_usage > 100

        # Force GC if memory usage is high
        GC.start if memory_usage > 200
      end
    end
  end
end

# Initialize optimizations
OptimizedGuard.setup

# Custom efficient file categorization
module FileHelper
  RUBY_EXTENSIONS = %w[.rb .rake].freeze
  ASSET_EXTENSIONS = %w[.scss .sass .css .js .coffee].freeze
  CONFIG_EXTENSIONS = %w[.yml .yaml .json].freeze

  def self.categorize_fast(file)
    ext = File.extname(file)

    return :ruby if RUBY_EXTENSIONS.include?(ext)
    return :asset if ASSET_EXTENSIONS.include?(ext)
    return :config if CONFIG_EXTENSIONS.include?(ext)

    # Fast path-based categorization
    case file
    when /^spec\//
      :test
    when /^app\/(models|controllers|helpers)\//
      :app_core
    when /^lib\//
      :library
    else
      :other
    end
  end
end

Best Practices

Guard Best Practices

  • Use groups to organize guards by concern and control execution scope
  • Optimize file watching patterns to avoid unnecessary executions
  • Configure appropriate notifications for your development environment
  • Ignore irrelevant directories like logs, tmp, and vendor folders
  • Use conditional guards based on environment and project size
  • Batch file changes to avoid running tasks on every single file change
  • Monitor performance and memory usage for large projects
  • Create custom guards for project-specific automation needs

💡 Pro Tips

  • Use guard --no-interactions to run Guard without the interactive shell
  • Create separate Guardfiles for different environments (Guardfile.development, etc.)
  • Use Guard's callback system to chain complex workflows
  • Combine Guard with LiveReload for instant browser updates during development
  • Use Guard groups to run only specific sets of watchers with guard -g group_name

🚀 Next Steps

Explore CLI tools development to create powerful command-line applications and complete your Ruby development toolkit.