A Rails script lives in lib/scripts and is run with bin/rails runner lib/scripts/...
. They are a simple tool to perform some one-time actions on your Rails application. A Rails script has a few advantages over pasting some prepared code into a Rails console:
- Version control
- Part of the repository, so you can build on previous scripts for a similar task
- You can have tests (see below)
Although not part of the application, your script is code and should adhere to the common quality standards (e.g. no spaghetti code). However, a script has a few special requirements. Please consider these when writing a Rails script.
Be idempotent
Your script may crash while halfway through its task. Running it again should not break anything, but continue where it left off.
Be atomic
Use database transactions for atomic changes, i.e. changes that must happen together or not at all.
ActiveRecord::Base.transaction do
# Change something
# Another, dependent change
end
Be transparent
The script should always state what it is about to do, and await confirmation from the user. If a decision is needed, it should print all relevant information.
You can copy interaction helpers from Geordi Show archive.org snapshot .
When processing lists of records, print an identifier for each item. This way, you can watch it progress, and you'll know what the script was at in case it crashes.
Be robust
Your script will encounter invalid records, and record.save!
will crash your script. Either use if record.save
, or wrap the respective code like this:
begin
# If using transactions, nest them here
# ... record.save! ...
rescue ActiveRecord::RecordInvalid
# Handle error
end
On error, log the record for manual care.
Consider testing your script
Not every script needs a test. However, sometimes you'll want to add some. Here is how. (Inspired by Functional core, imperative shell Show archive.org snapshot .)
- Structure your script like this:
# This class will be specced # To simplify this, it should not read input class YourScript # Methods offering functionality end unless Rails.env.test? script = YourScript.new # All interactivity (i.e. reading input) goes here end
- Place tests in lib/scripts/spec and call them my_script_spec.rb. This way they're close to their corresponding script, but not included in regular full-app test runs. They can prove correctness of the script, while not breaking your test suite should your script outdate.
- Structure your test like this:
require_relative '../your_script_name' describe 'the purpose of your script' do subject { YourScript.new } it 'has output' do expect { subject.print_summary }.to output(/success/).to_stdout end end