merge-pathnamesについて勘違いしていたこと

この記事は, [Lisp Advent Calendar 2019](https://qiita.com/advent-calendar/2019/lisp)の3日目の記事です.

この記事のライセンスは[CC-BY](https://creativecommons.org/licenses/by/4.0/deed.ja)とします.

今回の記事では, ファイルのパスについてCommon Lispで扱う際に用いる`merge-pathnames`について,
最近, 私が勘違いした使い方をしていたのでそれについて書き残して置くことにしたいと思っています.

## TL;DR

Common Lisp HyperSpec[^clhs]より引用します. [^clhs-merge-pathnames]

[^clhs]: [LispWorks](http://www.lispworks.com/)によって提供されているCommon Lispの仕様書.
[^clhs-merge-pathnames]: http://www.lispworks.com/documentation/HyperSpec/Body/f_merge_.htm

> Constructs a pathname from pathname by filling in any unsupplied components with the corresponding values from default-pathname and default-version.

第一引数のパスネームの与えられていないの構成要素(スロット)を, それぞれ対応する第二引数と第三引数(バージョン)の値で埋めることで, パスネームを構築する.

あと, ちょっと雑な説明だけど, 第一引数が相対パスだったら, ディレクトリの部分は第二引数に第一引数を連結する.

## パス

プログラムを書いている時に, ファイルの在り処 [^path] を指し示したいということはよくあることだと思います.

[^path]: これは比喩なのかどうなのか分からない.

例えば, コンパイラの様な, あるファイルの内容を別のファイルに変換したいと言うような場合は,
ユーザからの入力(ターミナル上での入力や設定ファイルに書かれた文字列など)から, 入力ファイルの在り処を特定して読み込みを行い,
また出力するファイルの在り処をどこにするのかということも同様に特定する必要があります.

この様な時に, 次のような表現を用いることが多いと思います.
Unix系のOSであれば`/hoge/piyo/fuga`, Windowsでは`C:\hoge\piyo\fuga`などの文字列を見たことがある人は多いでしょう.
これをパスと呼ぶようです. [^word-path]

[^word-path]: ファイルシステムにおける各ファイルの識別子の文字列表現のことだと思う. 提喩で用いられていることも多い気がするので, ホントの所指しているのが何かは分からない. 筆者の知識の無さが現れる注釈だ.

ところで, この例のようにファイルの在り処を示す標準的な表現はOS(ファイルシステム)毎に違っています.
つまり, 何も仕組みなく書くと, ファイルシステム毎にパスを用意しなければならず大変です.
そこで, 少なくない言語で, パスを抽象的に扱う方法を提供している用に思います.

## pathname

さて, ここからCommon Lispの話に移ります.

Common Lispでは, 前節で述べたパスの抽象化としてpathnameという型があります.

Common Lisp HyperSpecには,

> pathname n. an object of type pathname, which is a structured representation of the name of a file.
> A pathname has six components: a host, a device, a directory, a name, a type, and a version.

とあります. (改行はこの記事の著者が入れました. また, マークアップされないように一部引用符を取り除きました.) [^pathname]

[^pathname]: http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_p.htm#pathname

Common Lispではこれを用いて, 何というディレクトリ内のどの名前のファイルということを指し示します.

例えば, 以下のような構造を持ったものになります.

```common-lisp:repl
CL-USER> (let ((p (pathname "/home/user/hoge/piyo.lisp")))
           (format t "~{~A~%~}" (list (pathname-directory p)
                                      (pathname-name p)
                                      (pathname-type p))))
(ABSOLUTE home user hoge)
piyo
lisp
NIL
```

最後のNILは, letこの場合formatの返り値です. `pathname-foo`というのはアクセサです.
WindowsでCドライブ内のというような`C:`は`device`というスロット[^member]に値が入ります.

[^member]: 他の言語ではメンバやプロパティなんて呼ばれることもあるとおもうアレのこと. 構造体を構成する要素のこと.

## merge-pathnames

### パスの組み立てをしたい

やっと本題です.

あるアプリケーションが特定のディレクトリとその中の特定のファイルを扱うことはよくあることだと思います.

例えば,

- `nｐｍ`[^npm]のプロジェクトのディレクトリと`package.json`
- `git`[^git]のプロジェクトのディレクトリと`.git/`

[^npm]: https://www.npmjs.com/ Node.jsのパッケージマネージャ
[^git]: https://git-scm.com/ バージョン管理ツール

などです.

### 例

`/hoge/piyo/`というディレクトリに, `fuga.txt`という名前のファイルを作成したいとしましょう.

`#p"/hoge/piyo/"`というディレクトリのパスと, `#p"fuga.txt"`というパス(相対パス)と`merge-pathnames`関数を使います.
次のコードを見てください. (例は今後筆者の環境[^env]での場合で書きます.)

[^env]: OS: Ubuntu 19.10 eoan, Kernel: x86_64 Linux 5.3.0-23-generic, SBCL 1.4.4

```common-lisp:repl
CL-USER> (merge-pathnames #p"/hoge/piyo/" #p"fuga.lisp")
#P"/hoge/piyo/fuga.lisp"
```

やったー, 出来たー.

と喜ぶにはまだ早いのですが, うまくパスが組み立てられたように見えます.

### 別の例

今度は`/hoge/piyo/`というディレクトリの中の, サブディテクトリ`foo`の中に, `fuga.txt`という名前のファイルを作成したいとしましょう.
先程の例と同じ様に書いてみます.

```common-lisp:repl
CL-USER> (merge-pathnames #p"/hoge/piyo/" #p"foo/fuga.lisp")
#P"/hoge/piyo/fuga.lisp"
```

あれ? `foo`ディレクトリが反映されません.

というわけで先程の例が糠喜びであると分かります. (まあ、間違いとまでは言えませんが.)

### 結局, merge-pathnamesって何してんのん？

冒頭にも書きましたが, Common Lisp HyperSpecより引用します.

> Constructs a pathname from pathname by filling in any unsupplied components with the corresponding values from default-pathname and default-version.

日本語にすると, 『与えられていないの構成要素(スロット)を, default-pathname(第二引数)とdefault-version(第三引数)からの対応する値で埋めることで, パスネーム(第一引数)からパスネームを構築する.』ということでしょうか.　(()内は著者が補った.)

#### 最初の例

最初の例をもう一度見ます.

```common-lisp:repl
CL-USER> (merge-pathnames #p"/hoge/piyo/" #p"fuga.lisp")
#P"/hoge/piyo/fuga.lisp"
```

この例では, 第一引数にはnameとtypeのスロットは与えられていませんでした(typeはこの場合拡張子).

一方で, 第二引数のパスネームはnameが`"fuga"`で,typeが`"lisp"`ですので, これらの値で第一引数の該当するスロットを埋めて, 返り値となるパスネームを構築します.
そこで, `#P"/hoge/piyo/fuga.lisp"`が返ってきます.

#### 第二の例

第二の例をもう一度見ます.

```common-lisp:repl
CL-USER> (merge-pathnames #p"/hoge/piyo/" #p"foo/fuga.lisp")
#P"/hoge/piyo/fuga.lisp"
```

この例でも, 第一引数にはnameとtypeのスロットは与えられていません.
この場合も, 第一の例と同じ様に, 第二引数のname`"fuga"`とtype`"lisp"`を第一引数のnameとtypeに埋めたものが返ってくるはずです.
一方, 第一引数のdirectoryスロットは空ではないので, `#P"/hoge/piyo/fuga.lisp"`が返ってきます.

#### 第二の例で本当にやりたかったこと

ところで, このままでは, 第二の例で本当にやりたかったことが`merge-pathnames`で実現できなさそうに見えます.

実は, `merge-pathnames`のこの動作には例外があります. 第一引数のdirectoryが相対パスを表し, 第二引数のパスネームのdirectoryが空でなければ,
第二引数のディレクトリからの相対パスとしての第一引数のディレクトリが指すディレクトリが返り値のディレクトリになります.

つまり, 以下のように書けばうまく行きます.

```common-lisp:repl
CL-USER> (merge-pathnames #p"foo/fuga.lisp" #p"/hoge/piyo/")
#P"/hoge/piyo/foo/fuga.lisp"
```

その他の細かい動作に付いては, [CLHSのmerge-pathnames](http://www.lispworks.com/documentation/HyperSpec/Body/f_merge_.htm)を読んでください.

## まとめ

- Common Lispのmerge-pathnames関数はただ文字列をくっつけるという様な関数ではない
- 基準となるディレクトリとそこからの相対パスを使って, 絶対パスや親ディレクトリまで含んだ相対パスを構成したい際には, 第一引数に相対パス, 第二引数に基準と成るパスを渡す.
- CLHSにちゃんと書いてあるので, 困ったら読もう.