find, mdfind, xargs, peco で空白入りのパス文字列を扱う

`find` と `xargs` の組み合わせで空白文字が入った行があるとハマる場合があります。

```shell-session
$ find /Library/Desktop\ Pictures -name Yosemite\*
/Library/Desktop Pictures/.thumbnails/Yosemite 2.jpg
/Library/Desktop Pictures/.thumbnails/Yosemite 3.jpg
/Library/Desktop Pictures/.thumbnails/Yosemite 4.jpg
/Library/Desktop Pictures/.thumbnails/Yosemite 5.jpg
/Library/Desktop Pictures/.thumbnails/Yosemite.jpg
/Library/Desktop Pictures/Yosemite 2.jpg
/Library/Desktop Pictures/Yosemite 3.jpg
/Library/Desktop Pictures/Yosemite 4.jpg
/Library/Desktop Pictures/Yosemite 5.jpg
/Library/Desktop Pictures/Yosemite.jpg
```

サムネイルディレクトリのパスは `grep -F -v /.thumbnails` で除きつつ

```shell-session
$ find /Library/Desktop\ Pictures -name Yosemite\* | grep -F -v /.thumbnails
/Library/Desktop Pictures/Yosemite 2.jpg
/Library/Desktop Pictures/Yosemite 3.jpg
/Library/Desktop Pictures/Yosemite 4.jpg
/Library/Desktop Pictures/Yosemite 5.jpg
/Library/Desktop Pictures/Yosemite.jpg
```

（`grep` の `-F` は「正規表現を使わない」、`-v` は文字列にマッチしたものを表示しないという意味です）

この5つのファイルについて `ls -l` の結果を見たい場合

```shell-session
$ find /Library/Desktop\ Pictures -name Yosemite\* | grep -F -v /.thumbnails | xargs ls -l
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: 2.jpg: No such file or directory
ls: 3.jpg: No such file or directory
ls: 4.jpg: No such file or directory
ls: 5.jpg: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite.jpg: No such file or directory
```

といった感じでエラーになってしまいます。

これは `find /Library/Desktop\ Pictures -name Yosemite\* | grep -F -v /.thumbnails` の結果を `xargs` によって空白類文字（改行文字や空白文字）で区切られた引数の列として `ls -l` に与えているからです（言葉にしようとすると少しややこしい…）

もっと結果が小さいとわかりやすい？

```shell-session
$ touch "toei bus"
$ touch "tokyo bus"
$ ls -l
total 0
-rw-r--r--  1 ogata  staff  0  7  3 21:30 toei bus
-rw-r--r--  1 ogata  staff  0  7  3 21:30 tokyo bus
$ find . -type f
./toei bus
./tokyo bus
$ find . -type f | xargs ls -l
ls: ./toei: No such file or directory
ls: ./tokyo: No such file or directory
ls: bus: No such file or directory
ls: bus: No such file or directory
```

2行が2引数になると思ったら、空白も引数の区切り文字になって4引数として解釈されてしまったという感じ。

これの解決には `find` の `-print0` オプション、そして `xargs` の `-0` オプションを使います。

```shell-session
$ find . -type f -print0 | xargs -0 ls -l
-rw-r--r--  1 ogata  staff  0  7  3 21:30 ./toei bus
-rw-r--r--  1 ogata  staff  0  7  3 21:30 ./tokyo bus
```

`find` は `-print0` によって、行ごとの出力を改行区切りではなくヌルバイト文字区切りとして出力し、`xargs` は入力を空白類文字ではなくヌルバイト文字で分割したそれぞれの文字列を指定のコマンド（この場合は `ls -l`）の引数の列とします。

Mac の Spotlight のコマンドラインインターフェースである `mdfind` にも `-0` というオプションがあります（`-print0` ではないことに注意）。

```shell-session
$ mdfind "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'"
/Library/Desktop Pictures/Yosemite.jpg
/Library/Desktop Pictures/Yosemite 5.jpg
/Library/Desktop Pictures/Yosemite 4.jpg
/Library/Desktop Pictures/Yosemite 3.jpg
/Library/Desktop Pictures/Yosemite 2.jpg
```

この出力を `ls -l` するとき

```shell-session
$ mdfind "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'" | xargs ls -l
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: /Library/Desktop: No such file or directory
ls: 2.jpg: No such file or directory
ls: 3.jpg: No such file or directory
ls: 4.jpg: No such file or directory
ls: 5.jpg: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite: No such file or directory
ls: Pictures/Yosemite.jpg: No such file or directory
```

と、先ほどの `find` と同様にうまくいかないものを

```shell-session
$ mdfind -0 "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'" | xargs -0 ls -l
-rw-r--r--  1 root  wheel   9993401  8 26  2015 /Library/Desktop Pictures/Yosemite 2.jpg
-rw-r--r--  1 root  wheel  12361649  8 26  2015 /Library/Desktop Pictures/Yosemite 3.jpg
-rw-r--r--  1 root  wheel  10539824  8 26  2015 /Library/Desktop Pictures/Yosemite 4.jpg
-rw-r--r--  1 root  wheel   4617954  8 26  2015 /Library/Desktop Pictures/Yosemite 5.jpg
-rw-r--r--  1 root  wheel  11914786  8 26  2015 /Library/Desktop Pictures/Yosemite.jpg
```

