Logo address

xargs and 9xa

2012/07/12
2012/07/19 追加
2021/07/14 追加 9xa

春学期の講義も、ほぼ終わりかけて、つかの間の休憩がとれた。書くつもりで、延び延びになっていた問題を扱う。

mdfind と xargs

mdfind

だいぶ前の話だが、mac/osx の mdfind を扱いやすくするためのスクリプトを作っていて、その中に xargs コマンドが使われていた。
mdfind は mac/osx の GUI ベースの検索ツール spotlight のコマンドインターフェースを提供する。僕にとっては便利な便利なコマンドである。
例えば

mdfind python
を実行すると、パスの中に "python" が含まれているものや、内容に "python" が含まれているファイルの一覧(パスの一覧)をあっと言う間に表示する注1
そして、mdfind の出力をフィルタにかけたくなることがある。基本的には
mdfind python | xargs grep pattern
のような使い方をするスクリプトである。(もちろん、この程度に簡単なら、スクリプトを作らずに、直接実行する)

しかし、これは時々間違える。mac/osx のファイル名には空白が含まれることがあるからである。

注1: システムのファイルに関しては必ずしも表示されない。

xargs

UNIX のコマンドには、ファイルの一覧をコマンド引数で与えると言う統一ルールがあるらしい。例えば grep の場合には

grep pattern file ...
のような具合である。file の一覧を、標準入力から読み取ったり、ファイルから読み取ったりするオプションは grep には存在しない。そこで xargs が登場する。xargs は
mdfind python | xargs grep pattern
のように使い、標準入力から読み取ったファイルの一覧を grep の引数として grep を実行する。ここでは grep を実行させたが xargs は汎用に設計されていて、grep である必要はない。

なお mdfind の出力が小さい場合には、必ずしも xargs が必要なわけではない。

grep pattern `mdfind python`
あるいは
grep pattern "`mdfind python`"
のようにすれば、
mdfind python
の出力を grep の出力を grep の引数として展開してくれる。

「あるいは」と言ったけど、本当は最初の方法には問題がある。ファイル名に空白が含まれる場合には、第一の方法は正しく展開されず、リスクを覚悟しなくてはならない。(後に採り上げる)

args

2012/07/19

コマンド引数がどのように展開されるのか? echo コマンドでは展開結果が分かりにくいので筆者は args と言うコマンドを持っている。

-bash$ args abc def
1	abc
2	def
-bash$ args "abc def"
1	abc def
-bash$ x='abc def'
-bash$ args $x
1	abc
2	def
-bash$ args "$x"
1	abc def
-bash$ echo abc def
abc def
-bash$ echo "abc def"
abc def
-bash$ echo $x
abc def
-bash$ echo "$x"
abc def
-bash$
つまり args は、echo と違い、空白を含む引数であるが、それとも別々の異なる引数であるかが分かるようにできている。
sh や bash でスクリプトを書いていると、引数がコマンドラインで展開されてしまうような、問題のあるコードを書きやすい。(Plan9 の rc は、この点で良くできている)
UNIX本や、マニュアルにすら、そのようなサンプルが警告無しに書かれているのが現状である。展開のされ方を正確に捉えるには、args のようなツールが欠かせない。

args は Lua で書けば次のようなものである。

#!/usr/local/bin/lua
for n=1,#arg do
  print(n,arg[n])
end
これに相当するプログラムは大抵の言語で簡単に書けるはずである。

注: args は、引数の番号も出力しているが、番号を出力しなくてもよいなら、シェルスクリプでも簡単に書ける。しかし、その場合には、改行コードを含む引数を正しく表示できないであろう。)

ファイル名の中の空白文字 SP と xargs

さて、UNIX の伝統に反して、mac/osx では、しばしば空白文字 SP (文字コード 32) を含むファイル名やディレクトリ名が現れる。そのために mdfind が出力するパスの一覧の中にも空白文字が含まれるものが現れる。ところが、 xargs は1つの行の中に複数のパスを許している。その場合にはパスとパスの区切り文字は空白である。このままでは空白文字を含むパスを扱えない注1。そのためにxargs はファイル名の区切りを NUL 文字(文字コード 0)にするオプションを持っている。そして mdfind も xargs のこのオプションに対応して、出力するパスの一覧を NUL 文字で区切るオプション(-0 オプション)を持っている。このオプションを見ていると、フーと考え込んでしまう。なぜ、パスの区切りを改行文字にしなかったのか?

