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-interactionsto 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.