Quantcast
Viewing all articles
Browse latest Browse all 2722

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

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

前提とする環境

  • Linux (Ubuntu)
  • bash

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

問題提起

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

$ls'file ss.txt'   file1.txt
$$find .-type f | xargs -rls-lls: 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-0ls-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-0ls-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とか)

参考


  1. 他の空白っぽい文字も区切りに使われているかも(未確認) 

  2. 実際にはこの記事で主眼を置いていない問題が山ほどあるので、スペースだけケアすればよいわけではありません。同様のケアを必要に応じて他の文字にも行います。 


Viewing all articles
Browse latest Browse all 2722

Trending Articles