よく見る方法
/path/to/hoge
内に存在するファイル1件1件に対して何らかの処理をしたい時。
素直に書くならこんな感じ。
proc_hoge.sh
#!/bin/bash
find /path/to/hoge -type f | while read x; do
echo "do something...: ${x}!!"
done
問題点
ファイル名に特殊な文字が含まれていた場合、、、?
tree /path/to/hoge
# => /path/to/hoge
# ├── 'aaa ' :末尾に空白が含まれている
# ├── 'bbb\012ccc' :改行コード(\012)が含まれている
# └── 'ddd\eee' :バックスラッシュが含まれている
ファイル名を正しく取得することができなかった。
./proc_hoge.sh
# => do something...: /path/to/hoge/bbb!!
# do something...: ccc!!
# do something...: /path/to/hoge/aaa!!
# do something...: /path/to/hoge/dddeee!!
要因
findによって各ファイル名が改行区切りで出力されているが、その内容をreadが正しく取得できていない。
- IFSの設定(デフォルトではスペース・タブ・改行)に従ってフィールドを分割するため、末尾の空白が無視されてしまっている
- 標準入力から内容を一行ずつ読んでいるため、改行が含まれるファイル名を一度に取得できていない
- エスケープ文字として認識されているため、バックスラッシュが無視されてしまっている
丁寧な方法
上で挙げた3つの要因を解決してあげると次のようになる。
proc_hoge.sh
#!/bin/bash
find /path/to/hoge -type f -print0 | while IFS= read -r -d $'\0' x; do
echo "do something...: ${x}!!"
done
ファイル名が正しく取得できていることがわかる
./proc_hoge.sh
# => do something...: /path/to/hoge/bbb
# ccc!!
# do something...: /path/to/hoge/aaa !!
# do something...: /path/to/hoge/ddd\eee!!
解説
-
IFS=
としてreadを実行することで、フィールド分割されなくなっている - findに
-print0
を指定することで、改行区切りでなくヌル文字\0
区切りで出力されるようになる - readに
-d $'\0'
を指定することで、一行ずつでなくヌル文字\0
区切りで読み取るようになる - readに
-r
を指定することで、バックスラッシュがエスケープ文字として扱われなくなる
おまけ
上記の用にパイプ|
でつないでwhile文を実行している場合、
while文はサブシェルで実行されるため、外の変数を変更することができない。
#!/bin/bash
count=0
find /path/to/hoge -type f -print0 | while IFS= read -r -d $'\0' x; do
echo "do something...: ${x}!!"
count=$(($count + 1)) # ここでの変更はwhile文の外に影響しない
done
echo $count
# => 0
このような処理をしたいときは、代わりにプロセス置換<()
を用いてあげる必要がある。
#!/bin/bash
count=0
while IFS= read -r -d $'\0' x; do
echo "do something...: ${x}!!"
count=$(($count + 1)) # サブシェルで実行されていないため、ここでの変更が影響する
done < <(find /path/to/hoge -type f -print0)
echo $count
# => 3
まとめ
シェルでイレギュラーなケースまで網羅するのは面倒で難しい。