Hibariya

Thorで簡単にコマンドラインアプリをつくる

Thorをご存知の方は多いと思いますが、Retterを作るにあたってとても便利に使えたので、手短に紹介したいと思います。 といってもだいたいはWikiに書いてあることしか書けないんですが、何しろ英語ですし、さわりだけでも伝えられたらなと思います。

Thor(トール、ソアー?)は便利なコマンドラインツールで、これを使うとコマンドラインオプションのパーズやサブコマンドごとのhelpをつくるなどの面倒な作業を簡単にこなせ、手早くビルドツールや実行可能なコマンドを作成できます。 特殊なDSLを使わずメソッドを定義することで処理を記述するため、テストしやすいという特徴もあります。

より便利なRakeとして使う

便利なRakeというのは主に引数とオプションの扱い方のことです。 Rakeは今も現役で便利に使っているんですが、例えば引数を渡したいときは環境変数として渡さないといけなくて、 これが割と面倒なのでした。

こんなふうに。

  $ TO=alice rake greeting:deliver

Thorだともう少し自然に書くことができるようになります。

オプションを渡す

  $ thor greeting:deiver --to alice

TO=aliceをコマンドの後ろに--to aliceと書けて少し見やすくなりました。

実際のタスクの書き方は以下のようになります。ファイル名はgreeting.thorのようにクラス名.thorとし、タスク名と同名のメソッドを定義します。 タスクがひとつのメソッドとして定義されていると、テストがとても書きやすそうです。

  class Greeting < Thor
    desc 'deliver', 'deliver greeting message' # タスクの説明
    method_options to: :string                 # 直後に定義するタスクのオプション
    def deliver                                # タスクの定義
      puts "sending greeting to #{options[:to]}"
    end
  end

methodoptions には、オプション名をキーにしたハッシュを渡すことができます。要素には型を表すシンボルを指定します。 渡されたオプションは`options[:optionname]`のようにアクセスすることができます。

引数を渡す

  $ thor greeting:deliver alice

というふうに書けるようにもなります。

コマンドの引数は、以下のようにメソッドの引数で受け取ることができます。

  class Greeting < Thor
    desc 'deliver', 'deliver greeting message'
    def deliver(to)
      puts "sending greeting to #{to}"
    end
  end

便利ですね。

そういえば--helpをつけると、タスクの一覧を表示できます。rake -T相当のあれです。

  $ thor greeting --help
  Tasks:
    thor greeting:deliver      # deliver greeting message
    thor greeting:help [TASK]  # Describe available tasks or one specific task

今更ですが、今回のサンプルコードのGreetingは割と適当なプロダクトなので、目的に応じて適宜読み替えてください。

独立したコマンドとして使う

Rakeのような使い方ではなく、単体で実行可能なコマンドをつくることもできます。

  #!/usr/bin/env ruby
  # coding: utf-8

  require 'thor'

  class Greeting < Thor
    desc 'deliver', 'deliver greeting message'
    def deliver(to)
      puts "sending greeting to #{to}"
    end
  end

  Greeting.start

Thorを継承したクラスの書き方は、先程と同じです。このファイルを実行することになるのでshebangやthorのrequireが必要です。 これをgreetingとか適当な名前のファイルにして、実行属性をつければ独立したコマンドになります。ポイントはGreeting.startです。

  $ chmod +x greeting
  $ ./greeting deiver alice

もちろんさきほどの--helpも使えます。ヘルプが出ると一気にちゃんとしたコマンドっぽくなりますね。

Railsのジェネレータのように使う

Thor::Actionsで提供されている便利なメソッドたちを使うことで、Railsでよくみるファイルの自動生成と全く同じようなものがthorで簡単に実装できます。rails newしたときのあれです。

  #!/usr/bin/env ruby
  # coding: utf-8

  require 'thor'
  require 'thor/group'

  class Newgreeting < Thor::Group
    include Thor::Actions

    argument :name             # タスク全体の引数

    def self.source_root       # ファイルのコピー元のベースディレクトリ
      File.dirname(__FILE__)
    end

    def create_templates       # 最初に実行される処理
      %w(title.txt body.txt).each do |fname|
        template "templates/#{fname}", "#{name}/#{fname}"
       end
    end

    def create_readme          # 次に実行される処理
      copy_file 'templates/README', "#{name}/README"
    end

    def complete_message       # 最後に実行される処理
      say 'greeting templates created.', :green
    end
  end

  Newgreeting.start

Thor::Groupを継承した場合は、Thorを継承したときとは違い、そのクラス全体がひとつのタスクとして扱われます。 タスクnewgreetingが呼ばれたとき(↑のコマンドが実行されたとき)、クラスに定義したインスタンスメソッドが定義された順番に実行されるようになります。 ↑の場合はcreate_templates, create_readme, complete_message の順で実行されることになります。

argument :nameは引数です。この引数にはインスタンスメソッドからnameで参照することができます。

templatecopy_fileThor::Actionsからincludeしたメソッドで、これがファイルジェネレータの役割を果たします。 templateはコピー元とコピー先のふたつの引数をとり、コピー元のファイルはERBとして評価されます。 copy_fileは単純にファイルをコピーします。

Newgreeting.source_rootには、Thor::Actions#templatesThor::Actions#copy_fileなどのコピー元のファイルのベースとなるディレクトリ名を指定しています。

このコマンドを実行するとこんな感じになります。

  $ ./newgreeting foo
        create  foo/title.txt
        create  foo/body.txt
        create  foo/README
  greeting templates created.

ちなみに、Newgreeting#complete_messageでさり気なく:greenとか書いていますが、これで緑色の文字が標準出力に表示されます。手軽に色をつけられるのは便利です。

コンフリクト時の動作

このコマンドがファイルを生成するとき、すでに同名のファイルがある場合の動きはこんな感じになります。

  $ ./newgreeting foo
        create  foo/title.txt
     identical  foo/body.txt  # 同名で同じ内容
      conflict  foo/README    # 同名で違う内容
  Overwrite /path/to/foo/README? (enter "h" for help) [Ynaqdh]

identicalは既に同名のファイルがあるけど内容が同じなのでスキップされます。conflictは内容が違うから上書きするかどうかを訊いてきています。 conflictしたときは差分を表示したりはできますが、マージはできないみたいですね。それにしても、ここまでの機能を数行で記述できてしまうthorは大変魅力的に見えるのではないでしょうか。

さらに多くの情報

よく使いそうな機能を中心にざっくりと紹介してみましたが、他にも便利で魅力的な機能がたくさんあります。 より詳しくはthorとかbundle open thorなどで。

謝辞

けっこう前にThorのことを教えてくれたursm先生。