シェルスクリプト(bash) ファイル一覧ループのベストな書き方

シェルスクリプトにて、
特定のディレクトリのファイル一覧で、ループ処理がしたいとき
どんな書き方が一番良いでしょうか?
この記事では、bashを使ったベストな書き方を紹介していきます。

今回の内容は、bashに限定した書き方になっているので、ご留意ください。
Shebangで、#!/bin/bash と書いておいて実行権限を付与しておくと間違いありません。

ベストな書き方

先に、結論を書いておきます。
プロセス置換をつかって、while read でループ処理するのが一番良いです。

#!/bin/bash

while read -r f; do

  # ファイル一つ毎の処理
  echo "file: $f"

done < <(find ./hoge -mindepth 1 -maxdepth 1)

上記は、hogeディレクトリ直下のファイル(ディレクトリ含む)で
ループ処理をしている例です。

<(...) の部分がプロセス置換になります。
括弧中のfindコマンドをいじれば、いろいろ絞り込んでループさせる事ができます。

直下じゃなくて、サブディレクトリも含めたい場合は、-maxdepth 1 を外せばOK。

なぜこの書き方がベストなのか?
他の書き方についても後述していきます。

for文とglobマッチを使った場合

一番単純な書き方は下記のとおりでしょう。
これはglob展開を使って、展開されたパスをfor文でループしています。

for f in ./hoge/*; do

  # ファイル一つ毎の処理
  echo "file: $f"

done

hogeディレクトリ直下のファイルを一覧で取得し、処理できます。
空白を含んだファイル名であっても、問題なく動作します。

しかし、このhogeディレクトリの中身が、もし空っぽだった場合。
一回もループせずに終わってほしいところ残念ながら、
変数fには "./hoge/*" という、glob展開前の文字列がセットされて来てしまいます。

空ディレクトリに対して使うと問題

for f in ./hoge/*; do ... を空ディレクトリに対して使うと、
glob展開に失敗するため、"./hoge/*"という文字列自体が変数fにセットされて、ループ内に来ます。

ちゃんとしたファイル名が来るのを期待して、
処理を書いているとエラーになってしまいます。

これに対しての回避策は、3つあります。

  • 処理の前に、空ディレクトリかどうか判定しておく。
  • ループ内処理の冒頭で、存在するPathか判定する。
  • glob展開できなかった場合の挙動を変更する。

空ディレクトリか判定する

空ディレクトリだった場合には、ループ処理に入いらないようにif文を設ける方法です。

if [ ! -z "$(ls -A hoge)" ]; then
  # TODO: ループ処理
fi

有効なファイルか判定してスキップする

ループの冒頭で、正しいファイルか判定しておく方法です。

for f in ./hoge/*; do
  # 存在しないファイルなら、スキップする。
  [ -e "$f" ] || continue

  # ファイル一つ毎の処理
  echo "file: $f"

done

空ディレクトリじゃない場合には、無意味な判定処理になるので、そこが残念などころ。

globできなかったときの挙動を変更する

bashに限って、このglob展開できなかったときの挙動が変更できます。
shoptコマンドというのを使うと、bash自体に設定ができます。

shopt -s nullglob

シェルスクリプトの冒頭か、ループ処理の前に設定しておくことで、
空ディレクトリの場合に、glob展開が空文字になります。
それによって、変数fに"./hoge/*"という文字列が入ってループするという事がなくなります。

設定変更が実行中のシェル(bash)に影響するので、気持ち悪い気もします。
サブシェルを起動して、その中で変更して使うのもありですね。
(親シェルには影響しない)

shoptコマンドについて

あまり聞き慣れないコマンドが出て来たので、ちょっとだけ解説しておきます。
shoptコマンドは、bashの挙動を設定するための(built-in)コマンドです。

shopt -p でシェル(bash)の現在の設定状態を一覧で見ることができます。

$ shopt -p

shopt -u lastpipe
shopt -u lithist
shopt -u localvar_inherit
shopt -u localvar_unset
shopt -u login_shell
shopt -u mailwarn
shopt -u no_empty_cmd_completion
shopt -u nocaseglob
shopt -u nocasematch
shopt -u nullglob
shopt -s progcomp
shopt -u progcomp_alias
.
.

オプション引数の後に書かれている名称が、設定項目です。
-s が有効状態 (set)
-u が無効状態 (unset)
を表しています。

有効状態にしたければ、shop -s <設定項目名> とコマンドを打つと設定されます。
無効状態にしたい場合は、shop -u <設定項目名> ですね。

今回は、globで展開できなかった場合に、
空文字列にするという設定項目 nullglobを有効にしていました。
shopt -s nullglob

ベストな書き方の解説

これまで述べてきたとおり、
for f in <globパターン>; do ... の書き方は、空ディレクトリを考慮しないといけない。
というちょっと面倒な問題が隠れています。

他にも、for f in $(find ...); do ... のように、for文にコマンド置換して渡すという方法もあります。
しかし、こちらの場合は、ファイル名に空白が含まれている場合にループが分割されてしまいます。

あとは、パイプでwhile readに渡すという方法もあります。
find ... | while read -r f; do ...
この方法だと、ループ内の処理がサブシェルでの実行になってしまうので、実はこの方法もあまりおすすめしません。
(シェル変数を扱う場合に罠っぽくなる)

なので、最終的には、プロセス置換をつかって、while read でループ処理するのが一番副作用もなくて良い方法だと思います。
findコマンドには様々な条件も指定できます。

while read -r f; do

  # ファイル一つ毎の処理
  echo "file: $f"

done < <(find ./hoge -mindepth 1 -maxdepth 1)

プロセス置換とは
出力内容を一時ファイルとして渡しているようなもの。(bashの機能)
その出力を一行ずつwhile read で処理しています。

まとめ

一見、for f in hoge/*; do ... で問題なく動くので、注意しないといけない。
空ディレクトリの場合は、"hoge/*" が来てしまいます。

予め、空ディレクトリかどうか判定するか、
[ -e "$f" ] || continue などでスキップするようにするか、
shopt -s nullglob と設定しておかないといけない。

for文にコマンド置換を渡す場合は、空白で分断される。
パイプでwhile readに渡す場合は、ループ処理がサブシェルになる。

最終的には、プロセス置換を使って、
while read でループさせるのが、一番副作用がすくなくて簡素に書けるベストな方法となります。


シェルスクリプトについて、ほかにも記事を書いています。
bashスクリプトで、オプション解析がしたい場合はこちらの記事がオススメ。

コメント

タイトルとURLをコピーしました