Adding custom Thor commands to your Rails app
Thor is a really nice command-line runner, with much nicer support for arguments than Rake imo. The Rails framework supports both, but since Rails 5.1 most Rails’ built-in commands (like bin/rails generate) use Thor, and it’s very simple to add custom Thor-based commands to gems and engines too, which Rails will auto-discover when placed in the engine’s lib/commands directory.
The one place Thor commands don’t work out of the box is your Rails application. Rails ships with a lib/tasks directory, but not a lib/commands directory. But when you create that directory, and add your custom command to it, it still won’t work.
There’s a simple fix to make lib/commands work and have Rails auto-discover your app’s custom Thor commands: add your app’s lib directory to the Ruby load path in your app’s config/boot.rb:
# config/boot.rb
# ...
$LOAD_PATH << File.expand_path("../lib", __dir__) # Discover Rails commands in lib/commands.
And then you can simply add some custom Thor commands like so:
# lib/commands/foobar_command.rb
class FoobarCommand < Rails::Command::Base
desc "foobar", "Prints foobar"
def perform(*)
boot_application! # optional, like a Rake task's => :environment dependency
say "foobar"
end
end
Why?! Patching boot.rb sucks
It is unfortunate! Here’s why it’s necessary for this:
When you invoke a command like bin/rails foobar, Rails will attempt to find a matching commands/foobar_command.rb within Ruby’s $LOAD_PATH. If that file exists in the load path, the file is loaded and the command invoked. If not, you’ll see Unrecognized command "foobar" (Rails::Command::UnrecognizedCommandError).
But, Rails does this command lookup before fully loading your Rails application (because that’s slow, I presume). And by recent convention, Rails does not add lib to the Ruby $LOAD_PATH until your application has been loaded. Bit of a problem, in this context. And that’s why the patch adds lib to the load path.
There’s got to be a better way
I made a PR to upstream Rails to try to make lib/commands work out of the box. It’s neat because the change I proposed first does the fast-path to resolve the command without loading the application, and then if it hasn’t found the command, it loads the application (adding lib to the load path) and tries to look up the command again.
There’s also another funny/interesting thing I discovered while digging into this: Rails only loads the first matching command. So if you have the aaaaaaa gem and it has a lib/rails/commands/generate/generate_command.rb earlier in Ruby’s $LOAD_PATH than Rails’ own matching file, then Rails will never load its built-in generate command. Bit of a problem! This seems like an artifact of pre-Ruby 1.9 that didn’t store the expanded canonical path of loaded features (e.g. require "feature"). I have a PR to change that too.