本記事では、コマンド間で文字列をやり取りするときに勝手にスペースで区切られてしまうことで起きる問題の解決法をまとめます。
前提とする環境
- Linux (Ubuntu)
- bash
Linux 周りに疎いため、これが必要十分かどうかはわかりかねます。(さすがにLinux以外だとコマンドの挙動が変わってくると思われますが、Ubuntu は絞りすぎかも。bash についても同様で他の sh でも同じかも。)
問題提起
問題の例を1つ挙げます。たとえば、以下のようなケースです。
$ ls
'file ss.txt' file1.txt
$
$ find . -type f | xargs -r ls -l
ls: cannot access './file': No such file or directory
ls: cannot access 'ss.txt': No such file or directory
-rw-r--r-- 1 user group 0 Feb 27 21:50 ./file1.txt
find . -type f
でカレントディレクトリ以下のすべてのファイルを取得し、それらを ls -l
に引数として渡そうとしていますが、ls でエラーが発生しています。もしうまくいけば、file1.txt
だけでなく ls -l
の引数として渡されたすべてのファイルについて更新日時や所有者などの情報が表示されるはずです。
原因
原因を端的に言えば、find
の出力と xargs
の入力の解釈に齟齬があるためです。find
は複数のファイルパスを改行区切りで出力しているのに対し、xargs
は入力をスペースと改行で区切っています1。
これを明らかにするため、まず前半の find . -type f
の出力を観察してみます。
$ find . -type f
./file ss.txt
./file1.txt
このように、find . -type f
では改行区切りでファイルパスを出力しています。この標準出力が xargs
コマンドによって引数として ls -l
に与えられるので、気持ちとしては次のようになることを期待しています。
- コマンド:
ls
- 第一引数:
-l
- 第二引数:
./file ss.txt
- 第三引数:
./file1.txt
ところが、実際にはエラーが表示され、その内容を読むと「./file が存在しない」「ss.txt が存在しない」と言われていることから、次のようになってしまっていることがわかります。
- コマンド:
ls
- 第一引数:
-l
- 第二引数:
./file
- 第三引数:
ss.txt
- 第四引数:
./file1.txt
つまり、xargs がスペース&改行区切りと解釈してしまっているのです。
解決法
上述の通り原因が「出力と入力で区切り文字が違う」ことなので、まっとうな解決法は「区切り文字を統一する」ということになります。これを基本方針として解決法を並べます。
区切り文字をヌル文字 (\0
) に統一する
ググって真っ先に見つかった方法です。まずはこの方法を使用した例を提示します。
$ find . -type f -print0 | xargs -r -0 ls -l
-rw-r--r-- 1 user group 0 Feb 27 21:50 './file ss.txt'
-rw-r--r-- 1 user group 0 Feb 27 21:50 ./file1.txt
出力側の find
では見つかったファイルの出力方法を指定できるのですが、-print0
によって「ヌル文字区切りで出力する」という指定になります。それを受け取る側の xargs
では -0
を指定することで、入力文字列をヌル文字で区切って、そのそれぞれを引数として後続のコマンドに受け渡します。それ以外の文字は区切りと解釈されません。このように出力と入力で区切り文字を統一できているので、意図したとおり find
で見つけたすべてのファイルについて ls -l
の引数として使用されています。
ヌル文字を区切りに指定する方法は各コマンドで用意されています。
出力側
コマンド | 方法 | 補足 |
---|---|---|
find |
引数で -print0 を指定する。 |
find では出力フォーマットを指定できる。デフォルトでは改行区切り。 |
echo |
オプション引数 -en を指定したうえで区切り位置に \0 を入れる。(例: echo -en 'abc\0ABC\0' ) |
デフォルトで末尾に入る改行は -n オプションで消せる。-e オプションを指定するとエスケープシーケンスを解釈するようになるので、引数内の \0 がヌル文字と解釈される。スペースが勝手にヌル文字に変換されるわけではないので注意。 |
sed |
オプション引数 -z を指定する。 |
sed は通常行単位で処理するため改行で区切るが、この方法でヌル文字区切りにできる。これを使用すると sed への入力もヌル文字区切りとして解釈されるため注意。 |
入力側
コマンド | 方法 | 補足 |
---|---|---|
xargs |
オプション引数 -0
|
デフォルトでは改行とスペース区切り1。 |
awk |
オプション引数 -F '\0'
|
フィールドの区切り文字を -F で指定できる。デフォルトではスペース区切り (改行はレコードの区切り)。これを指定しても awk で '{print $1,$2}' によって出力した際のフィールド区切り文字はスペース1字になる。 |
sed |
オプション引数 -z を指定する。 |
sed は通常は行単位で処理するため改行で区切るが、この方法でヌル文字区切りにできる。これを使用すると sed の出力もヌル文字区切りのままになるので注意。 |
区切り文字を改行 (\n
) に統一する
ヌル文字を区切り文字にする方法で解決できるのですが、パイプで受け渡していたデータを一度ファイル出力したい場合には不都合が生じます。以下の例では、ヌル文字区切りで出力した文字列を filelist.txt へ書き込んでいますが、その内容を cat
で表示すると区切りが表示されません。
$ find . -type f -print0 > filelist.txt
$
$ cat filelist.txt
./file ss.txt./file1.txt
$
$ xargs -r -0 ls -l < filelist.txt
-rw-r--r-- 1 user group 0 Feb 27 21:50 './file ss.txt'
-rw-r--r-- 1 user group 0 Feb 27 21:50 ./file1.txt
これを回避するためには、ヌル文字以外で区切りを統一する必要が出てきますが、もっとも単純に考えれば改行区切りで統一するのが筋でしょう (問題提起の段階では改行区切りを想定していましたし)。
出力側の find
は何も考えなければ改行区切りになるので、入力側の xargs
だけ手を加えます。
$ find . -type f > filelist.txt
$
$ cat filelist.txt
./file ss.txt
./file1.txt
$
$ xargs -r -d '\n' ls -l < filelist.txt
-rw-r--r-- 1 user group 0 Feb 27 21:50 './file ss.txt'
-rw-r--r-- 1 user group 0 Feb 27 21:50 ./file1.txt
上のケースでは出力側はデフォルトのまま改行区切りにし、入力側を -d '\n'
オプションで改行区切りにすることで、区切り文字を統一しています。
終わりに
シェルでは「コマンドの引数はスペースで区切る」に始まり、スペースで文字列で区切る例が多々見られるように感じます。なので、文字列の入出力の際は下記のことに留意するようにしています。2
- スペースが混ざることはないか
- スペースが混ざっても問題ないか
問題があれば上で述べたように区切り文字からスペースを除外することで解決できるか考えます。
for ループの区切り文字についても言及を考えましたが、まとまりを欠きそうなので割愛。(while read
とか IFS
とか)
参考
- 【linux】 スペースを含むファイル名を find | xargs で使う方法
- 【 xargs 】コマンド――コマンドラインを作成して実行する
- 【 awk 】コマンド(基本編その4)――テキストの加工とパターン処理、printとprintfの使い方