find と xargs の間に任意のコマンドを実行する (半角スペース対策)

(お知らせ:このブログはだいぶ昔に趣味的にやっていたものです。技術的な話題メインではないですが、現在は出版レーベルのシロイブックスを運営しているので、よければそちらもチェックしてみてくださいね!)

Linux や Mac などでコマンドを使用する際、よく出てくるのが「find で抽出したファイル一覧に対して xargs で処理を行う」というケースです。

このとき発生するやっかいな問題が、パスの半角スペース問題です。問題の具体例と、解決策1、さらに改良した解決策2について見ていってみましょう。

なお、せっかちな方のために先に情報を述べておきます。解決策1は -print0 オプションと -0 オプションを使用する方法です。この方法を既にご存知で、find と xargs の間に任意のコマンドを実行したくて困っているという方は、後半の解決策2をご覧ください。

半角スペース問題とは

一番簡単な例として、「find でカレントディレクトリ以下のファイル一覧を出力して、xargs コマンドに wc -c コマンドを渡し、ファイルサイズ一覧を出力する」という例を考えてみます。以下のようなコマンドになります。

find . -type f | xargs wc -c
# 注意: パスに半角スペースが含まれると正常動作しない

しかし、これは注意書きにあるように、パスに半角スペースが含まれるとうまく動作しません。xargs が半角スペースを区切り文字としてみなしてしまうため、たとえば「test file.txt」というパスがあった場合、「test」と「file.txt」の2つのパスがあると認識してしまうためです。

解決策1: NULL文字の使用

一つ目の解決策として、find と xargs に「NULL文字を区切り文字とする(半角スペースを区切り文字としない)」という指示を与える方法があります。find コマンドは通常、改行コードで区切られて出力されますが、改行コードの代わりにNULL文字という目に見えない文字を使用するというものです。

NULL文字を区切り文字とするには、find コマンドに -print0 オプションを、xargs コマンドに -0 オプションをそれぞれ渡します。具体的には、以下のようなコマンドになります。

find . -type f -print0 | xargs -0 wc -c
# このコマンドは正常動作する

しかし、この方法には少し問題があります。find と xargs の2つのコマンドの間でやりとりされている入出力情報は、先述のとおりNULL文字で区切られるようになります。そのため、改行コードを利用する通常のコマンドが2つのコマンドの間で使用できなくなってしまうのです。

具体的なケースを見てみましょう。先のコマンドに対して、「拡張子が .txt のテキストファイルだけを grep して処理したい」という例を考えてみます。 以下のコマンドは動作するでしょうか。

find . -type f -print0 | grep .txt$ | xargs -0 wc -c
# 注意:このコマンドは正常動作しない

注意書きにもあるとおり、このコマンドは正常動作しません。grep コマンドはあくまで改行コードを区切りとして動作するものであり、区切り文字がNULL文字になってしまっている入力を処理できないからです。

解決策2: ダブルクォーテーションの使用

上記の問題を避けるのが、パスを1行ずつダブルクォーテーションで囲むという方法です。この方法では、先述したNULL文字を区切り文字として使用するオプションは使用しません。

解決策1と同じく、「カレントディレクトリ以下のファイルから、拡張子が .txt のテキストファイルだけを抽出して、wc -c でファイルサイズを表示する」という例を考えてみます。以下のコマンドを見てみましょう。

find . -type f | grep .txt$ | awk '{print "\"" $0 "\""}' | xargs wc -c
# このコマンドは正常動作する

find でファイル一覧を出力したあと、grep で拡張子が .txt のテキストファイルのみに絞っているところまでは分かりますね。次に使用するのが、文字列処理を行う awk コマンドです。

上記の awk コマンドによって、入力されたすべての行の先頭と末尾にダブルクォーテーションを付与して出力しています。このようにダブルクォーテーションを付与することで、xargs コマンドがパス中の半角スペースを区切り文字として解釈してしまう問題を回避できます。

find、grep、xargs の各部分を好きなコマンドに変えることで、あなたの行いたい処理を記述してみてください。たとえば次の例は、find で出力したディレクトリ一覧から grep で「src」という文字列の含まれるディレクトリを抽出し、ls でファイル一覧を表示させるというコマンドです。

find . -type d | grep src | awk '{print "\"" $0 "\""}' | xargs ls

今回の記事の内容としましては以上です。