Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでシェルコマンドの標準出力と標準エラー出力をキャプチャする方法

はじめに

要約: Rubyからシェルコマンドを実行し、その標準出力(stdout)・標準エラー出力(stderr)・終了ステータスをまとめて取得したい場合は、Open3.capture3メソッドを使いましょう。出力をストリーミング形式で逐次処理したい場合は、Open3.popen3が便利です。

数多くの不十分な選択肢

Rubyからシェルコマンドを実行する方法は文字通り492通りあり、それぞれ動作が微妙に異なります。あなたも以下のいずれかを使ったことがあるのではないでしょうか。筆者の定番はバッククォート(``)でした。

exec("echo 'hello world'")    # Rubyを終了してからコマンドを実行
system('echo', 'hello world') # 終了ステータスコードを返す
sh('echo', 'hello world')     # 終了ステータスコードを返す
`echo "hello world"`          # 標準出力を返す
%x[echo 'hello world']        # 標準出力を返す

しかしこれらの方法には大きな制限があります。シェルコマンドの標準出力だけでなく標準エラー出力も取得したい場合、残念ながら対応できません。また、コマンドの実行完了後に一括で処理するのではなく、標準出力のデータをストリームとして逐次処理したい場合も同様に不可能です。

ところが、別の選択肢があります。コマンドを非同期で実行でき、標準出力・標準エラー出力・終了コード・PIDをすべて取得できる方法です。早速見ていきましょう!

Open3とは

一風変わった名前を持つopen3モジュールは、Rubyの標準ライブラリの一部です。具体的には何をしてくれるのでしょうか?