xargs のマニュアルを見ていると、xargs は結構多彩である。xargs へのインプットは必ずしもファイルのパスではないらしい。そのために、データの区切り文字を UNIX で伝統的な空白文字(SP, TAB, NL)に設定したのではないかと思われる。

注1: 扱えないとは言い過ぎかも知れない。しかし、パス名に空白文字を含む場合の UNIX のパスの扱いは複雑である。
生のパス名(open関数の引数)が "my file" の場合を例にとる
bash での扱い:
"my file"    'your file'   his\ file
この程度ならまだ簡単であるが、生のファイル名が "$1 dollar" なら
"\$1 dollar"    '$1 dollar'    \$1\ dollar
こうした表現法はシェルが決める部分であり、他のシェルであれば他の流儀がある。

確かに大丈夫と言えるのは、open 関数の引数として与えられる生のパス名だけであろう。しかし、その場合には、1つの行に複数のパスを書く事はできない。

UNIX のファイル名規則

UNIX のファイル名規則によると、ファイル名には "/" と NUL 文字以外はあらゆる文字が使える。制御文字だって使える! スラッシュ "/" はディレクトリの区切り文字だから、パス名は null 文字以外はどのような文字でも使える。だから、パス名を区切ることが可能なのは、NUL 文字だけであると考えたのだろうか?

UNIX のファイル名規則は、UNIX の病的な側面であり、これは克服されるべきものである。しかし現在に至っても克服されない。次の結果が UNIX のファイル名規則の危うさを示している。

bash$ touch $'ab\bc'
bash$ ls
9xa	9xa.c	9xa.o	README	ac	mkfile
bash$ ls *b*
ac
bash$ ls *b*|od -c
0000000    a   b  \b   c  \n
0000005
bash$

見かけ上 "ac" のように見えるファイル名には、"b" が含まれている。この "b" はバックスペース "\b" によって見えなくなっている。このことは、いたずらが可能である事を示している。

注釈: 制御文字を扱える bash の $'.....' は実際には殆ど使われていないようで、バグバグである。もっとも、bash は、このようなシンタックスをサポートする必要はないのだが、一応マニュアルには書かれている。

UNIX ではファイル名に制御コードを埋め込めるが、もちろん実際にはそのようなファイルは作らない。可能だとしても作ればいろいろな問題をもたらすからである。制御コードではなく、空白文字ですら UNIX のツール群とのミスマッチをもたらす。そのために、(mac/osx を除いて) UNIX では空白文字を含むファイルやディレクトリを作らない習慣がある。

find コマンドの -print0 オプション

find コマンドは、膨大なファイルの中から、ある特性を持ったファイルを探し出してくれる。基本は探し出したファイルを1行に1ファイルで表示するが、多彩なオプションを持っている。その中の一つとして -print0 オプションがある。このオプションはファイルの一覧(相対パスあるいは絶対パス)の区切りを、(改行ではなく) NUL 文字にする。なぜ改行ではいけないのか? ファイル名あるいはディレクトリ名の中に改行文字(NL)が含まれている時の対策ではないかと思われる。そして、find のこのオプションが、xargs の -0 オプションと連携するのである。

しかし、それにしても、find コマンドの -print0 オプションは何のためにあるのか? このオプションを使って、ファイルの一覧を手に入れ、その中から、何かのツールを使って、セキュリティ対策のために、制御コードを名前に含む変なファイルの一覧を作ろうと言うのだろうか? しかし、もしそのような目的であれば、セキュリティチェックを行う専用のツールを持った方が良かろう。あるいは、-print0 オプションは制御コードを名前に含む変なファイルに生存権を与えようとしているのだろうか? 多分、後者である。

