シェルスクリプトでカレントディレクトリにあるディレクトリだけを対象に処理したいことがあります。私はこれまで、以下のように ls -F を使っていました。
for d in $(ls -F | grep /); do
echo "$d"
done
ls -F はディレクトリ名の末尾に / を付加するので、grep / でディレクトリだけを抽出するという発想です。これでも動くのですが、実はもっとシンプルで安全な方法がありました。
*/ がおすすめ
シェルにはグロブ(glob)というパターンマッチの仕組みがあり、 */ と書くだけでカレントディレクトリ直下のディレクトリのみにマッチします。
for d in */; do
echo "$d"
done
たったこれだけです。ls コマンドもパイプも不要です。
実は、シェルスクリプトの世界では「ls の出力をプログラム的に使わない」というのがベストプラクティスとされているということを学びました。ファイル名やディレクトリ名にスペースや特殊文字が含まれている場合、ls の出力をパースする(利用する)と意図しない動作になることがあるためです。 */ はシェルが直接展開するので、こうした問題を回避できます。
実際に試してみる
まず、練習用のディレクトリを準備します。適当な空のディレクトリを準備し、その中で以下を実行してください。
mkdir -p dir1 dir2 dir3 archive excluded
*/ で列挙してみましょう。
for d in */; do
echo "$d"
done
出力は以下のようになります。
archive/ dir1/ dir2/ dir3/ excluded/
特定のディレクトリを除外したい場合は continue を使う
「あるディレクトリにすべてのディレクトリを移動したいが、移動先のディレクトリ自身は除きたい」というケースを考えます。例えば、 archive ディレクトリを作成し、それ以外のディレクトリをすべて archive に移動する場合です。
mkdir -p archive
for d in */; do
[[ $d == "archive/" ]] && continue
mv "$d" archive/
done
[[ $d == “archive/” ]] && continue の部分で、$d が archive/ であればループの残りの処理をスキップし、次のディレクトリに進みます。
注意:比較対象にはスラッシュ / を忘れずに
ここで重要なのが、比較対象を “archive/” とスラッシュ付きで書くことです。*/ で展開されたディレクトリ名には末尾に / が付いているため、“archive” と書くとマッチしません。
- スラッシュを抜くとマッチしない
[[ $d == "archive" ]] && continue
- スラッシュを入れることで正しくマッチする
[[ $d == "archive/" ]] && continue
これは */ を使う際に忘れやすいポイントなので、気をつけてください。
複数のディレクトリを除外したい場合: case を使う
除外したいディレクトリが複数ある場合、[[ … ]] && continue を何行も並べるのは煩雑です。このような場合は case 文を使うとすっきり書けます。
例えば、archive と excluded の2つのディレクトリを除外して、残りを archive に移動する場合は以下のようになります。
mkdir -p archive
for d in */; do
case "$d" in
archive/|excluded/)
continue
;;
esac
mv "$d" archive/
done
case 文では | で複数のパターンを区切ることができるので、除外対象が増えても見通しよく記述できます。
まとめ
以上、まとめると以下のようになります。
- ディレクトリの列挙には ls -F ではなく */ を使う
- 特定のディレクトリを除外するには continue を使う
- 除外対象が複数なら case 文を使うとすっきり書ける
- */ で展開されたディレクトリ名には末尾に / が付くので、比較時にはスラッシュを忘れない