シェルスクリプトで名前にスペース(空白)を含むファイルがxargsでうまく処理できない場合の対処

Bash

はじめに

チームメンバから「名前にスペースを含むファイルをxargsでうまく取り扱えない」という相談を受けました。

調べたらすぐに解決する あれ だと思ったのですが、 find ではなく grep の実行結果を xargs に渡していたので、なるほどあまり見ないケースだなと思ったついでにエントリをまとめました。

検証環境

$ uname -moi
x86_64 MacBookPro16,1 Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.0.18(1)-release (x86_64-apple-darwin19.5.0)

$ grep --version
grep (BSD grep) 2.5.1-FreeBSD

$ find --version
find (GNU findutils) 4.7.0

"名前にスペース(空白)を含むファイルがxargsでうまく処理できない" とはどんな問題?

シェルあるあるではありますが 、実際にどんな問題なのか見ていきます。

まずは、ファイルを3つ作成します。それぞれ以下の名前とします。

  • hello1.txt
  • hello 2.txt
  • hello 3.txt
$ touch "hello1.txt"

$ touch "hello 2.txt"

$ touch "hello 3.txt"

ここで find コマンドを使ってファイルの一覧を xargs に渡して echo を実行させてみます。

-n 1 は標準入力の内容を1つずつ処理するオプションです。

$ find . -type f | xargs -n 1 echo

./hello
3.txt
./hello
2.txt
./hello1.txt

悲しいことに hello 2.txthello 3.txt というファイル名が分割して出力されました。 xargs は標準入力からもらった内容を 改行空白 で区切って処理するためです。

説明するだけで長くなりましたね。 これを解決するための方法を紹介します。

対処法 : find コマンドの実行結果をxargsで処理する場合

find コマンドには -print0 というオプションがあります。

オプションなしの場合は以下のように改行区切りでファイル名が出力されます。

$ find . -type f
./hello 3.txt
./hello 2.txt
./hello1.txt

ここで -print0 オプションを付与すると以下のように出力内容が変わります。

$ find . -type f -print0
./hello 3.txt./hello 2.txt./hello1.txt%

ターミナル上はわからないのですが、区切り文字が改行から null文字 ( \0 ) と言われる特殊な文字に変わります。

この状態で xargs に渡してみましょう。

$ find . -type f -print0 | xargs -n 1 echo
xargs: WARNING: a NUL character occurred in the input.  It cannot be passed through in the argument list.  Did you mean to use the --null option?
./hello
3.txt
2.txt

少しだけ出力内容が変わりました。 「xargs コマンド実行しているけど、 --null 文字つけたかったんじゃないの?」と。

xargs--null オプションを付与すると、デフォルトの区切り文字を 改行空白 ではなく null文字 ( \0 ) を使うようになります。

$ find . -type f -print0 | xargs --null -n 1 echo
./hello 3.txt
./hello 2.txt
./hello1.txt

ちなみに、 -0 オプションも同じ意味になります。

$ find . -type f -print0 | xargs --null -n 1 echo

./hello 3.txt
./hello 2.txt
./hello1.txt

ここまではよくあるケースですね。

対処法 : grep -l コマンドの実行結果を xargs で処理する場合

grep -l というコマンドで、 grep で検索して該当したファイルの名前だけを出力させることができます。

先程作成した3つのファイルに適当なデータを格納しておきます。

$ seq 10 > "hello1.txt"

$ seq 20 > "hello 2.txt"

$ seq 11 20 > "hello 3.txt"

ファイルの中身は以下のようになります。

$ cat hello1.txt
1
2
3
4
5
6
7
8
9
10

$ cat hello\ 2.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

$ cat hello\ 3.txt
11
12
13
14
15
16
17
18
19
20

ここで、 10 という数値を含むファイルだけを検索するためには以下のコマンドを実行します。

$ grep -l 10 *

hello 2.txt
hello1.txt

この結果を xargs に渡して echo しようとすると、 find と同じ問題に直面します。

$ grep -l 10 * | xargs -n 1 echo
hello
2.txt
hello1.txt

解決方法も find と同じです。 --null オプションというものが用意されており、これを利用します。

$ grep -l 10 * --null | xargs --null -n 1 echo
hello 2.txt
hello1.txt

tar コマンドにも xargs と同じようにnull文字を区切り文字として受け取るオプションがある

xargs だけではなく、 tar コマンドにも圧縮対象のファイルを標準入力から受け取る際に null文字 を区切り文字として利用するオプションがあります。

過去のエントリでもサラリと紹介していました

$ man tar | grep null -C 2
             again with HFS+ compression.

     --null  (use with -I or -T) Filenames or patterns are separated by null
             characters, not by newlines.  This is often used to read file-
             names output by the -print0 option to find(1).
--
--
     a character or block device such as a tape drive.  If the output is being
     written to a regular file, the last block will not be padded.  Many com-
     pressors, including gzip(1) and bzip2(1), complain about the null padding
     when decompressing an archive created by tar, although they still extract
     it correctly.

ひとこと

grep より高速と言われる ag コマンドというものがありますが、やはり同じようなオプションが用意されています。

$ ag --help | grep 'null'
  -0 --null --print0      Separate filenames with null (for 'xargs -0')

ファイル名を標準出力に出力するコマンドファイル名を標準入力から受け取るコマンド には、 大体区切り文字として null文字 を利用するためのオプションがあるのでしょう。

他にもコマンド、オプションを見つけた方はコメント下さい!

Bash