はじめに
thor は Ruby で CLI ツールを作るときによく使われる gem で、サブコマンドに対応しているなど、機能が充実しています。
Thor as a Task Runner
最近、thor をタスクランナーとして Rake の代わりに使うこともできる、と知りました。
この記事では、Rake に対するメリットや、thor をタスクランナーとして使うための機能について、調べたことや試したことを載せています。
使い方
下の記事に割とよくまとまっています。
Thorfile
Rakefile に相当するものです。
中身としては、Thor を継承した CLI クラスを記述します。
単純なものだと下のような感じ:
class Foo < Thor
desc 'foo', 'Say foo'
def foo
say 'foo'
end
end
実行例は以下です。
% thor -T
foo
---
thor foo:foo # Say foo
% thor foo:foo
foo
#say
は Thor::Shell::Basic で定義されています。
上の例なら puts
でいいのですが、#say
だと色を付けたりもできます。
Thor::Shell::Basic の他の機能については、後半でもう少し紹介します。
上のファイルは foo.thor
として、カレントディレクトリや lib/tasks/
以下に置いても動作します。
参考記事
本家の Wiki にいくつか関係する記事があります:
Rake に対する利点
まとめると以下になりそうです:
- 引数の取り扱いが楽になる。コマンド引数もオプション形式もふつうに使える。
- 構造化しやすい。ネームスペースを class や module 単位で分割できる。
- テストコードが書きやすい。
上の記事にもありましたが、Rake のペインポイントとして「タスクに引数を渡したいときにつらい」というのがあるようで、thor ではシンプルに関数引数や、オプションの形で扱えます。
あと、thor であればテストコードが書きやすいとか、構造化しやすいという意見があります。
@key_amb rubyスクリプト化するにしても、ふつうに getopt でオプション処理するか、サブコマンド欲しいなら thor 使うかにしたほうが rake より綺麗な構造化されたコードが書きやすいです
— そのっつ (Naotoshi Seo) (@sonots) 2016年9月10日
Rake だと全てのコードがグローバルスコープに置かれるから、名前の競合がつらい、という話も聞いたことがあります。
経験が浅いので、実際どのくらいまずいのか肌感覚ではわかりませんが、タスク用の関数をあちこちに書くようになったら危険なのかな、などと想像しています。
求む知見。
便利な機能
Thor::Actions
コマンド実行やファイル・ディレクトリ操作を提供してくれます。
Thor を継承した class で include して利用します。
よく使いそうだな、と思ったメソッドは #inside
と #run
です。
-
#inside
は指定したディレクトリ配下で、ブロックで渡した Ruby コードを実行してくれます。 -
#run
はシェルコマンドを実行します。:capture
オプションの有無によって、バッククォートまたはsystem
メソッドでコマンドを実行します。
以下に、サンプルコードを示します。
class App < Thor
include Thor::Actions
class_option :verbose, aliases: 'v', type: :boolean, default: false
desc 'work', 'Sample command in workdir'
def work
inside 'workdir', verbose: options[:verbose] do
pwd = run 'pwd', capture: true # `pwd`
say "PWD: #{pwd}"
ret = run 'false' # system('false')
say "RESULT: #{ret}"
end
end
end
実行すると、下のようになります。
% thor app:work -v
inside workdir
run pwd from "./workdir"
PWD: /home/quiche/my/repos/tutorials/thor/action/workdir
run false from "./workdir"
RESULT: false
その他の使い方については、Thor::Actions の RubyDoc を確認ください。
参考:
Thor::Shell::Basic
端末に対する入出力や、対話操作用の便利な機能を提供してくれます。
便利そうだな、と思ったのは #ask
, #yes?
, #no?
, #say
あたり。
#say
については上の方でも触れました。
-
#ask
は端末にテキストを表示して、ユーザからの入力を受け取ります。 -
#yes?
,#no?
は#ask
を wrap して、それぞれ入力がy|yes
,n|no
なら true, でなければ false となるものです。
サンプルコードを以下に示します:
class App < Thor
desc 'dialog', 'Sample dialog'
def dialog name='...'
say "Hello, #{name}", :bold
if yes? 'Do you know how useful thor is?', :bold
say 'Wondeful!', [:magenta, :bold]
else
say %(It's a shame.), [:blue, :bold]
end
say ''
answer = ask 'Then, tell me the name of who you fell in love with for the first time:', :bold
if answer.length.nonzero?
say "#{answer} ... OK. I remember.", [:magenta, :bold]
else
say %(OK. I'll ask you again later), [:blue, :bold]
end
say ''
say 'Bye!', :bold
end
end
実行すると下のようになります。
% thor app:dialog MyName
Hello, MyName
Do you know how useful thor is? y
Wondeful!
Then, tell me the name of who you fell in love with for the first time: Oh, yes!
Oh, yes! ... OK. I remember.
Bye!
% thor app:dialog
Hello, ...
Do you know how useful thor is? NOOOOO
It's a shame.
Then, tell me the name of who you fell in love with for the first time:
OK. I'll ask you again later
Bye!
(※本当は色が付いたりします。)
その他詳細については、RubyDoc を参照して下さい。
結びに
試しながら使っているところですが、タスクに柔軟に引数を渡したい場合には使いやすそうです。
成熟度としては Rake や Capistrano ほどではないかもしれませんが、それなりに便利な機能も揃っていると思います。