Rake タスクって何だか変ですよね。テストも書きづらいし、できることなら書きたくないですよね。
Ruby には Thor というイケてる Gem があり、これを利用するとイケてるコマンドライン・ユーティリティを書くことができます。Thor はイケてるので、サブコマンドも書くことができます。Thor を利用する場合、コマンドライン・ユーティリティは、Thor クラスを継承したクラスとして作成するので、テストも簡単で、特別な知識は不要です。
Rake タスクではなくて、Thor を利用できたら素敵ですよね。
実は rails コマンドは Thor をすでに利用しているんです。そして、Rails エンジンの場合、Thor を使って独自コマンドを提供する標準的な方法が用意されているようですが、Rails アプリケーションの場合、標準的な方法はありません。自分でなんとかするしかありません。
以降では自分でなんとかする方法を説明します。generator のアシストは受けられないので、全て手作業でファイルを修正したり、ファイルを作成したりする必要があります。
前提
本記事で作成したサンプルは https://github.com/sunny4381/rails_command_extension に置いておきます。
このサンプルは Rails チュートリアルの第14章のソースコードを元にしています。Rails チュートリアルでは User
と Micropost
の二つのモデルが登場しますので、本記事もこの2つのモデルを操作してみたいと思います。
Rails::Command::Base
早速、独自コマンドを作成していきます。独自コマンドは、直接 Thor を継承せずに、Rails が Thor をラップしたクラス Rails::Command::Base を提供しているので、このクラスを継承するようにします。
早速、独自コマンドを実装しましょう。以下のような main_command.rb
ファイルを作成し、このファイルに独自コマンドを実装していきます。
require "rails/command"
module SampleApp
module Command
class MainCommand < Rails::Command::Base
namespace "sample"
@command_name = "sample"
def hello
say "hello"
end
end
end
end
namespace
と @command_name
を指定して、コマンド名を明示的に指定しています。この 2 つの指定がなければ sample_app:main
なんていう冗長なコマンド名になってしまいます。
そして、hello
というコマンドを実装しており、bin/rails sample_app:hello
と実行することを意図しています。
コマンドの実装方法の詳細については、Thor の Wiki を参照ください。
bin/rails の変更
次に独自コマンドを rails コマンドに認識させる必要があります。このため bin/rails
を修正して、独自コマンドを組み込みます。次のように修正します。
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
#### ↓↓↓↓↓↓↓↓追加
# 独自コマンドの組み込み
require_relative '../lib/commands/main/main_command'
#### ↑↑↑↑↑↑↑↑追加
# run rails command
require 'rails/commands'
独自コマンドを実装したファイル lib/commands/main/main_command.rb
を require_relative
で読み込んでいます。独自コマンドを rails コマンドのコマンド一覧へ登録する処理が Rails::Command::Base
にありますので、読み込むだけで rails コマンドに登録されます。
試しに bin/rails
を実行してみます。
$ bin/rails
...
routes
runner
sample_app:hello
secret
secrets:edit
...
多数のコマンドが出力されるので少しわかりづらいですが、rails の標準コマンドに混じって sample_app:hello
と独自コマンドが表示されています。
試しに独自コマンドを実行してみます。
$ bin/rails sample_app:hello
hello
モデルの操作とサブコマンド
コンソールに文字列を表示するような単純な処理ならこのままでも問題ありませんが、Rails アプリケーションが初期化されていないので、モデルを検索したり、作成したり、削除したりすることはできません。
Rails アプリケーションの初期化方法と合わせて、モデルを作成する独自コマンドを sample_app
のサブコマンドとして追加する方法をみていきます。
まず、main_command.rb
を修正してサブコマンドを追加します。
require "rails/command"
require_relative '../user/user_command'
require_relative '../micropost/micropost_command'
module SampleApp
module Command
class MainCommand < Rails::Command::Base
namespace "sample_app"
@command_name = "sample_app"
subcommand "user", SampleApp::Command::UserCommand
subcommand "micropost", SampleApp::Command::MicropostCommand
end
end
end
Rails アプリケーションが初期化されていないので、クラスのオートロードは効きません。require_relative
を用いて明示的に user_command.rb
と micropost_command.rb
を読み込む必要があります。クラスを読み込んだ後、Thor の subcommand
命令で user
と micropost
という2つのサブコマンドを追加しています。
user
サブコマンドの実体 user_command.rb
は次のように実装します。
module SampleApp
module Command
class UserCommand < Rails::Command::Base
desc "list", "list users."
def list
require_application_and_environment!
say
say "#{'Name'.ljust(14)} #{'Email'.ljust(32)} Updated At"
say "-" * 80
User.all.each do |user|
say "#{user.name.ljust(14)} #{user.email.ljust(32)} #{user.updated_at.iso8601}"
end
end
end
end
end
user_command.rb
では、ユーザー一覧を表示する list
というコマンドを定義しています。このコマンドは bin/rails sample_app:user list
と実行することを意図しています。
list
の先頭で require_application_and_environment!
を呼び出し Rails アプリケーションを初期化し、続いて User をデータベースから読み込み、コンソールに出力しています。
なお、Rails アプリケーションの初期化後は、オートロードが効くようになるので、User モデルを明示的に読み込む必要はありません。
micropost
サブコマンドの実体 micropost_command.rb
を次のように実装します。
module SampleApp
module Command
class MicropostCommand < Rails::Command::Base
desc "list", "list microposts."
def list
require_application_and_environment!
say
say "#{'Name'.ljust(14)} #{'Content'.ljust(14)} Created At"
say "-" * 80
Micropost.all.each do |post|
say "#{post.user.name.ljust(14)} #{post.content.ljust(14)} #{post.created_at.iso8601}"
end
end
end
end
end
ほぼ user_command.rb
と同じで、こちらの方は Micropost モデルの一覧をコンソールに出力しています。
サブコマンドを追加できたら試しに実行してみます。
$ bin/rails sample_app:user list
Name Email Updated At
--------------------------------------------------------------------------------
sample sample@example.jp 2020-12-05T05:33:11Z
Rails チュートリアルを少し進め、ユーザーを登録したら、上のように出力されます。
rails コマンドのその他の実行方法
単に rails
と実行した場合も bundle exec rails
などと実行した場合も bin/rails
ファイルが実行されますので、bin/rails sample_app:user list
に代えて bundle exec rails sample_app:user list
と実行することもできます。
要検討・改善点など
- Rails の作法にならって
Rails::Command::Base
を継承したApplicationCommand
というクラスを作成し、ApplicationCommand
クラスを継承するようにした方が良いのかも? -
help
コマンドがなからず追加されるが、help
コマンドを実行するとエラーになる。例bin/rails sample_app:help
やbin/rails sample_app:user help
など。- 理由は標準の
help
コマンドが Rails エンジンしかサポートしてない。Rails アプリケーションは全く考慮されていない。 - 改善方法としては
ApplicationCommand
クラスでhelp
コマンドを独自実装するのが良いのかなと考えています。
- 理由は標準の