とすることでうまく渡すことができます

本題とは関係ありませんが、`mdfind` コマンドのクエリ文字列のキーは `mdls` コマンドで類推することができます。

```shell-session
$ mdls /Library/Desktop\ Pictures/Yosemite.jpg
_kMDItemOwnerUserID            = 0
kMDItemContentCreationDate     = 2015-08-26 00:13:34 +0000
kMDItemContentModificationDate = 2015-08-26 00:13:34 +0000
kMDItemContentType             = "public.jpeg"
kMDItemContentTypeTree         = (
    "public.jpeg",
    "public.image",
    "public.data",
    "public.item",
    "public.content"
)
kMDItemDateAdded               = 2016-03-09 16:42:27 +0000
kMDItemDisplayName             = "Yosemite.jpg"
kMDItemFSContentChangeDate     = 2015-08-26 00:13:34 +0000
kMDItemFSCreationDate          = 2015-08-26 00:13:34 +0000
kMDItemFSCreatorCode           = ""
kMDItemFSFinderFlags           = 0
kMDItemFSHasCustomIcon         = (null)
kMDItemFSInvisible             = 0
kMDItemFSIsExtensionHidden     = 0
kMDItemFSIsStationery          = (null)
kMDItemFSLabel                 = 0
kMDItemFSName                  = "Yosemite.jpg"
kMDItemFSNodeCount             = (null)
kMDItemFSOwnerGroupID          = 0
kMDItemFSOwnerUserID           = 0
kMDItemFSSize                  = 11914786
kMDItemFSTypeCode              = ""
kMDItemKind                    = "JPEG イメージ"
kMDItemLogicalSize             = 11914786
kMDItemPhysicalSize            = 11915264
kMDItemSupportFileType         = (
    MDSystemFile
)
```

画像を `find` もしくは `mdfind` で選別して、それを peco に渡して1つ選択してそれを Mac のデスクトップの壁紙にすることを考えてみます。

壁紙の変更は私が以前書いた「[Mac の JXA で現在のデスクトップの壁紙を変更する - Qiita](http://qiita.com/xtetsuji/items/6a238ddaba2dec509df8)」で解説した `macgui wallpaper` コマンドを使います。今回も、ファイルパスに空白文字列が含まれている場合を想定することにします。

`find` または `mdfind` と `peco` コマンドの間は、全て行指向の絞り込みなので `-print0` や `-0` の必要はありません。

```shell-session
$ mdfind "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'" | peco
/Library/Desktop Pictures/Yosemite.jpg
```

（上記結果は peco の絞り込みインターフェースで当該ファイルを選んだ場合）

ただ、この peco の絞り込み結果を `xargs` に渡すときには空白類文字を考慮する必要があります。

```shell-session
$ mdfind "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'" | peco | xargs macgui wallpaper
"/Library/Desktop" is not found
```

今回想定しているような `peco` で絞り込まれる結果が1行のみの場合は（壁紙画像指定は唯一つの画像ファイル）、`xargs` に `-0` オプションを与えることで「空白類文字は区切りではなく、それも通常の文字の一つとしてまとめて `macgui wallpaper` に与える」ということになり、うまくいくはずです。

```shell-session
$ mdfind "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'" | peco | xargs -0 macgui wallpaper
"/Library/Desktop Pictures/Yosemite.jpg
" is not found
```

あれ、うまくいかない……。

よく見ると「ファイルがない」と言われていますが、どうもファイル名の末尾に改行文字が付いたもの全体をファイル名として渡しているようです。

改行文字を落とすのであれば `perl -ne 'chomp if eof; print;'` というコマンドを `peco` の後ろのフィルタに入れることで、改行を落とすことができます。

```shell-session
$ mdfind "kMDItemContentTypeTree=public.image && kMDItemFSName='*Yosemite*'" | peco | perl -ne 'chomp if eof; print;' | xargs -0 macgui wallpaper
true
```

うまくいきました。

この末尾改行落としワンライナーをよく使うなら、スクリプトとして実行権限を付けてパスの通ったところに置いておくとよいでしょう。

```perl:chomp
#!/usr/bin/perl

use strict;
use warnings;

while (<>) {
    chomp if eof;
    print;
}
```

または

```perl:chomp
#!/usr/bin/perl -n
chomp if eof;
print;
```

`peco --help` の結果を見ると、`peco` には `--null` オプションがありますが、これが生きてくるのは peco の選択モードで Ctrl+Space を使い複数行を選択した場合です。`--null` オプションがある場合、`peco` は選択された複数行を改行区切りではなくヌルバイト文字区切りで出力します。パイプでつながれた場合には右側のコマンドがヌルバイト文字区切りの入力を受け取ります。

壁紙変更の例では一行のみの選択だったので、`--null` オプションの出番はありませんでしたが、`peco` で複数行選択する場合には `--null` オプションが活躍します。