スペースを含んだファイル名を bash で操作しようとした時にハマったので、メモ。
基本はls *.txt
などの出力を文字列として扱わず、ファイルパスとして扱えば良い。
例えば、下記のような bash があったとする。
touch a.txt
touch b.txt
for file in `ls *.txt`
do
ls "${file}"
done
このケースではちゃんと動くが、ファイル名にスペースを含んだ場合は動かなくなる。
touch I\ and\ you.txt
for file in `ls *.txt`
do
ls "${file}"
done
# 下記のように別々のファイルとみなされる
# ls: I: No such file or directory
# ls: and: No such file or directory
# ls: you.txt: No such file or directory
なぜ動かないかと言うとls *.txt
の出力が文字列として評価され、次のように展開されているから。たとえば、ls *.txt
をダブルクオーテーションとかで囲んでも同じエラーになる。
for file in I and you.txt
これの解決策をふたつ。まずは単純にワイルドカードを使う例。
touch I\ and\ you.txt
for file in *.txt
do
ls "${file}"
done
こうすれば文字列として認識されないので、1ファイル毎動く。ただ、これだと使いづらい。例えば find とかで見つけたファイルを処理する場合は、次のように行う。
touch a\ and\ b.txt
find . -name '*.txt' | while read file
do
ls "${file}"
done
シングルクォートを含んだファイルも同様に対応可能。
#シングルクォートのハイライトが変ですね Qiita さん
touch eiji\'s.txt
find . -name '*.txt' | while read file
do
ls "${file}"
done
しかし、ここで問題が発生する場合も。例えば、ファイル名を配列に入れたい場合に次のように記述すると失敗する。
touch a.txt
touch b.txt
touch a\ and\ b.txt
files=()
find . -name '*.txt' | while read file
do
files+=("${file}")
done
echo "${files[@]}"
これは、スペースが入ってるとかでは無くて、パイプの前後でプロセスが別になるため、files
にファイル名が足されないのが原因。
これを解決するには、パイプ出力を現在のシェル上のwhileに喰わせる上手いやり方が参考になる。
touch a.txt
touch b.txt
touch a\ and\ b.txt
files=()
while read file
do
files+=("${file}")
done < <(find . -name '*.txt')
echo "${files[@]}"
doneの後ろの<
と<
が離れているのがポイント。
見てしまえば、なるほどな、と思うが、思いつかないなー。
普通にperlとかに逃げそうw。