ネットの記事を見ていると、find コマンドの -print0 オプションは必ずしも、どの UNIX でもサポートされている訳ではない。当然ながら、サポートすべきではない、とする批判的な意見がある。

mdfind の -0 オプション

Mac の mdfind コマンドには、空白文字(SP)を含むパスの一覧を、xargs に渡すために、 -0 オブションを持っている。マニュアルの中に、このオプションを見て、僕は考え込んでしまったのだ。mdfind の -0 オプションは、空白文字を含むパスに対する正しい解決策だとは思えない。例えば

mdfind -0 python
の出力を sort して、それを xargs に渡したいと思っても、sort ができない。xargs は欲張りすぎていて、1つの行に複数のパスを許している。その事が裏目に出ているのである。

xargs は、制御文字を含む変なパスはサポートする必要はないのである。xargs はまた、1つの行に複数のパスが含まれる入力をサポートする必要もないのである。UNIX のツールは、複数のパスを出力する時には、「1つの行に1つのパス」の原則を守るべきなのである。ls コマンドはこの原則を破っているのであるが、それでも、パイプに渡す時には、「1つの行に1つのパス」の原則を守っている。

UNIX は単機能の小さなプログラムを組み合わせて問題を解決して行くのに最適化されたシステムである。そのために UNIX は「再利用」について良く配慮されている。しかし、ls に関しては、出力を再利用しやすいように、もっと注意深い設計ができたのではないかと思える。
例えば日付の扱いやファイルの一覧をフルパスで得られない事など、もう少し工夫が欲しい。

9xa

xargs に見切りをつけて、結局、新しくツールを作る事にした。名前は 9xa である。Plan9 の xargs の意味である。今のところ機能は限定されている。
9xa のインプットは、"one file per line" のルールを守る。もちろん、各行には open 関数にそのまま与えられる生のパスを書く。使い方もシンプルで

9xa [-n number] [-f file] program arg ...
だけである。number は、program の arg 達に追加されるパスの個数を表す。default は 20 である。(これ以上大きくしても、速度の向上は期待できない)
9xa は、通常では標準入力からパスの一覧を読み取るが、-f オプションで file から読み取る事もできる。

mdfind と grep との組み合わせでは

mdfind python | 9xa grep pattern
のように使う。

他の使用例として、現在のディレクトリより下層にある(指定されたファイル名パターンの)ファイルの中に指定された文字列が含まれているか否かは、次のように簡単に判る

% lr -f * | grep '\.[ch]$'|9xa grep getvfsbyname
cmd/9pfuse/fuse.c:	if(getvfsbyname(v="osxfusefs", &vfs) < 0 &&
cmd/9pfuse/fuse.c:	   getvfsbyname(v="osxfuse", &vfs) < 0 &&
cmd/9pfuse/fuse.c:	   getvfsbyname(v="fusefs", &vfs) < 0){
cmd/9pfuse/fuse.c:		if(getvfsbyname(v, &vfs) < 0){
cmd/9pfuse/fuse.c:			werrstr("getvfsbyname %s: %r", v);
ここに行頭の "%" はプロンプトであり、この問題では文字列 "getvfsbyname" を含むファイルを調べている。"lr" は筆者のツールであり unix の "ls -1R" と異なり、1行に1つのファイルパスを書き出す。
lr は http://p9.nyx.link/netlib/cmd/lr/ に置かれている。

program の引数はスタックを通じで exec システムコールに渡される。従って number の最大値は OS とメモリの実装に依存する。最近は十分大きなメモリを積んでいるので、気にする話でもない。number はメモリの許す限り大きく採れるが、実際的な意味はないだろう。 9xa のプログラムは、最初はシステムパラメータの ARG_MAX を参照していたが、その価値無しと判断し削除した。(stack over flow になれば program の実行前に OS がエラー処理をするはずである)

9xa は、Plan9 の他、UNIX 系の OS でコンパイルできる。ただし Plan9port が必要である。


補注: 今頃気づいたが、9front には xargs が既にインプリメントされている。もちろん 9xa と同じコンセプトに基づいている。ただし "-n" の意味が異なる。(2021-09-18)