natural born minority
ファイル整理で、ファイルやディレクトリ名の特定文字を置換したり、連番振ったり…というシチュエーションは多く在ると思います。
そういう時に役立つのが、find
, ls
やそれと組み合わせた rename
,sed
, for
, xargs
などのコマンド群だと思います。
…ですが、forで回す際に「ファイル名に半角スペースが含まれる」場合、多くの場合うまくいきません。
それは、bash
の「標準の区切り文字」を半角スペースだと認識しているためです。
例えば、 a b c.txt
を a
, b
, c.txt
というような分解で、次のコマンドへと送るためです。
これを考慮し「予め半角スペースを含むディレクトリ・ファイル名をなくしておく」という「下ごしらえのスクリプト」を作成しました。
やりたいことは「自身の居るディレクトリパス以下を再帰的に掘り進んで、ディレクトリ・ファイル名から、半角スペースをアンダースコアに置換したい」だとします。
#!/usr/bin/env bash
pre_ifs=$IFS
IFS=$'\n'
for i in $(find $(pwd) -type d | sort -r) ; do
cd ${i}
rename 's/ /_/g' ./*
done
IFS=$pre_ifs
まず、環境変数の IFS
ですが、これは「Internal Field Separator:フィールドセパレータ環境変数」と呼ばれるもので、コマンドの解釈として「何を区切り文字にするか」を決めているものです。
これが「半角スペース」になっていることが、半角スペース入りのファイル名を扱えない理由なので、元の値を保存しておいて、改行に変更しています。
find
を使って「ディレクトリのみを再帰的に取得」しています。
その際、相対パスでなく絶対パスとなるように、pwd
で現在パスを指定します。
そして、sort -r
でそれを逆順にし、for
に渡ってくるようにしています。
これは、例えばファイル
~/a b/
/c d/
/e f/
/g h/
みたいな構成だった場合、ソートしない場合
/home/kazuhito/a b/
/home/kazuhito/a b/c d/
/home/kazuhito/a b/c d/e f/
/home/kazuhito/a b/g h/
と渡ってきてしまい、/c d/
がリネームされた後、/c d/e f/
をリネームすることとなり「そんなディレクトリはない」ので失敗するからです。
そして、渡ってきたディレクトリパスに移動し、rename
コマンドにより、直下のディレクトリ・ファイル名を半角スペースからアンダースコアに置換しています。
環境によっては rename
コマンドが無いかもしれません。
その場合は sed
や perl
など、より多くの環境に入っているコマンドで置き換え操作をすれば良いと思います。
sed
の例
cd ${i}
# rename 's/ /_/g' ./* これが出来ないので…
for j in $(ls) ; do
mv ${j} $(echo ${j} | sed -e 's/ /_/g')
done
できれば「ワンライナーかつシンプルな構成にしたい」と思っていたのですが、自分にはこれが限界でした。
これの「下ごしらえ」さえ済んでしまえば、あとはフツーのワンライナーとかで名前置換が容易なのですが…これを一撃でするコマンドやワンライナーは、(自分の検索力では)見つけることが出来ませんでした。
もっと「こんな簡単にできるよ」や「定石としてはすでにこんなんがあるよ」など、ご存知の方がいらしたら、ぜひお教えいただけるとうれしいです。
記事公開後、Twitterからご助言をいただきました。
記事を拝見しましたが、やりたいことはスペースが含まれたファイル名を扱うことですよね?それだけならさほど難しくはないと思います。おそらく変数名をダブルクォートで括ってないのが足りてないピースです
— Koichi Nakashima (@ko1nksm) December 5, 2021
ファイル名に改行が含まれている場合に対応するとなるともうひと工夫必要ですが…
改行文字が含まれたファイル名を考慮しないという前提の場合「for と IFS=$'\n' の組み合わせ」または「while IFS= read -r ...を使った方法」のどちらか使うことになります。どちらでもIFSを使いますが後者であればIFSを保存して戻す作業が不要になります。
— Koichi Nakashima (@ko1nksm) December 5, 2021
なるほど、 while IFS='' read -r ...
でやれば良いんですね。実際に動かしてみましょう。
#!/usr/bin/env bash
find $(pwd) -type d | sort -r | while IFS='' read -r i; do
cd "${i}"
rename 's/ /_/g' ./*
done
これであれば IFS
の状態を管理したり気にしなくて良いですし、幾分素直なループ文になりますね。
これからは、こちらを常用していきたいと思います。お教えいただきありがとうございました。