このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

ファイルシステムにおける名指しと解釈:XPathを使って

一昨日書いた記事、それと過去の2つの記事(↓)で扱っていることは、「プラットフォームやコマンドごとに、ファイル名/ディレクトリ名の解釈が違うよ」ってな話です。

僕は、このような違いを個別の事例としては知ってましたが、系統的に考えたことはなかったです。そこで、LinuxのようなPOSIXベースのシステムとWindowsにおいて、名指しの方法や名前の解釈がどう違うかを例示してみます。XPath言語の小さなサブセット言語を使った形式的記述も紹介します -- これが本題ですので、鬱陶しい実例を飛ばして「XPath言語のサブセット」へ飛んでもいいです。

内容:

  1. ファイルシステムのツリー構造
  2. Windowsのcopyにおけるファイル名/ディレクトリ名の扱い
  3. rsyncにおける末尾スラッシュの扱い
  4. XPath言語のサブセット
  5. コマンド仕様の正確な記述

ファイルシステムのツリー構造

たいていのOS/プラットフォームで共通していることは、ファイルシステムはツリー状に編成されていることです。ツリーのノードにファイルとディレクトリ(フォルダー)の区別があるのも共通です。

他の種類のノードとして、「便利な厄介者: ディレクトリを指すシンボリックリンク」で話題にしたシンボリックリンク、そしてハードリンクもあります。リンクは、「リンクそのもの」と「参照している先」という意味の二重性を持つので、難しい概念です。リンクの存在により、ファイルシステムが“ツリーではないグラフ構造”になってしまいますし。

今回は、難しいリンクは除外します。ツリーのノードはファイルかディレクトリとします。リンクによる別名がないので、ファイルシステムは本物のツリーの形になります(ルートからたどって合流はしない)。

リンクを考えない場合でも、ディレクトリ名の解釈はプラットフォーム/コマンドごとに多様です。コマンドの引数位置により解釈が違う、末尾スラッシュのあるなしで解釈が変わる。しかも、その説明が曖昧な自然言語でされていて、ホントのところはやってみないと分からない。もうヤンナッチャイます。以下に、ヤンナッチャウ実例を示します。

Windowsのcopyにおけるファイル名/ディレクトリ名の扱い

Linuxなどと比べて、Windowsで特徴的な点は:

  1. ファイル名を必要とする文脈でディレクトリ名を指定することができる。
  2. 複数のファイルを単一ファイルにコピーすることがある
  3. 明示的にディレクトリであることを示すために\を使う。

次のようなディレクトリ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と、他に次のノードセットを導入します。

  1. self -- ルートノードだけからなるノードセット
  2. child -- ルートの子ノード達からなるノードセット
  3. descendant -- ルート以外のすべてのノード達からなるノードセット
  4. descendant-or-self -- ルートも含めたすべてのノード達からなるノードセット


これらのノードセットは空になることがあります。例えば子のないルートでは、self以外のノードセットは空です。XPathにはない概念ですが、明示的に空ノードセットを表すために:

  • nothing -- ひとつもノードを含まない空なノードセット

ノードセットをさらに絞り込むためにノードテストを導入します。ノードテストは、ノードに対して真偽値を返す関数です。node(), file(), dir(), never() というノードテストを定義します。

  1. node() -- どんなノードに対しても常にtrueを返す
  2. file() -- ファイルノードに対してはtrue、ディレクトリノードに対してはfalseを返す
  3. dir() -- ファイルノードに対してはfalse、ディレクトリノードに対してはtrueを返す
  4. 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セレクターでも何でもいいのだけど、ノード状態/ノードセットの正確な記述を導入すれば、事情は改善すると思います。