Thor Tips
--no-delete オプションを指定したら削除される
class App < Thor
desc "func1", "(desc)"
method_option :delete, type: :boolean
def func1
options[:delete] # => false
params = {
delete: true,
}.merge(options)
if params[:delete]
p "削除しました"
end
end
end
App.start(["func1", "--no-delete"])
# >> "削除しました"
- options は悪しき HashWithIndifferentAccess のインスタンスになっている
- 文字列とシンボルを区別しないでよくなることよりもマージした際にキーが一致せずに事故るデメリットの方がはるかに大きい
- 上の場合は
options.transform_keys(&:to_sym)
としてから merge しないといけない- ActiveSupport が入っている場合は
options.to_options
とする - ただし
options = options.to_options
と書くとnil.to_options
を呼ぶことになってまたはまるのでこの場合はoptions = self.options.to_options
と書かないといけない
- ActiveSupport が入っている場合は
自分で定義した say メソッドの挙動がおかしい
module Kernel
def say(message)
puts message
system "say '#{message}'"
end
end
class App < Thor
desc "func1", "(desc)"
def func1
say "ok"
end
end
App.start(["func1"])
# >> ok
ok と表示はされるがしゃべらない
原因は say を上書きされていたというオチ
Thor はよかれと次の便利メソッドたちを用意してくれている
mute mute? padding= indent ask say say_error say_status yes? no? print_in_columns print_table print_wrapped file_collision terminal_width error set_color
これらと名前がかぶると負ける
これに気づかず、めちゃくちゃはまった
引数がエラーでもステイタス0を返してしまう問題
class App < Thor
def self.exit_on_failure?
true
end
end
と、書けばいいらしいが、なんでこれがデフォルトじゃないんだ! と、みんな青筋を立てている
しかし最初の Issue が立ってから10年放置っていう
オプションを繰り返し指定する
class App < Thor
desc "func1", "(desc)"
method_option :foo, type: :array, repeatable: true
method_option :bar, type: :hash, repeatable: true
def func1
options[:foo] # => [["1", "2"], ["3", "4"]]
options[:bar] # => {"a"=>"1", "b"=>"2", "c"=>"3", "d"=>"4"}
end
end
App.start([
"func1",
"--foo", "1", "2",
"--foo", "3", "4",
"--bar", "a:1", "b:2",
"--bar", "c:3", "d:4",
])
-
repeatable: true
とすれば引数をマージできる- ハッシュの場合は単に merge する
- 配列の場合はネストしている
- 平らにするにはあとで flatten が必要
- デフォルトは false
- 重複指定でもエラーにはならない
- 後から指定した方で上書きする
よく使うコマンドにはショートカットを指定する
class App < Thor
map "s" => :schedule
desc "schedule", "スケジュール"
def schedule
end
end
-
s
でschedule
が呼べるようになる - とはいえ最初から設定してはいけない
- 設定しない場合 s で始まるコマンドを列挙してくれる便利機能が働く
コマンド実行前にクラスオプションを受け取りたい
class App < Thor
class_option :foo, type: :boolean
def initialize(...)
super
options # => {"foo"=>true}
end
desc "func1", "(desc)"
def func1
end
end
App.start(["func1", "--foo"])
- 上の例であれば func1 に入った直後で options を参照すればいい
- しかしコマンドが100個がある場合、100箇所に同じコードを差し込まないといけない
- このあたり何か用意してくれててもおかしくないがいくら調べてもわからなかった
- とりあえず initialize の中で参照する方法にして問題はなかった
options 固有のメソッドを活用するな
コマンド内で使える options (HashWithIndifferentAccess 型) が
options[:foo] # => "bar"
となっているとき次のように書ける
options.foo? # => true
options.foo # => "bar"
options.foo?("bar") # => true
が、Hash との整合性が取れなくなり、逆にリファクタリングが難しくなるので、一切活用してはいけない
long_desc を使うと改行が削除される
-
--help xxx
としたときにコマンド xxx の説明文を最後に出力してくれる便利機能がある - その説明文を long_desc メソッドで登録できる
- なので詳しい使い方を自由に書けると思っていたら改行コードを削除して詰めて画面幅で折り返されてしまった
- その上、英語である前提なので日本語なんか入れると折り返し位置が変になる
- とりあえず次のように「何もしない」ようにすると使いやすくなる
class Thor::Shell::Basic
def print_wrapped(message, options = {})
puts message
end
end
Thor のサブクラスに一切のロジックを書くべからず
次のように書いていたら破綻した
class App < Thor
desc "foo_func", "(desc)"
def foo_func
foo_func_perform
end
desc "bar_func", "(desc)"
def bar_func
bar_func_perform
end
private
def foo_func_perform
end
def bar_func_perform
end
end
最初はシンプルでわかりやすかったが、そのまま膨らませていくと、プライベートメソッドが干渉し合ってメンテ不能に陥った。二つの機能 foo と bar は互いに何も関係がないにもかかわらず、同じスコープで共存しているのが失敗だった。
これは Rake タスクなどにも言えることで、エントリーポイントには完成されたメソッドを一行書くぐらいでないといけない。可能だからといって同じスコープにプライベートメソッドをぶちまけてはいけない。そこからおかしくなる。
便利メソッドが使えるのはサブクラスだけというジレンマ
Thor継承クラスが肥大化しがちな理由の1つとしてそこでしか便利メソッドが使えないからというのがある
便利メソッドを活用していたコードを別のクラスに移動させるとたちまち undefine method
の嵐になってしまうのでコードを分離できないというわけ
結局どうすればいいのかよくわかっていないが、ソースを読んだところ、次のようにすればどこからでも呼べることがわかった
Thor::Base.shell.new.say("message")
Discussion