一昨日書いた記事、それと過去の2つの記事(↓)で扱っていることは、「プラットフォームやコマンドごとに、ファイル名/ディレクトリ名の解釈が違うよ」ってな話です。
僕は、このような違いを個別の事例としては知ってましたが、系統的に考えたことはなかったです。そこで、LinuxのようなPOSIXベースのシステムとWindowsにおいて、名指しの方法や名前の解釈がどう違うかを例示してみます。XPath言語の小さなサブセット言語を使った形式的記述も紹介します -- これが本題ですので、鬱陶しい実例を飛ばして「XPath言語のサブセット」へ飛んでもいいです。
内容:
ファイルシステムのツリー構造
たいていのOS/プラットフォームで共通していることは、ファイルシステムはツリー状に編成されていることです。ツリーのノードにファイルとディレクトリ(フォルダー)の区別があるのも共通です。
他の種類のノードとして、「便利な厄介者: ディレクトリを指すシンボリックリンク」で話題にしたシンボリックリンク、そしてハードリンクもあります。リンクは、「リンクそのもの」と「参照している先」という意味の二重性を持つので、難しい概念です。リンクの存在により、ファイルシステムが“ツリーではないグラフ構造”になってしまいますし。
今回は、難しいリンクは除外します。ツリーのノードはファイルかディレクトリとします。リンクによる別名がないので、ファイルシステムは本物のツリーの形になります(ルートからたどって合流はしない)。
リンクを考えない場合でも、ディレクトリ名の解釈はプラットフォーム/コマンドごとに多様です。コマンドの引数位置により解釈が違う、末尾スラッシュのあるなしで解釈が変わる。しかも、その説明が曖昧な自然言語でされていて、ホントのところはやってみないと分からない。もうヤンナッチャイます。以下に、ヤンナッチャウ実例を示します。
Windowsのcopyにおけるファイル名/ディレクトリ名の扱い
Linuxなどと比べて、Windowsで特徴的な点は:
- ファイル名を必要とする文脈でディレクトリ名を指定することができる。
- 複数のファイルを単一ファイルにコピーすることがある
- 明示的にディレクトリであることを示すために\を使う。
次のようなディレクトリfoo\を考えましょう。
.\ | +--- foo\ | a.txt | b.txt | +--- sub\ c.txt
この状況で、copy foo bar
というコマンドの意味を解釈してみます。
まず、copyコマンドは、ファイルを対象とするコマンドです。copyには、ディレクトリをコピーするという発想はありません。したがって、ディレクトリfoo\を第一引数とするのはエラーとしてもいいはずです。でもここで、親切心(余計なお世話)が介在します。
ディレクトリfoo\は、foo\*と再解釈されて、foo\の直下のファイル群に展開されます。この例では、a.txt と b.txt です。そして、この2つのファイルがfooにコピーされます。
「fooにコピーされます」がまた厄介で、Linuxのcpとは違い、複数のファイルを単一のファイルにコピーする動作がデフォルトになっています。barが存在しないなら、ファイルbar(ディレクトリbar\ではない!)を作って、2つのファイルを結合(concatenate)します。
念の為に注意しておくと、ファイルbarの末尾には、Ctrl-Zという前世紀の遺物が付きます。/bオプションを付けてCtrl-Zを抑制してください。
copyの第二引数barが既存ディレクトリであるとき、a.txtとb.txtはbar\の下にコピーされます。barがディレクトリであると期待しているときは、copy foo bar\
と明示的に\を付けておけば、barをファイル名と解釈されることを防げます。
と、グダグダ説明しましたが、こんな自然言語(日本語)の説明って鬱陶しいですよね。改善したい。でもその前にもうひとつの実例を。
rsyncにおける末尾スラッシュの扱い
Linuxにおいては、ディレクトリ名末尾のスラッシュはあってもなくても同じことが多いのですが、スラッシュあるなしを積極的に利用している例もあります。
rsyncはコピーコマンドで、名前に反してローカルのファイルコピーにも使えます。rsync -r foo bar
とすると、ディレクトリbar/がなければ作り、その下に bar/foo/ から始まるツリーをそっくりコピーします。
一方、末尾スラッシュを付けた rsync -r foo/ bar
では、ディレクトリbar/の下がfoo/の下と一致するようにコピーがされます。
って、分かりますか? だいぶ分かりにくいと思います。改善したい。
XPath言語のサブセット
だいたいねー、こんなことを自然言語で説明するのが間違ってますよ。正確な記述と伝達が可能な形式言語を使うべき!
ツリーのノードセットを記述するなら、XPath言語が好適です。XPathについては次の2つの記事で書いてますが、とても小さなサブセットしか使わないので、この記事内の説明だけでも十分でしょう。
構文としてはXPathを使いますが、話はうんと単純です。ツリーが与えられたとき、ルートノード(相対的なルート)を基準に考えるので、ルートノード(だけからなるノードセット)をselfと呼びます。selfと、他に次のノードセットを導入します。
- self -- ルートノードだけからなるノードセット
- child -- ルートの子ノード達からなるノードセット
- descendant -- ルート以外のすべてのノード達からなるノードセット
- descendant-or-self -- ルートも含めたすべてのノード達からなるノードセット
これらのノードセットは空になることがあります。例えば子のないルートでは、self以外のノードセットは空です。XPathにはない概念ですが、明示的に空ノードセットを表すために:
- nothing -- ひとつもノードを含まない空なノードセット
ノードセットをさらに絞り込むためにノードテストを導入します。ノードテストは、ノードに対して真偽値を返す関数です。node(), file(), dir(), never() というノードテストを定義します。
- node() -- どんなノードに対しても常にtrueを返す
- file() -- ファイルノードに対してはtrue、ディレクトリノードに対してはfalseを返す
- dir() -- ファイルノードに対してはfalse、ディレクトリノードに対してはtrueを返す
- never() -- どんなノードに対しても常にfalseを返す
念の為に真偽表を書いておくと:
ノードテスト | テスト対象のノード | 真偽値 |
---|---|---|
node() | ファイル | true |
node() | ディレクトリ | true |
file() | ファイル | true |
file() | ディレクトリ | false |
dir() | ファイル | false |
dir() | ディレクトリ | true |
never() | ファイル | false |
never() | ディレクトリ | false |
与えられたノードセットを、ノードテストでフィルタリングした結果のノードセットを、<与えられたノードセット>::<ノードテスト> の形で書きます。ノードテストに引数を渡す形は使いません(XPath構文についてはコチラ)。
以下は、child::file()、descendant::file()、descendant-or-self::dir()、descendant-or-self::never() を図示したものです。
与えられたノードセットが何であっても、次が成立します。
- <与えられたノードセット>::node() = <与えられたノードセット>
- <与えられたノードセット>::never() = nothing
コマンド仕様の正確な記述
ファイルシステムのノードセットが正確に記述できれば、コマンドの仕様も正確に記述できます。rsyncの例を見てみましょう。
rsync -r foo bar
の仕様は、
fooの状態 | barの状態 | fooのノードセット | 挙動 |
---|---|---|---|
dir | never | descendant-or-self | fooのノードセットを、新しく作ったbarの下にサブツリーとしてコピー |
dir | dir | descendant-or-self | fooのノードセットを、既存のbarの下にサブツリーとしてコピー |
dir | file | descendant-or-self | エラー |
fooの状態、barの状態には、ノードテストと同じ記号を使っています。この場合のneverは名前が指すノードが存在しないことです。fooの状態、barの状態のすべての組合せの列挙はしていません。rsync -r foo/ bar
ならば、foo/が意味するノードセットをdescendantと解釈します。
copy foo bar
に関しては、
fooの状態 | barの状態 | fooのノードセット | 挙動 |
---|---|---|---|
file | never | self | fooの単一ノードを、新しく作ったファイルbarとしてコピー |
dir | never | child::file() | fooのノードセットを、結合して新しく作ったファイルbarとしてコピー |
dir | dir | child:file() | fooのノードセットを、既存のbarの下にコピー |
dir | file | child::file() | fooのノードセットを、結合してファイルbarとして上書きコピー |
このテの話がややこしくなるのは、同じ指定やスラッシュの違いだけで、その解釈が descendant-or-self, descendant, self, child::file() のように変わるからです。そしてさらに鬱陶しいのは、ノード状態やノードセットを正確に記述できる記述言語がないことです。
XPathでもCSSセレクターでも何でもいいのだけど、ノード状態/ノードセットの正確な記述を導入すれば、事情は改善すると思います。