スクリプトの中でパスを組み立てていると、同じファイルなのに毎回違う書き方になってしまうことがあります。
相対パスだったり、../ が挟まっていたり、途中にシンボリックリンクが混ざっていたり。
この「表記ゆれ」を一本に畳んでくれるのが realpath と readlink です。
どちらも名前は似ていますが、見ている範囲はまったくの別物。
今回はその違いと、実務で効くオプションを実際に動かしながら整理していきます。
検証環境は GNU coreutils 8.32 / bash 5.2.37(Windows 上の Git Bash、MSYS2)です。
検証に使うディレクトリを作る
まず、シンボリックリンクを1本だけ含む小さな構成を用意します。
mkdir -p /c/work/tmp/rp/app/config
cd /c/work/tmp/rp
echo 'db=1' > app/config/settings.ini
ln -s app/config conf
これで conf が app/config を指すリンクになりました。
同じ settings.ini に、conf 経由と app/config 経由の 2 通りでたどり着ける状態です。
この二重性が、あとから効いてくる比較処理や重複判定を静かに壊します。
readlink はリンクの中身を1段だけ読む
readlink は、オプションなしだと シンボリックリンクに書かれている文字列をそのまま返すだけのコマンドです。
$ readlink conf
app/config
絶対パスに直してくれるわけではなく、リンクの「中身」を1段だけ覗いた結果が出ています。
相手がリンクでなければ、何も出さずに終了ステータス 1 を返すだけ。
$ readlink app/config/settings.ini
$ echo $?
1
この性質のおかげで「これはリンクか?」の判定にそのまま使えます。
ここに -f を足すと役割が変わり、返ってくるのはリンクを最後までたどった絶対パス。
$ readlink -f conf/settings.ini
/c/work/tmp/rp/app/config/settings.ini
realpath はパスを1本の絶対パスに畳む
一方の realpath は、最初から 正規化そのものが仕事 です。
$ realpath conf/settings.ini
/c/work/tmp/rp/app/config/settings.ini
readlink -f と同じ結果ですが、こちらは ./ や ../ の畳み込みも同時に片づきます。
$ realpath ./app/../app/config/../config/settings.ini
/c/work/tmp/rp/app/config/settings.ini
回り道をどれだけ書いても、返ってくるのは同じ1本。
文字列としてパスを比較したいときは、必ずここを通してから比べるのが安全です。
-s ならリンクを解かずに絶対パス化する
リンクを解きたくない場面では -s を付けます。
$ realpath -s conf/settings.ini
/c/work/tmp/rp/conf/settings.ini
conf のまま絶対パスになったのが分かります。
ログに出す名前は利用者が打った conf のままにしておきたい、といったときに便利。
–relative-to で基準からの相対パスに戻す
逆に、絶対パスを特定のディレクトリ基準の相対パスへ落とすこともできます。
$ realpath --relative-to=/c/work/tmp/rp/app /c/work/tmp/rp/app/config/settings.ini
config/settings.ini
生成物の一覧をプロジェクトルート基準で出したいときに、この一手でそろいます。
まだ存在しないパスをどう扱うか
出力ファイルのパスを先に決めておきたい、というケースでつまずくのがここです。
オプションなしの realpath は、途中のディレクトリが無いと失敗する一方で、最後の1要素は無くても通ります。
$ realpath ./missing.txt
/c/work/tmp/rp/missing.txt
$ realpath ./a/b/c.txt
realpath: ./a/b/c.txt: No such file or directory
ここを緩めるのが -m、逆に厳しくするのが -e です。
$ realpath -m ./a/b/c.txt
/c/work/tmp/rp/a/b/c.txt
$ realpath -e ./missing.txt
realpath: ./missing.txt: No such file or directory
-m は「これから作る場所」、-e は「必ず在るはずの場所」、と役割で覚えると迷いません。
存在確認を兼ねたいときに -e を選んでおくと、後段の処理が空振りしなくなるわけです。
スクリプト自身の場所を確実に取る
実務でいちばん使うのは、この組み合わせでしょう。
#!/usr/bin/env bash
here=$(dirname "$(readlink -f "$0")")
echo "here=$here"
このスクリプトをシンボリックリンク越しに起動しても、返るのはリンク先の実体があるディレクトリでした。
$ ./where_link.sh
here=/c/work/tmp/rp/demo
$0 をそのまま dirname に渡すと、リンクが置かれた側のディレクトリを指してしまうので、設定ファイルの相対参照が崩れます。
間に readlink -f を1枚はさむだけで、この事故は消えるのです。
パスの分解そのものについては、別記事で basename と dirname を整理しました。
そもそもリンクの種類が怪しい、というときはこちらもあわせてどうぞ。
最後に
readlink は リンクを1段だけ覗く道具、realpath は パスを1本に畳む道具。
この線引きさえ押さえておけば、-f や -s の使い分けで迷うことはありません。
パスを比較する前に正規化する、という一手を癖にしておくと、地味なバグがまとめて減ります。
私も自作スクリプトの先頭には、ほぼ機械的に readlink -f "$0" を書くようになりました。
たどり着いたパスの中身をもう一段詳しく見たいときは、stat が続きの道具になります。
以上です。











コメントを残す