Thor入門 - RakeからThorへ -

## Rakeタスクの引数の取り扱いまとめ

RakeタスクはRailsのプロジェクトでよく作るのだけれど、どうも引数の受け渡しが面倒でイライラする。
Rakeタスクで引数を扱うには主に以下の2パターンのやり方があるのだが・・・・。

### 1. 引数を定義する方法

```rb:hoge.rake
task :hoge, [:a, :b] do |task, args|                                                                                                                                                   
  p args.a                                                                                                                                                                                                                
  p args.b                                                                                                                                                                                                                
end
```

```shell-session:実行結果
$ bin/rake hoge[yo,hey]
"yo"
"hey"
$ bin/rake "hoge[yo,hey]" # zsh拡張している場合
"yo"
"hey"
```

実行時に[]を付けたり、自分はzshユーザーなのでタスクを「"」でくくる必要があったりと面倒。
第2引数だけ指定したい場合は bin/rake hoge[,'hey!'] などと微妙な書き方をしなければならず面倒。

### 2. 環境変数を経由する方法

```rb:hoge.rake
task :hoge do |task|                                                                                                                                                                                                        
  p ENV['a']                                                                                                                                                                                                                
  p ENV['b']                                                                                                                                                                                                                
end 
```

```shell-session:実行結果
$ bin/rake hoge a=yo b=hey
"yo"
"hey"
```

実行時に環境変数名を書いてやらねばならず、やはり面倒。

## Rakeタスクの引数がウザいのでThorを使う

