【POSIX準拠】readlink･realpath コマンドの使い分けと歴史

## はじめに

`readlink` コマンドと `realpath` コマンドは、どちらもシンボリックリンクのリンク先を調べるコマンドです。`readlink -f` は `realpath` とほぼ同等です。

- `readlink` はシンボリックリンクファイルのリンク先を調べるコマンド
- `realpath` はシンボリックを再帰的に解決し実際のパスを調べるコマンド

`readlink` コマンドと `realpath` コマンドは多くの環境で使えるようになり、**POSIX.1-2024 (Issue 8) で標準化されました**。標準化は一般的に実装された後に行われます。ほとんどの環境ですでに実装ずみであるため、使ったことがある人は多いでしょう。もし古い使えない環境にも対応したい場合には、**`readlink -f` と互換性がある `readlinkf` 関数**を作っているので環境が対応するまでそれを使用して時間稼ぎができます。

https://github.com/ko1nksm/readlinkf

`readlink` コマンドと `realpath` コマンドを使ったことがある人は、どちらも同じような機能を持っているコマンドだと考えているのではないかと思います。この二つのコマンドは POSIX で標準化されたときに役割が明確に分離されました。この記事では `readlink` コマンドと `realpath` コマンドの現状と歴史について解説します。

## どこでも使えるようになった readlink -f と realpath

まず、この記事にたどり着いた人がおそらく気にしていることだと思う件について、簡単に説明しておきます。シンボリックリンクを（再帰的に）解決して、ファイルの実体を取得する方法には、`readlink` コマンドを使う方法と、 `realpath` コマンドを使う方法の二つがあります。 `readlink` コマンドはデフォルトではシンボリックリンクが示すリンク先を取得するだけなので、リンク先がシンボリックリンクの場合にそのまま出力されます。リンク先を再帰的たどるって最終的なパスを取得するにはには `-f` オプションを指定します。

`readlink` コマンド Linux や BSD 系 OS の多くの環境でインストールされていたコマンドですが、`-f` オプションは長い間使えませんでした。BSD 系 Unix では `-f` オプションは 2010 年代の中頃に使えるようになり、最後に 2022 年の macOS 12.3 から使えるようになりました。現在ではほとんどの環境でも使えるオプションですが、 **`readlink` コマンドの `-f` オプションは POSIX では標準化されていない**ので注意してください。

`realpath` コマンドもまたほとんどの環境でも使えるコマンドになりましたが、`readlink` コマンドよりも普及が遅く、歴史的にみると使える環境が少なという問題がありました。それでも徐々に広まっていき、macOS では 2022 年の 13.0 から使えるようになりました。現在では `readlink -f` も `realpath` もほとんどの環境で使うことができるようになりました。

## POSIX で標準化された `readlink` と `realpath`

POSIX で標準化された `readlink` と `realpath` はオプションが少なく使い方も簡単です。

```txt:シンボリックリンクのリンク先を取得する（再帰的に取得するのではない）
readlink [-n] file

-n: 末尾の改行を出力しない
```

```txt:シンボリックリンクを再帰的に解決して実体のパスを取得する
realpath [-e | -E] file

-e: ファイルが見つからない場合にエラーにする
-E: ファイルが見つからない場合にエラーにしない
```

POSIX 標準化された範囲では `readlink` は再帰的にシンボリックリンクのパスを解決するものではなく「シンボリックリンクファイルのリンク先を取得するだけ」のコマンドです。再帰的にパスを解決するのではなく単純にシンボリックリンクファイルの中身（リンク先）を取得するだけです。名前と機能がうまくマッチしていると思います。

実際の実装はもっと多くのオプションを持ち、`readlink` と `realpath` は同じように使うことができたため、使い分けに混乱していたのですが、POSIX の標準化の内容ではそれがなくなりました。私は最初、POSIX はなぜ似たようなコマンドを二つとも標準化したのか疑問だったのですが、よく見ると役割を明確に分けたということがわかります。

## `readlink` コマンドと `realpath` コマンドの歴史

`readlink` と `realpath` はどちらも古くからあるコマンドです。どちらもシステムコールまたはC言語関数の `readlink()` と `realpath()` をラップしたものとして何度も再発明されたようで、おそらく以下の説明以外の実装もあると思われます。

