Thor をタスクランナーとして使うために調べたこと

# はじめに
[thor](https://github.com/erikhuda/thor) は Ruby で CLI ツールを作るときによく使われる gem で、サブコマンドに対応しているなど、機能が充実しています。

# Thor as a Task Runner

最近、thor をタスクランナーとして Rake の代わりに使うこともできる、と知りました。

この記事では、Rake に対するメリットや、thor をタスクランナーとして使うための機能について、調べたことや試したことを載せています。

## 使い方

下の記事に割とよくまとまっています。

- [Thor入門 - RakeからThorへ - - Qiita](http://qiita.com/yatmsu/items/5ba7016db33c13d7a882)

### Thorfile

Rakefile に相当するものです。

中身としては、Thor を継承した CLI クラスを記述します。
単純なものだと下のような感じ:

```ruby:Thorfile
class Foo < Thor
  desc 'foo', 'Say foo'
  def foo
    say 'foo'
  end
end
```

実行例は以下です。

```bash
% thor -T
foo
---
thor foo:foo  # Say foo

% thor foo:foo
foo
```

`#say` は [Thor::Shell::Basic](http://www.rubydoc.info/github/wycats/thor/master/Thor/Shell/Basic) で定義されています。
上の例なら `puts` でいいのですが、`#say` だと色を付けたりもできます。
Thor::Shell::Basic の他の機能については、後半でもう少し紹介します。

上のファイルは `foo.thor` として、カレントディレクトリや `lib/tasks/` 以下に置いても動作します。

### 参考記事

本家の Wiki にいくつか関係する記事があります:

- [Invocations · erikhuda/thor Wiki](https://github.com/erikhuda/thor/wiki/Invocations)
- [Namespaces · erikhuda/thor Wiki](https://github.com/erikhuda/thor/wiki/Namespaces)

## Rake に対する利点

まとめると以下になりそうです:

- 引数の取り扱いが楽になる。コマンド引数もオプション形式もふつうに使える。
- 構造化しやすい。ネームスペースを class や module 単位で分割できる。
- テストコードが書きやすい。

上の記事にもありましたが、Rake のペインポイントとして「タスクに引数を渡したいときにつらい」というのがあるようで、thor ではシンプルに関数引数や、オプションの形で扱えます。

あと、thor であればテストコードが書きやすいとか、構造化しやすいという意見があります。

- [Replace Rake with Thor – [codecrate]](http://codecrate.com/2014/01/replace-rake-with-thor.html)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/key_amb">@key_amb</a> rubyスクリプト化するにしても、ふつうに getopt でオプション処理するか、サブコマンド欲しいなら thor 使うかにしたほうが rake より綺麗な構造化されたコードが書きやすいです</p>&mdash; そのっつ (Naotoshi Seo) (@sonots) <a href="https://twitter.com/sonots/status/774497835421044736">2016年9月10日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

Rake だと全てのコードがグローバルスコープに置かれるから、名前の競合がつらい、という話も聞いたことがあります。
経験が浅いので、実際どのくらいまずいのか肌感覚ではわかりませんが、タスク用の関数をあちこちに書くようになったら危険なのかな、などと想像しています。
求む知見。

## 便利な機能

### Thor::Actions

コマンド実行やファイル・ディレクトリ操作を提供してくれます。
Thor を継承した class で include して利用します。


よく使いそうだな、と思ったメソッドは `#inside` と `#run` です。

- `#inside` は指定したディレクトリ配下で、ブロックで渡した Ruby コードを実行してくれます。
- `#run` はシェルコマンドを実行します。`:capture` オプションの有無によって、バッククォートまたは `system` メソッドでコマンドを実行します。

以下に、サンプルコードを示します。

```ruby:Thorfile
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
```

実行すると、下のようになります。

```bash
% 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](http://www.rubydoc.info/github/wycats/thor/master/Thor/Actions) を確認ください。

参考:

- [Actions · erikhuda/thor Wiki](https://github.com/erikhuda/thor/wiki/Actions)

### Thor::Shell::Basic

端末に対する入出力や、対話操作用の便利な機能を提供してくれます。
便利そうだな、と思ったのは `#ask`, `#yes?`, `#no?`, `#say` あたり。
`#say` については上の方でも触れました。

- `#ask` は端末にテキストを表示して、ユーザからの入力を受け取ります。
- `#yes?`, `#no?` は `#ask` を wrap して、それぞれ入力が `y|yes`, `n|no` なら true, でなければ false となるものです。

サンプルコードを以下に示します:

```ruby:Thorfile
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](http://www.rubydoc.info/github/wycats/thor/master/Thor/Shell/Basic) を参照して下さい。

# 結びに

試しながら使っているところですが、タスクに柔軟に引数を渡したい場合には使いやすそうです。
成熟度としては Rake や Capistrano ほどではないかもしれませんが、それなりに便利な機能も揃っていると思います。