[シェルスクリプト] forループでのパス名展開

2017年10月2日月曜日

bash unix シェルスクリプト

forループ

シェルスクリプトでファイルごとに同じ処理を実行するなんてことはよくあるね。
そんな場合はforループでパス名展開を使う。
例えば、*.log形式のファイルの中身をすべて出力したい場合は以下のようにする。
for f in *.log
do
  cat "$f"
done

※この場合、実はループなんか使う必要はなくて、cat *.log で十分だけど。

ファイルがない場合

ところが、展開の結果、該当のファイルが1つも存在しない場合、これはうまくいかない。

$ ls
$ for f in *.log; do cat "$f"; done
cat: *.log: No such file or directory

なんで、こんなことになってしまうのかというと、パス名展開の結果、該当するファイルが存在しない場合はパターン文字列がそのまま残ってしまうからだ。

$ echo *.log
*.log
$ touch {1..3}.log
$ ls
1.log 2.log 3.log
$ echo *.log
1.log 2.log 3.log

man bash(1)のパス名展開によると

マッチするファイル名が見つからず、かつシェルのオプション nullglob が無効ならば、その単語は変更されずにそのまま残ります。

とある。

nullglob

なので、nullglobオプションを有効にすると

$ rm *.log
$ shopt -s nullglob
$ echo *.log

$

確かにechoが何も受け取らなくなっている。
じゃあ、シェルスクリプトの先頭でnullglobをセットすればいいかというと、そうもいかない。
今度は該当のファイルが存在しない場合にlsやcatといったコマンドが引数なしで実行されるという結果になってしまう。

結局

というわけで、あまりかっこよくはないけど、ファイルが実在するかどうかチェックを入れるのが妥当ということになる。
for f in *.log
do
  [ -e "$f" ] || continue
  cat "$f"
done

これであれば、ファイルが1つもない場合でも、ループ内の処理は実行されずに終わることになる。

[参考文献]
Bashのよくある間違い