概要
grep
は,検索がヒットしないとエラー終了します.その仕様を知らず,微妙にハマったときの話です.
詳細
状況の再現
たとえば,以下のようなスクリプトがあったとします.
#!/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
オプションを使っても同じです.
$ 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.