`realpath` は Debian に含まれている [dwww](https://packages.debian.org/trixie/dwww) パッケージの一部として 1996 年頃には存在していたようです。`realpath` コマンドを dwww なしで単独で使いたいと[要望](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=52483)があり、2002 年の Debian 3.0 から realpath パッケージとして独立したようです。これが Linux 系で昔に使われていた初期の `realpath` コマンドです。2001 年には FreeBSD 4.3 でも実装されており、リリースのタイミングを考慮すればほぼ同時に FreeBSD と Debain で `realpath` コマンドが使えるようになったようです。2012 年には GNU 版が実装され、Red Hat 系では 2014 年の RHEL 7.0 から使えるようになり、Debian では 2015 年の Debian 8 から GNU 版に置き換えられました。2022 年以降 BSD 系 Unix でも実装されましたが、おそらく POSIX による標準化をきっかけに先行実装したようです。

`readlink` コマンドは 1997 年の OpenBSD 2.1 で Perl 組み込みの `readlink` 関数の置き換えとして実装されたのが最初です。そしてすぐに `-f` オプションが追加されました。続いて `readlink` コマンドは 2002 年に NetBSD が実装しました。続いて GNU、FreeBSD で実装されました。`readlink` コマンドは GNU が実装した Linux にしか無いコマンドだと勘違いされがちがですが、実際には **`readlink` コマンドは Linux よりも先に BSD 系 Unix で実装されていた**ことがわかります。大まかな時系列は次のとおりです。

|         | realpath                  | readlink                        |
| ------- | ------------------------- | ------------------------------- |
| 1996 年 | dwww パッケージの一部      |                                 |
| 1997 年 |                           | OpenBSD 2.1, OpenBSD 2.2 (-f)   |
| 2001 年 | FreeBSD 4.3               |                                 |
| 2002 年 | Debian 3.0（dwwwから独立） | NetBSD 1.6                      |
| 2003 年 |                           | GNU Coreutils 4.5.5 (-f)        |
| 2004 年 |                           | FreeBSD 4.10                    |
| 2007 年 |                           | NetBSD 4.0 (-f)                 |
| 2012 年 | GNU Coreutils 8.15        | FreeBSD 8.3 (-f), Mac OS X 10.8 |
| 2014 年 | RHEL 7.0（GNU版を組み込み）|                                 |
| 2015 年 | Debian 8（GNU版に変更）    |                                 |
| 2022 年 | OpenBSD 7.1, macOS 13.0   | macOS 12.3 (-f)                 |
| 2023 年 | NetBSD 10                 |                                 |
| 2024 年 | POSIX.1-2024で標準化       | POSIX.1-2024で標準化              |

この表から `realpath` コマンドの実装が（FreeBSD 版を除き）一回り遅かったことがわかります。また `readlink` コマンドも初期の頃は `-f` オプションを実装していたものが少なく、`-f` オプションが実装されていない macOS はよく問題になっていました。

Solaris 11 では GNU 版の [realpath](https://docs.oracle.com/cd/E56342_01/html/E54074/realpath-1.html) と [readlink](https://docs.oracle.com/cd/E56342_01/html/E54074/readlink-1.html) がインストールされています。AIX では `readlink` も `realpath` も無いようですが POSIX で標準化されたので、近いうちに実装されるでしょう。無いならないで最初に紹介した `readlinkf` 関数を使用すればよいです。

## realpath はなぜシェルスクリプトにとって重要なのか？

`realpath` コマンド（または `readlink -f`）はシェルスクリプトにとって重要なコマンドです。その理由は実行しているシェルスクリプトがあるパスを知るためです。

シェルスクリプトをどこからでも実行できるように、シェルスクリプトがある場所に移動するという話を聞きます。これは実は下記の記事で書いたようにアンチパターンです。

https://qiita.com/ko1nksm/items/37a55543073b8239a21c

シェルスクリプトがある場所に移動する場合、次のようなコードを良く見かけます。

```sh
# シェルスクリプトのある場所にカレントディレクトリを移動する
cd "$(dirname "$0")"

# シェルスクリプトのある場所の取得方法
WORKDIR=$(cd "$(dirname "$0")" && pwd)
echo "$WORKDIR"
```

実は**上記のコードはシェルスクリプトがシンボリックリンクの場合に機能しません**。例えばディレクトリ構造が次のようになっているとき、`test.sh` のシンボリックリンクを実行すると正しくないディレクトリに移動してしまいます。

```console 
/tmp/test
|-- bin
|   +-- exec.sh -> ../src/test.sh
+-- src
    +-- test.sh
    +-- file.txt    👈 このファイルを相対パスで読み書きしたい
```

```sh:/tmp/test/src/test.sh
#!/bin/sh

cd "$(dirname "$0")"
pwd
```

```console
$ cd /tmp/test
$ ./src/test.sh
/tmp/test/src

$ ./bin/exec.sh    👈 /tmp/test/src/test.sh へのシンボリックリンクを実行
/tmp/test/bin      👈 これでは /tmp/test/src/file.txt が読み書きできない
```

上記の `test.sh` は**シンボリックリンクからの実行に対応していないというバグ**があります。`realpath` が標準化されたので、今後は以下のようにシェルスクリプトから実行しても動くようになる書き方に変化しているのではないかと思います。

```sh:/tmp/test/src/test.sh
#!/bin/sh
set -eu
self=$(realpath -- "$0") # エラーで止まるように一旦変数に入れる
cd "$(dirname -- "self")"
pwd
```

ただし繰り返しますが、シェルスクリプトがある場所に移動するというのはアンチパターンなので注意してください。

## さいごに

かつては `readlink` と `realpath` コマンドは POSIX で標準化されておらず、実際に移植性がありませんでした。しかし歴史をちゃんと見てみると徐々にシェルスクリプトの世界も便利に変わっているということが分かると思います。昔に使われていてテクニックで現在不要になっているものはいくつもあります。知識のアップデートを行わないと、いつまでも古い効率の悪いやり方を続けてしまいます。