読者です 読者をやめる 読者になる 読者になる

Folioscope

プログラミング/Unix系/デザイン/CG などのメモがもりもり

Bashでstdout/stderr/exit codeをキャプチャ

Shell Script Advent Calendar 2015 7日目の記事です。 BashのちょっとしたTipsです。 Bashに限らないかも知れませんが、検証環境がBashしかありませんでした。

問題

Bash$(command) と書くと、commandがサブシェルで実行され、 stdout(標準出力)を変数に格納することができます.

hoge_value=$(echo 'hoge')    # hoge_value => "hoge"

ただしstderr(標準エラー出力)はキャプチャできません。 いま、stdoutとstderrへ出力し、100を返す関数があるとします。

out_err_and_exit() {
  echo "This is stdout"
  echo "This \\is \"tricky 'stdout"
  echo "This is stderr" >&2
  echo "This \\is \"tricky 'stderr" >&2
  return 100
}

この関数を愚直に $(...) で出力を受け取ろうとしても、取りこぼしたstderrが無残にもターミナルに表示されます。

stdout=$(out_err_and_exit)

それではstdout/stderr/exit code をそれぞれ $stdout, $stderr, $status に格納するにはどうすればよいのでしょうか?

解答

. <(
  out_err_and_exit \
    2> >(stderr=$(</dev/stdin); declare -p stderr) \
    1> >(stdout=$(</dev/stdin); declare -p stdout)
  declare -i status=$?
  declare -p status
)

以下のコードで、特殊文字も難なく表示できるのが確認できます

echo "stdout => $stdout"
echo "stderr => $stderr"
echo "status => $status"

解説

out_err_and_exit のコマンドの実行結果を、>(...) を使ってそれぞれサブシェルに投げています。 サブシェル内では受け取った出力が /dev/stdin 経由で参照できます。 なので /dev/stdin$(...) で取得して、それを $stdout, $stderr 変数に格納します。 しかしサブシェル内のコードはサブシェル外に副作用が無いので、サブシェル外からは $stdout, $stderr にアクセスできません。 そこで declare -p の登場です。

組込みコマンド declare-p オプションをつけると、evalできるフォーマットで変数の名前と値が表示されます。

declare -p SHELL    # => declare -x SHELL="/bin/bash"

$stdout, $stderrdeclare -p して定義を出力し、 外側のシェルでその出力を評価すると、外部でも $stdout, $stderr が定義されます。 トップレベルのシェルでは、. コマンド (= source コマンド) に . <(...) して出力を評価させます。 問題ではexit codeも$statusに格納するので、. <(...) 内で $?$status に格納して、同様に declare -p してます。

Bash 4.0未満の場合(追記)

@akinomyogaさんからコメントがありました。 Bash4.0ではパイプからsourceできないバグがあるそうです。 代わりにevalを使って式を評価します。

eval -- "$(
  out_err_and_exit \
    2> >(stderr=$(</dev/stdin); declare -p stderr) \
    1> >(stdout=$(</dev/stdin); declare -p stdout)
  declare -i status=$?
  declare -p status
)"

この記事は Shell Script Advent Calendar 2015 7日目の記事です。 明日は @yudsuzuk さんです。