Open3を使うと、別のプログラムを実行する際に、標準出力・標準エラー出力・終了コード、そして子プロセスの終了を待つためのスレッドにアクセスできます。プログラムの各種属性、リダイレクト、カレントディレクトリなどは、Process.spawnと同じ方法で指定できます。(出典:Open3公式ドキュメント

使ったことがない、聞いたことすらない、という方も多いのではないでしょうか。おそらく、このライブラリがあまり親しみやすく感じられないからでしょう。名前自体がC言語のようでRubyらしくありませんし、ドキュメントもかなり専門的でハードルが高めです。しかし、実際に試してみると、想像ほど難しいものではないことがわかります。

capture3で簡単に取得する

標準出力・標準エラー出力・終了ステータスコードを一度に簡単にキャプチャできたら素敵だと思いませんか?実はそれが可能です。この記事を読む時間がない方のために先に結論を言うと、capture3というメソッドを使えばOKです。

具体例を見てみましょう。カレントディレクトリのファイル一覧を取得したいとします。その場合はlsコマンドを実行すればよいわけです。

バッククォート構文を使うと、次のように書けます。

puts(`ls`)

一方、capture3を使うとこうなります。

require 'open3'
stdout, stderr, status = Open3.capture3("ls")

これだけでコマンドが実行され、標準出力と標準エラー出力が文字列として取得できます。非常にシンプルですね。

セキュリティ面でのメリット

一般的に、ユーザーにWebサーバー上で任意のコマンドを実行させるのは避けるべきです。だからこそ、identify #{ params[:filename] }のようなコードは非常に危険なのです。悪意のある入力によって、意図しないコマンドが実行される恐れがあります。

Open3では、コマンドとデータを分離することで、この種の問題を回避できます。使い方はsystemメソッドと同じ感覚です。

Open3.capture3("identify", params[:filename], other_unsafe_params)

popen3によるストリーミング処理

実はcapture3の内部では、さらに強力なpopen3というメソッドが使われています。このメソッドは、system()などのおなじみのメソッドとは少し異なる動作をします。

基本的な使い方は以下の通りです。

require 'open3'
Open3.popen3("ls") do |stdout, stderr, status, thread|
  puts stdout.read
end

ファイルを開いて読み込む処理に少し似ていますね。次のようなコードを見たことがあるはずです。

File.open("my/file/path", "r") do |f|
  puts f.read
end

パイプの扱い方

Open3では、標準出力と標準エラー出力はどちらも「パイプ」として扱われます。パイプはファイルバッファによく似た振る舞いをし、ファイルと同様に使い終わったら閉じる必要があります。これこそがブロック構文を採用している理由です(ブロックを使わない書き方もありますが、その場合はstdoutとstderrに対して手動でcloseを呼び出す必要があります)。

readメソッドは、パイプが閉じられるまで待機してから値を返します。しかしパイプは、利用可能になった行を順次読み取ることもできます。例えば、シェルコマンドの実行に数秒かかり、その間に標準エラー出力へ進捗メッセージを出力しているとしましょう。それをリアルタイムに取得してユーザーに表示したいケースです。

標準エラー出力を1行ずつ取得するコードは次のようになります。

require 'open3'
Open3.popen3("sleep 2; ls") do |stdout, stderr, status, thread|
  while line = stderr.gets do
    puts(line)
  end
end

thread引数で得られる情報

まだ触れていない引数がもう1つあります。それがthreadです。

thread引数は、コマンドの終了を待ち受けているRubyスレッドへの参照を返します。ただし、コマンド自体がそのスレッド内で実行されているわけではありません。コマンドは完全に独立した別プロセスとして実行されており、スレッドはそのプロセスを監視して完了を待つだけです。

このスレッド参照からは、次のような有用な情報を取得できます。

  • thread.pid — シェルコマンドのプロセスID(PID)が格納されています。対象プロセスに対してOSレベルの追加操作を行いたい場合に必要となります。
  • thread.value — コマンドの終了を待ち、Process::Statusオブジェクトを返します。終了ステータスは.exitstatus.success?で確認でき、Unix系の慣習では0が成功、0以外が失敗を意味します。

注意点:デッドロックに気をつける

最後に、Open3公式ドキュメントからの重要な注意点を紹介します。

デッドロックを避けるよう注意してください。パイプは固定長のバッファであるため、::popen3("prog") {|i, o, e, t| o.read }は、プログラムがstderrに大量の出力を生成するとデッドロックします。stdoutとstderrは同時に読み取る必要があります(スレッドまたはIO.selectを使用)。ただし、stderrの出力が不要な場合は::popen2を、stdoutとstderrが混在しても問題ない場合は::popen2eを、stdoutとstderrを別々の文字列として取得したい場合は::capture3の利用を検討してください。

長時間実行されるコマンドや大量の出力を生成するコマンドを扱う際は、この点を必ず頭に入れておきましょう。状況に応じて適切なメソッドを選ぶことで、安全かつ効率的に外部コマンドを扱えるようになります。

  1. Rubyでゼロから学ぶ! シンプルなWebSocketサーバーの自作ガイド

    近年、WebSocketはますます注目を集めています。「これこそ未来の技術だ」「Rails 5のActionCableのおかげで、かつてないほど簡単に使える」——そんな話を耳にすることも多いでしょう。しかし、そもそもWebSocketとは何なのでしょうか? そして、どのような仕組みで動いているのでしょうか? この記事では、こうした疑問に答えるため、Rubyを使ってゼロからシンプルなWebSocketサーバーを構築していきます。最終的には、ブラウザとサーバーの間で双方向通信を実現できるようになります。 本記事のコードはあくまで学習用のものです。実際の本番アプリケーションにWebSocket

  2. HTTPヘッダーはどのようにnginxからRubyアプリへ渡されるのか

    現在のWeb開発のほとんどはフレームワークを使って行われています。Rails、Sinatra、Lotusなど、どれを使っていても、Cookieやその他のHTTPヘッダーがnginxやApacheからアプリケーションサーバーを経由してアプリ本体へ届くまでの仕組みについて意識する必要はほとんどありません。自動的に処理されるからです。 しかし、この「ヘッダーの旅」を少し深く掘り下げてみると、そこにはWebの歴史を感じさせる興味深い情報がたくさん詰まっています。 そもそもHTTPヘッダーとは何か? Webブラウザがリクエストを送信する際には、必ず「HTTPヘッダー」と呼ばれる情報が一緒に送られま