春学期の講義も、ほぼ終わりかけて、つかの間の休憩がとれた。書くつもりで、延び延びになっていた問題を扱う。
だいぶ前の話だが、mac/osx の mdfind を扱いやすくするためのスクリプトを作っていて、その中に xargs コマンドが使われていた。
mdfind は mac/osx の GUI ベースの検索ツール spotlight のコマンドインターフェースを提供する。僕にとっては便利な便利なコマンドである。
例えば
mdfind python
mdfind python | xargs grep pattern
しかし、これは時々間違える。mac/osx のファイル名には空白が含まれることがあるからである。
UNIX のコマンドには、ファイルの一覧をコマンド引数で与えると言う統一ルールがあるらしい。例えば grep の場合には
grep pattern file ...
mdfind python | xargs grep pattern
なお mdfind の出力が小さい場合には、必ずしも xargs が必要なわけではない。
grep pattern `mdfind python`
grep pattern "`mdfind python`"
mdfind python
「あるいは」と言ったけど、本当は最初の方法には問題がある。ファイル名に空白が含まれる場合には、第一の方法は正しく展開されず、リスクを覚悟しなくてはならない。(後に採り上げる)
コマンド引数がどのように展開されるのか? 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 と違い、空白を含む引数であるが、それとも別々の異なる引数であるかが分かるようにできている。
args は Lua で書けば次のようなものである。
#!/usr/local/bin/lua for n=1,#arg do print(n,arg[n]) endこれに相当するプログラムは大抵の言語で簡単に書けるはずである。
注: args は、引数の番号も出力しているが、番号を出力しなくてもよいなら、シェルスクリプでも簡単に書ける。しかし、その場合には、改行コードを含む引数を正しく表示できないであろう。)
さて、UNIX の伝統に反して、mac/osx では、しばしば空白文字 SP (文字コード 32) を含むファイル名やディレクトリ名が現れる。そのために mdfind が出力するパスの一覧の中にも空白文字が含まれるものが現れる。ところが、 xargs は1つの行の中に複数のパスを許している。その場合にはパスとパスの区切り文字は空白である。このままでは空白文字を含むパスを扱えない注1。そのためにxargs はファイル名の区切りを NUL 文字(文字コード 0)にするオプションを持っている。そして mdfind も xargs のこのオプションに対応して、出力するパスの一覧を NUL 文字で区切るオプション(-0 オプション)を持っている。このオプションを見ていると、フーと考え込んでしまう。なぜ、パスの区切りを改行文字にしなかったのか?
xargs のマニュアルを見ていると、xargs は結構多彩である。xargs へのインプットは必ずしもファイルのパスではないらしい。そのために、データの区切り文字を UNIX で伝統的な空白文字(SP, TAB, NL)に設定したのではないかと思われる。
"my file" 'your file' his\ file
"\$1 dollar" '$1 dollar' \$1\ dollar
確かに大丈夫と言えるのは、open 関数の引数として与えられる生のパス名だけであろう。しかし、その場合には、1つの行に複数のパスを書く事はできない。
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" によって見えなくなっている。このことは、いたずらが可能である事を示している。
UNIX ではファイル名に制御コードを埋め込めるが、もちろん実際にはそのようなファイルは作らない。可能だとしても作ればいろいろな問題をもたらすからである。制御コードではなく、空白文字ですら UNIX のツール群とのミスマッチをもたらす。そのために、(mac/osx を除いて) UNIX では空白文字を含むファイルやディレクトリを作らない習慣がある。
find コマンドは、膨大なファイルの中から、ある特性を持ったファイルを探し出してくれる。基本は探し出したファイルを1行に1ファイルで表示するが、多彩なオプションを持っている。その中の一つとして -print0 オプションがある。このオプションはファイルの一覧(相対パスあるいは絶対パス)の区切りを、(改行ではなく) NUL 文字にする。なぜ改行ではいけないのか? ファイル名あるいはディレクトリ名の中に改行文字(NL)が含まれている時の対策ではないかと思われる。そして、find のこのオプションが、xargs の -0 オプションと連携するのである。
しかし、それにしても、find コマンドの -print0 オプションは何のためにあるのか? このオプションを使って、ファイルの一覧を手に入れ、その中から、何かのツールを使って、セキュリティ対策のために、制御コードを名前に含む変なファイルの一覧を作ろうと言うのだろうか? しかし、もしそのような目的であれば、セキュリティチェックを行う専用のツールを持った方が良かろう。あるいは、-print0 オプションは制御コードを名前に含む変なファイルに生存権を与えようとしているのだろうか? 多分、後者である。
ネットの記事を見ていると、find コマンドの -print0 オプションは必ずしも、どの UNIX でもサポートされている訳ではない。当然ながら、サポートすべきではない、とする批判的な意見がある。
Mac の mdfind コマンドには、空白文字(SP)を含むパスの一覧を、xargs に渡すために、 -0 オブションを持っている。マニュアルの中に、このオプションを見て、僕は考え込んでしまったのだ。mdfind の -0 オプションは、空白文字を含むパスに対する正しい解決策だとは思えない。例えば
mdfind -0 python
xargs は、制御文字を含む変なパスはサポートする必要はないのである。xargs はまた、1つの行に複数のパスが含まれる入力をサポートする必要もないのである。UNIX のツールは、複数のパスを出力する時には、「1つの行に1つのパス」の原則を守るべきなのである。ls コマンドはこの原則を破っているのであるが、それでも、パイプに渡す時には、「1つの行に1つのパス」の原則を守っている。
xargs に見切りをつけて、結局、新しくツールを作る事にした。名前は 9xa である。Plan9 の xargs の意味である。今のところ機能は限定されている。
9xa のインプットは、"one file per line" のルールを守る。もちろん、各行には open 関数にそのまま与えられる生のパスを書く。使い方もシンプルで
9xa [-n number] [-f file] program arg ...
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);
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 が必要である。
xargs
が既にインプリメントされている。もちろん 9xa
と同じコンセプトに基づいている。ただし "-n
" の意味が異なる。(2021-09-18)