シェルで入出力のスペース区切りに悩まされたときの解決法

本記事では、コマンド間で文字列をやり取りするときに勝手にスペースで区切られてしまうことで起きる問題の解決法をまとめます。

# 前提とする環境
- Linux (Ubuntu)
- bash

Linux 周りに疎いため、これが必要十分かどうかはわかりかねます。(さすがにLinux以外だとコマンドの挙動が変わってくると思われますが、Ubuntu は絞りすぎかも。bash についても同様で他の sh でも同じかも。）


# 問題提起

問題の例を1つ挙げます。たとえば、以下のようなケースです。

```shell-session
$ 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` は入力を**スペースと改行で区切**っています[^xargs_split]。

これを明らかにするため、まず前半の `find . -type f` の出力を観察してみます。

```shell-session
$ 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`) に統一する

ググって真っ先に見つかった方法です。まずはこの方法を使用した例を提示します。

```shell-session
$ 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` |デフォルトでは改行とスペース区切り[^xargs_split]。 |
|`awk` |オプション引数 `-F '\0'` |フィールドの区切り文字を `-F` で指定できる。デフォルトではスペース区切り (改行はレコードの区切り)。これを指定しても `awk` で `'{print $1,$2}'` によって出力した際のフィールド区切り文字はスペース1字になる。 |
|`sed` |オプション引数 `-z` を指定する。 |`sed` は通常は行単位で処理するため改行で区切るが、この方法でヌル文字区切りにできる。これを使用すると `sed` の出力もヌル文字区切りのままになるので注意。 |

## 区切り文字を改行 (`\n`) に統一する
ヌル文字を区切り文字にする方法で解決できるのですが、パイプで受け渡していたデータを一度ファイル出力したい場合には不都合が生じます。以下の例では、ヌル文字区切りで出力した文字列を filelist.txt へ書き込んでいますが、その内容を `cat` で表示すると区切りが表示されません。

```shell-session
$ 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` だけ手を加えます。

```shell-session
$ 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'` オプションで**改行区切り**にすることで、区切り文字を統一しています。

# 終わりに

シェルでは「コマンドの引数はスペースで区切る」に始まり、スペースで文字列で区切る例が多々見られるように感じます。なので、文字列の入出力の際は下記のことに留意するようにしています。[^care]

- スペースが混ざることはないか
- スペースが混ざっても問題ないか

問題があれば上で述べたように区切り文字からスペースを除外することで解決できるか考えます。

for ループの区切り文字についても言及を考えましたが、まとまりを欠きそうなので割愛。(`while read` とか `IFS` とか)




# 参考
- [【linux】 スペースを含むファイル名を find | xargs で使う方法](https://www.softel.co.jp/blogs/tech/archives/5367)
- [【 xargs 】コマンド――コマンドラインを作成して実行する](https://www.atmarkit.co.jp/ait/articles/1801/19/news014.html)
- [【 awk 】コマンド（基本編その4）――テキストの加工とパターン処理、printとprintfの使い方](https://www.atmarkit.co.jp/ait/articles/1706/15/news021.html)





[^xargs_split]: 他の空白っぽい文字も区切りに使われているかも（未確認）
[^care]: 実際にはこの記事で主眼を置いていない問題が山ほどあるので、スペースだけケアすればよいわけではありません。同様のケアを必要に応じて他の文字にも行います。