概要
grep は,検索がヒットしないとエラー終了します.その仕様を知らず,微妙にハマったときの話です.
詳細
状況の再現
たとえば,以下のようなスクリプトがあったとします.
sapmle-grep.sh
#!/bin/bash
set -exo pipefail
lines="$(echo foo | grep bar | wc -l)"
echo "$lines"
echo foo がなにか動的な処理をするコマンドと想定しましょう.その出力の中から, bar を含む行を絞り込み,その行数をカウントして表示するスクリプトです.
直感的には, echo foo の出力内に bar なる文字列が存在しない場合,最後には 0 と出力されて終了するだろうと予想できます.しかし,現実にはそうならず,以下のような表示で終了します. 1
$ ./sample-grep.sh
++ grep bar
++ echo foo
++ wc -l
+ lines=0
最後の echo が実行されていません.一見,なぜかスクリプトが途中で停止する怪奇現象に思えます.
原因の調査
遭遇時,不思議に思いながら原因を絞り込んだところ,どうやら $() の中のコマンド時点で停止していることがわかりました.そして, grep が終了コード 1 を出していることもわかりました.そこまで判明すると,スクリプト全体が停止するのは set -e および set -o pipefail の効果によると予想できます.
肝心の「なぜ grep がエラー終了しているのか」(= なぜ終了コードが 1 なのか) というところです. man grep を見て exit status で検索すると,以下の内容がヒットします.
EXIT STATUS
Normally the exit status is 0 if a line is selected, 1 if no lines were
selected, and 2 if an error occurred. However, if the -q or --quiet or
--silent is used and a line is selected, the exit status is 0 even if
an error occurred.
(抜粋・雑訳)
検索対象が見つからなければ,ステータス 1 で終了します.
grep はマッチ文字列がないとエラー終了する
タイトル回収です.
man の通り,マッチする対象が見つからなかったために終了コード 1 を出しており,そこに set -eo pipefail が合わさって怪奇現象が起きていました.この挙動は, wc -l との組み合わせではなく grep の --count オプションを使っても同じです.
sample-grep-n.sh
$ echo foo | grep --count bar || echo fail
0
fail
set -eo pipefail も特にエラー文を出力しなかったので,怪奇現象となっていました.
完全にやられました.
回避策
これだけのために set -eo pipefail を外したくはありません.別の方法で回避したい気持ちになります.
grep 自体は「マッチしなくても 0 で終了する」的なオプションは持たないようです.エラー終了を回避するには,以下のようにする必要があるでしょう. 2
grep bar || true
これだと通常のエラー (コード 2) も握り潰してしまうため好ましくないですが, 1 だけをうまく躱すためにはこの Stack Overflow の回答 のように $? や [[ 等を組み合わせるしかなさそうです.
echo something | grep x || [[ $? == 1 ]]
少し冗長になってしまうので,ケースバイケースで使い分けることになるでしょう.今回は使い捨てのスクリプトだったため,簡潔な前者を採用しました.
より堅牢性 (?) が求められる場合は,後者のようにマッチしなかった場合のみをハンドルするようなやり方が求められるでしょう.
まとめ
grep の「マッチ文字列がないとエラー終了する」仕様を知らずに苦しめられた話でした.
筆者としては非直感的に思える挙動ですが,何か深い意図があるのでしょうか…….それにしたって,コマンドラインでオプトアウトできても良さそうなものですが,
この記事を読んだ方は,この挙動を頭においておくか,任意の grep に || true をつける習慣を持ちましょう 3.
+ から始まる行は,実行されたコマンドの内容です. set -x の働きです. ↩
true コマンドは,何も出力せず終了コード 0 を出します. ↩
冗談です. ↩
↧