実用的にRubyで外部コマンドを実行する - 車輪の再発明をしたくない人のための tty-command

## はじめに

こんにちは。（この記事は、[tty-command](https://github.com/piotrmurach/tty-command) の紹介記事です。tty-commandを知ってる人はこの記事は読まなくても大丈夫です。）

Rubyからコマンドを実行したいことは時々あると思います。よく知られている方法は、

1. `system` を使う
2. ``` `` ←　バッククオート```
を使う
3. `open3` を使う
4. `spawn` を使う

他にも方法はありますが、私は上記の4つをよく使います。
homebrewのレシピは`system`を使って書かれていますよね。

しかし、実際には、ターミナルに

* 実行するコマンドを出力したい
* 結果も出力したい
* できれば色付きされて、綺麗に整形された出力がいい！
* 実行時間も表示したい

場合が多いのではないかと思います。車輪の再発明は、基本的に良いことだと思います。しかし、便利に使いたいだけの場合は、別に車輪、再発明しなくてもよくないか？となりますよね。そんなあなたに `tty-command` を紹介します。

## tty-command とは？

Github: https://github.com/piotrmurach/tty-command

[TTYシリーズ](https://ttytoolkit.org/components/)は、[Piotr Murach](https://github.com/piotrmurach)さんが開発しているCLIツールの作成を強力に支援するツールです。どこかで下のアイコンを見たことがある方も多いのではないでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/9abc4a84-d710-1736-227c-9e4cb210d422.png)

TTYは好みが分かれると思いますが、私は好きです。tty-commandは、TTYシリーズのうちの一つの、Rubyの外部コマンドを実行するツールです。TTYシリーズは、独立したコンポーネントの集合体ですので、tty-commandだけ使って他のコンポーネントは一切使わないことができます。（あと、たまに誤解している人が居る気がしますが、TTYシリーズはThorと一緒に使う必要はありません。Thorを使わずOptParseだけで組んだCLIツールでもTTYは便利に使えます。）

そんなこともあってTTYシリーズが苦手な方でも、tty-command の単体利用は十分に恩恵があるのではないかなと思います。

使い方は簡単です。これまで `system` と書いていたのを、 `cmd.run` に変えるだけです。

```
gem install tty-command
```

例えば、`gem update` `gem cleanup` を実行する時はこんな感じです。

```ruby
require "tty-command"

cmd = TTY::Command.new
cmd.run "gem update"
cmd.run "gem cleanup"
```

上記のようなrubyスクリプトを実行すると、ローカルのrubygemが更新されます。tty-commandの威力を実感するために、irbやpryではなく、小さなスクリプトファイルを作成して、`ruby` コマンドで実行することをおすすめします。

↓こんな感じで綺麗に整形された出力がリアルタイムで得られる。実行時間もバッチリ表示されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/9567779e-09cc-5eed-6126-6d1fb85a7dd8.png)

公式READMEを見ると、主な機能はざっと網羅されていることがわかります。

以下はREADMEのコピペです。

標準出力と標準エラー出力

```ruby
out, err = cmd.run("date")
```

プログレスバー

```ruby
cmd = TTY::Command.new(printer: :progress)
```

ロガー

```ruby
logger = Logger.new("dev.log")
cmd = TTY::Command.new(output: logger)
```

環境変数

```ruby
cmd.run({"RAILS_ENV" => "PRODUCTION"}, :rails, "server")
```

リダイレクト

```ruby
cmd.run(:ls, :err => :out)
cmd.run(:ls, :stderr => :stdout)
cmd.run(:ls, 2 => 1)
cmd.run(:ls, STDERR => :out)
cmd.run(:ls, STDERR => STDOUT)

cmd.run(:cat, :in => "file")
cmd.run(:cat, :in => open("/etc/passwd"))
cmd.run(:ls, :out => "log")
cmd.run(:ls, :out => "/dev/null")
cmd.run(:ls, :out => "out.log", :err => "err.log")
cmd.run(:ls, [:out, :err] => "log")
cmd.run("ls 1>&2", :err => "log")
```

失敗したら次のコマンドを実行

```ruby
if cmd.run!("which xyzzy").failure?
  cmd.run("brew install xyzzy")
end
```

ユーザー、グループを指定

```ruby
cmd.run(:echo, "hello", user: "piotr")
cmd.run(:echo, "hello", group: "devs")
```

コマンドの実行結果の確認

```ruby
result = cmd.run(:echo, "Hello")
result.out
result.err
result.success?
result.failure?
result.exited?
result.complete?
```

実際の例

```ruby
cmd = TTY::Command.new

# dependencies
cmd.run "apt-get -y install build-essential checkinstall"

# fetch ruby if necessary
if !File.exists?("ruby-2.3.0.tar.gz")
  puts "Downloading..."
  cmd.run "wget http://ftp.ruby-lang.org/pub/ruby/2.3/ruby-2.3.0.tar.gz"
  cmd.run "tar xvzf ruby-2.3.0.tar.gz"
end

# now install
Dir.chdir("ruby-2.3.0") do
  puts "Building..."
  cmd.run "./configure --prefix=/usr/local"
  cmd.run "make"
end
```


カスタムプリンタ（出力の形式や色をカスタマイズすることも可能）

```ruby
CustomPrinter < TTY::Command::Printers::Abstract
  def write(cmd, message)
    puts cmd.to_command + message
  end
end

cmd = TTY::Command.new(printer: CustomPrinter)
```

すごいですね。だいたいの機能があります。（よかった、車輪の再発明をする手間が省けたよ！）

しかし、この手のサードパーティ製のツールは、メンテナンス状況が気になるところだと思います。

TTYシリーズは一定の利用者を持ち、少なくとも数年にわたるメンテナンスが続けられてきた実績があります。なので自分で下手な車輪を再発明するよりは安全かな？と個人的には思います。自分でコードを書いた場合も数年後には読めなくなる可能性はありますからね。

とはいえ `system` を使った単純なコードは、15年後でも問題なく動作するかも知れません。不用意にgemを増やして複雑にするべきではないというのも一理あります。そのあたりは利便性とメンテナンスのバランスだと思います。

この記事は以上です。