11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RailsAdvent Calendar 2020

Day 9

rails コマンドへ独自コマンドを組み込む方法

Last updated at Posted at 2020-12-08

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 チュートリアルでは UserMicropost の二つのモデルが登場しますので、本記事もこの2つのモデルを操作してみたいと思います。

Rails::Command::Base

早速、独自コマンドを作成していきます。独自コマンドは、直接 Thor を継承せずに、Rails が Thor をラップしたクラス Rails::Command::Base を提供しているので、このクラスを継承するようにします。

早速、独自コマンドを実装しましょう。以下のような main_command.rb ファイルを作成し、このファイルに独自コマンドを実装していきます。

lib/commands/main/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 を修正して、独自コマンドを組み込みます。次のように修正します。

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.rbrequire_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 を修正してサブコマンドを追加します。

lib/commands/main/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.rbmicropost_command.rb を読み込む必要があります。クラスを読み込んだ後、Thor の subcommand 命令で usermicropost という2つのサブコマンドを追加しています。

user サブコマンドの実体 user_command.rb は次のように実装します。

lib/commands/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 を次のように実装します。

lib/commands/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:helpbin/rails sample_app:user help など。
    • 理由は標準の help コマンドが Rails エンジンしかサポートしてない。Rails アプリケーションは全く考慮されていない。
    • 改善方法としては ApplicationCommand クラスで help コマンドを独自実装するのが良いのかなと考えています。
11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?