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
で参照することができます。
template
やcopy_file
はThor::Actions
からincludeしたメソッドで、これがファイルジェネレータの役割を果たします。
template
はコピー元とコピー先のふたつの引数をとり、コピー元のファイルはERBとして評価されます。
copy_file
は単純にファイルをコピーします。
Newgreeting.source_root
には、Thor::Actions#templates
やThor::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先生。