ならRakeを使わなければいい。[Thor](https://github.com/erikhuda/thor)を使おう。
Thorはコマンドラインツールを作る為のgemで、引数の受け渡しは当然簡潔に書けるし、オプションのパースやUsage Messageの表示などを簡単に作成でき、おまけにシンタックスがRake-likeなのでRakeを理解しているエンジニアなら違和感なく移行できるCLIツールです。

### インストール

普通にインストールします。

```shell-session
% gem i thor
```

### Thorで書くとどうなるか

先ほどのRakeタスクをThorを使って書くとこうなる。

```rb:test.thor
class Test < Thor
  desc 'hoge [a] [b]', 'parameter test'
  def hoge a, b
    p a
    p b
  end
end
```

問題のパラメータは・・・。

```shell-session:実行結果
$ thor test:hoge yo hey
"yo"
"hey"
```

これだ！こう書きたかったんだ！！２３４５６７８９０

## Thor入門

というわけで引数の問題は解決しましたが、それ以外にも機能が豊富なThorの使い方を解説します。

### 基本

では試しに超基本的なコードを書いてみましょう。
現在のディレクトリにtest.thorという名前のファイルを作成します。内容は以下のとおり。

```rb:test.thor
class Test < Thor # Thorクラスを継承
  desc 'say [Name]', 'say task' # Usage Message, 詳細な説明
  def say(name) # publicで定義したメソッドが実行するタスク
    puts "Hello, #{name}!"
  end
end
```

実行してみましょう。

```shell-session:実行結果
$ thor test:say # パラメータ無しだとUsage Massageを出力
ERROR: "thor say" was called with no arguments
Usage: "thor test:say [NAME]"
$ thor test:say Thor
Hello, Thor!
```

簡単ですね。

### 名前空間

基本を理解出来たら次は名前空間の説明です。
Thorのタスク名はデフォルトだとRubyの名前空間を元に決まります。
これはそのままなので分かりやすいでしょう。

```rb:app.thor
class App < Thor
  desc 'say', '言う'
  def say
    puts 'Yo!'
  end
end
```

```shell-session:app.thor実行結果
$ thor app:say # thor downcaseしたクラス名:メソッド名
Yo!
```

当然moduleをAppの上に追加すると、そのままタスクの名前に追加されます。

```rb:app.thor
module Livesense                                                                                                                                                                                                            
  class App < Thor                                                                                                                                                                                                         
    desc 'say', '言う'                                                                                                                                                                                               
    def say                                                                                                                                                                                                                 
      puts 'Yo!'                                                                                                                                                                                               
    end                                                                                                                                                                                                                     
  end                                                                                                                                                                                                                       
end 
```

```shell-session:app.thor実行結果
$ thor livesense:app:say
Yo!
```

名前空間を変更することもできます。

```rb:app.thor
module Livesense                                                                                                                                                                                                            
  class App < Thor                                                                                                                                                                                                         
    namespace :yo # namespace :任意の名前 で指定
    desc 'say', '言う'                                                                                                                                                                                               
    def say                                                                                                                                                                                                                 
      puts 'Yo!'                                                                                                                                                                                               
    end                                                                                                                                                                                                                     
  end                                                                                                                                                                                                                       
end  
```

```shell-session:app.thor実行結果
$ thor yo:say
Yo!
```

個人的にはnamespaceをあまり多用されると辛くなるかなと思っている。
正しく使えば問題ないのだが。

### 実行可能なコマンドを作る

ThorをRakeタスクの代替として使うのを前提に説明をしてきましたが、実行可能なコマンドも作成可能です。

```rb:yo
#!/usr/bin/env ruby
require 'thor'

class YoCommand < Thor
  desc 'say', '言う'
  def say
    puts 'hey!'
  end
end

YoCommand.start
```

上記のようにSheBangを書いてthorをrequireして、実行権限を付加し、実行します。

```shell-session:実行結果
$ chmod a+x ./yo
$ ./yo say
hey!
```

しかしこれだとパラメータを毎回指定しなければならないので、startメソッドのパラメータに配列でメソッド名を書いておけば実行時にパラメータを省略できます。

```rb:yo
YoCommand.start %w(say)
```

```shell-session:実行結果
$ chmod a+x ./yo
$ ./yo
hey!
```

PATHに通して普通のUNIXコマンドみたいに使うならこんな感じですね。

### メソッドオプション

実行可能なコマンドを作るとなると、今度は実行時にオプションを付けたくなる。
**method_option**を使えばよくあるオプションの挙動を簡単に定義できます。

#### コマンドに実行時オプションを追加する

とりあえずは超基本的なサンプルコードです。

```rb:yo
#!/usr/bin/env ruby                                                                                                                                                                                                         
require 'thor'                                                                                                                                                                                                              
                                                                                                                                                                                                                            
class YoCommand < Thor                                                                                                                                                                                                      
  desc 'say', '言う'                                                                                                                                                                                                        
  method_option :force                                                                                                                                                                                                      
  def say                                                                                                                                                      
    if options.force # options[:force]でもOK
      # optionsはhash形式で値が入っている => {"force"=>"force"}
      puts 'yo!hey!'                                                                                                                                                                                                        
    else                                                                                                                                                                                                                    
      puts 'yo!'                                                                                                                                                                                                            
    end                                                                                                                                                                                                                     
  end                                                                                                                                                                                                                       
end                                                                                                                                                                                                                         
                                                                                                                                                                                                                            
YoCommand.start %w(say) + ARGV
```

```shell-session:実行結果
$ ./yo
yo!
$ ./yo --force
yo!hey!
```

#### メソッドオプションのオプション

コマンドにオプションを追加すると、色々とお決まりの処理を書かなければならなくなる。
オプションのエイリアスを設定したいとか、デフォルト値を設定しておけばロジックが綺麗になるな・・などなど。

```rb:yo
#!/usr/bin/env ruby
require 'thor'

class YoCommand < Thor
  desc 'say', '言う'
  method_option :number, aliases: '-n', default: 1, type: :numeric                                                                                                                                                          
  def say                                                                                                                                                                                                                   
    puts 'yo!hey!' * options.number                                                                                                                                                                                          
  end                                                                                                                                                                                                                       
end                                                                                                                                                                                                                         
                                                                                                                                                                                                                            
YoCommand.start %w(say) + ARGV
```

当然その手の機能も上記のようにオプションを指定するだけ。

```shell-session:実行結果
% ./yo --number 2
yo!hey!yo!hey!
% ./yo -n 2 # aliasesオプション
yo!hey!yo!hey!
% ./yo --number stirng # typeオプション
Expected numeric value for '--number'; got "stirng"
% ./yo # defaultオプション
yo!hey!
```

他にもありますが、使用頻度が高そうなのはこのぐらいですかね。

## Thorコマンドをインストールしたい

実行可能なコマンドであればbinに置くなりPATHを通せばいいのだけど、もう少し手軽に使えればそれでいいという場合、Thor installでシステム領域にスクリプトをインストールし、使うことができます。
ローカルのファイルは当然ですが、Thorのinstallはウェブ上にアップしたファイルからでもインストールOKなので、例えばGistに公開しているファイルからでもインストールできます。

```shell-session:実行結果
$ thor install https://gist.githubusercontent.com/yatmsu/d156000dd6eefb9c52bd/raw/4fe831ce6e987e46833abee0444ef1505c1251f5/yo.thor
$ thor list
gist
----
thor gist:yo  # gist sample task

$ thor gist:yo
hey!
```

更にスクリプトがアップデートされても、update一発で更新できます。

```shell-session:実行結果
$ thor installed # インストール済みのスクリプトを表示
Modules  Namespaces
-------  ----------
yo.thor  gist

gist
----
thor gist:yo  # gist sample task

$ thor update yo.thor # モジュール名を指定することによって更新
```

これは便利。

## ThorをRailsで使いたい

「Rubyやってます！」って人は大体Railsを使っていることが殆どなので、RailsのRakeタスクをThorに置き換えたいと思うはず。手順は以下のとおり。

### インストール

RailsはGeneratorでThorを既に使っています。scaffoldとかファイルを生成するやつでお馴染みのアレです。なので、特別インストールする必要はありません。

### Thorfileを作成

次はThorfileを作成します。これはRakefileと同じようなファイルです。
これがないとActiveRecordにアクセス出来ません。

```rb:$RAILS_ROOT/Thorfile
require File.expand_path('../config/environment', __FILE__)
```

各タスクでrequireしている例を見かけるんですが、冗長的なのでこっちの方がDRYでおすすめ。

### Thorでタスクを作成

で、普通にThorでタスクを作成。

```rb:$RAILS_ROOT/lib/tasks/sample.thor
class Sample < Thor
  desc 'users', 'user名を表示'
  method_option :limit, default: 10
  def users
    User.all.limit(options.limit).each do |user|
      puts user.first_name
    end
  end
end
```

実行してみます。

```shell-session:実行結果
$ bundle exec thor sample:users --limit 3
洋平
陽平
遥平
```

よし動いた・・！
これでRakeのウザい引数を気にせず年を越せる！
(∩´∀｀)∩ﾜｰｲ

## 参考URL

* https://github.com/erikhuda/thor/wiki - もっと詳細を知りたい人はここを見るのが一番。