シェルスクリプトでハマった件→【募】ステキな回避方法
追記: 解答編を下の方に書きました!!!
追記: お題に不備があったので変更しました><
あとgistに一式置いたので簡単にテストできます!!
git clone https://gist.github.com/6232206.git oreno cd oreno ./test.sh source_me.sh ...
シェルは GNU bash, version 4.1.5(1)-release です。
こんな source_me.sh と、
case $PATH in */oreno/bin*) : # do nothing ;; *) . orenorc LOADED_ORENO=1; export LOADED_ORENO ;; esac
orenorc ってファイルがあって、
ORENO_HOME="/oreno"; export ORENO_HOME PATH=$PATH:$ORENO_HOME/bin # 無視してもいい警告 echo "trivial warning" >&2 # やばいエラー! echo "critical error" >&2
sourceするとこんな感じになる。
$ PATH=/bin:/usr/bin; . source_me.sh; echo $PATH trivial warning critical error /oreno/bin:/bin:/usr/bin
で、critical errorは出力したいけど、trivial warningは無視したいので画面に出したくないとします。(orenorcは深淵な理由でいじれないとします)
ぱっと思いつくのは
case $PATH in */oreno/bin*) : # do nothing ;; *) . orenorc LOADED_ORENO=1; export LOADED_ORENO ;; esac 2>&1 | grep -v trivial
とか
case $PATH in */oreno/bin*) : # do nothing ;; *) . orenorc 2>&1 | grep -v trivial LOADED_ORENO=1; export LOADED_ORENO ;; esac
とかだと思うんですが、これを実行すると、
$ PATH=/bin:/usr/bin; . source_me.sh; echo $PATH critical error /bin:/usr/bin
こうなっちゃって、trivial warning は表示されないんだけどPATHがセットされない。
原因は、シェルスクリプト書いてて毎年1回は踏むワナなのでわりとすぐに気がついたんだけど、回避方法できるのにちょっと時間がかかった上にスマートじゃない気がするのでステキな回避法を思いついたかたは教えてください!!!!
解答編
原因は?
原因は、外部コマンドが絡むとサブシェルで実行されるからです。※コメント欄のfumiyasuさんの書き込みもあわせて参照してください。
bashの場合、
Man page of BASH
- BASHPID
- BASH_SUBSHELL
- サブシェルやサブシェル環境が作成されるたびに 1 ずつ増えます。 初期値は 0 です。
で観測できるのでやってみましょう。
case文全体でgrepしているスクリプト(gistのsource_me-ng1.sh)は、
echo_pid() { echo "$@ $$ $BASHPID $BASH_SUBSHELL"; } echo_pid 1 case $PATH in */oreno/bin*) : # do nothing ;; *) echo_pid 2 . orenorc echo_pid 3 LOADED_ORENO=1; export LOADED_ORENO ;; esac 2>&1 | grep -v trivial echo_pid 4
こういう結果になります。
PID=27549 1 27549 27549 0 2 27549 27604 1 o 27549 27604 1 # ←これは orenorc の中でのです 3 27549 27604 1 4 27549 27549 0
$$は変わっていませんが、caseの中ではBASHPIDとBASH_SUBSHELLが変化しています。
次に . oreno をgrepしているスクリプト(gistのsource_me-ng2.sh)では、こうなります。
PID=27649 1 27649 27649 0 2 27649 27649 0 o 27649 27704 1 3 27649 27649 0 4 27649 27649 0
source_meの世界では変わっていませんが、orenorcの世界は別プロセスで実行されているのがわかります。
さて、
いただいた回避法の紹介コーナー
いちばんステキだと思ったのは、[twitter:@satoh_fumiyasu] さんのこれ(一部抜粋、完全版はgistのsource_me-ok-satoh_fumiyasu.sh)です。
*) . orenorc 2> >(grep -v trivial >&2) LOADED_ORENO=1; export LOADED_ORENO ;;
stderrをbash(限定)のプロセス置換に渡しています。自分はこの発想はなかったです!
同じくプロセス置換を使っているのが、[twitter:@sechiro]さんのこれ(file-source_me-ok-sechiro-sh)です。
exec 2> >(grep -v trivial >&2) case $PATH in */oreno/bin*) : # do nothing ;; *) . orenorc LOADED_ORENO=1; export LOADED_ORENO ;; esac
同じようにstderrをプロセス置換に渡してるんですが、それをexecでやっています。
常々、execにフィルタが書ければstdout,stderr全部にタイムスタンプつけるのが簡単になるのになぁと思ってたんですが、この手法を使えばできますね。
exec > >(tai64n | tai64nlocal) exec 2> >(tai64n | tai64nlocal)
bashのプロセス置換については、sechiro さんがまとめてくださっているのでそちらを参照するのがいいと思います。スバラシス。
で、自分が考えたのはこんなの(source_me-ok-hirose31.sh)です。
tmpf=$(mktemp) exec 3>&2 2>$tmpf case $PATH in */oreno/bin*) : # do nothing ;; *) . orenorc LOADED_ORENO=1; export LOADED_ORENO ;; esac exec 2>&3 3>&- cat $tmpf | grep -v trivial rm -f $tmpf
あとで復元するようにfd 2をdupして3を作り、2を一時ファイルに向けます。
case文が終わったら、fd 2を復元(3をdupして2にする)して、3はもう要らないので閉じます。あとは一時ファイルをgrepして消します。
[twitter:@tagomoris]さんも同じような手法を提案していました。
最後は[twitter:@m2ym]さん提案のmkfifoを使う方法です。こんなスクリプト(source_me-ok-m2ym.sh)になるかと思います。
fifo=$(mktemp -u) mkfifo $fifo grep -v trivial <$fifo & exec 3>&2 2>$fifo rm -f $fifo case $PATH in */oreno/bin*) : # do nothing ;; *) . orenorc LOADED_ORENO=1; export LOADED_ORENO ;; esac exec 2>&3 3>&-
自分の環境では exec の前に grep しないと exec がブロックして止まっちゃったので気をつけましょう。
いずれもfd 2をファイルやFIFOに向ける手法です。[twitter:@frsyuki] さんはコメント欄でファイル的なものを作らない方法を提案していましたが、残念ながらうまく動きませんでした。ファイル的なものを使わない方が消し忘れがなくてよいので、うまくできる書き方があったら是非教えてください><
回答スクリプトも含め gist (https://gist.github.com/hirose31/6232206) に置きましたので興味ある方はぞうぞ!
最後に、回答くださった皆様、ありがとうございました!!
参考ページ
- Advanced Bash-Scripting Guide
- 基本的なところからかなりマニアックなところまで説明しているので、bash使いは必読です。今回のに関係しそうなのはこのへんです。
- I/O Redirection
- Using exec
- Subshells