解説
シンボリックリンクを実体のパスに変換するために通常は readlink
コマンドを使用しますが、これは POSIX 準拠ではありません。(The Open Group Base Specifications Issue 7, 2018 edition の左上「Shell & Utilities」をクリックして左下の「4. Utilities」を参照)
つまり全ての Unix / Linux 系 OS で使えるとは限りません。readlink not found
で検索すると少数ながら実際に使えない環境はあるようです。
通常は readlink
を使えば良いとは思いますが、POSIX 準拠環境のどこでも動くようにしたい場合に困ります。ということで readlink
の代わりに ls
と cd
を使って readlink -f
相当の処理を行う関数を作りました。テストを行い互換性があることを確認しているのでそのまま置き換えられるはずです。ls
コマンドの出力形式は(今回必要な範囲では)決まっているようなので、全ての POSIX 準拠環境で動くはずです。
ソースコードについて(2020-06-25追記)
注意 最新のコードは読みやすいように一般的な書き方に変更しています。ソースコードのリポジトリはこちらです。またハイフンで始まるディレクトリの対応漏れ等を修正してるので、下記のコードは「非推奨」です。(ショートコード版は気が向いたら再実装する予定です)
2023-12-04追記
readlink
コマンドおよび realpath
コマンドは POSIX Issue 8 で標準化されました。したがって(将来的には)同等の機能を提供している realpath
コマンドを使用すれば、この関数を使う必要はありません。readlink
コマンドおよび realpath
コマンドはすでにほとんどの環境(すべてではありません)で実装済みです。
POSIX 準拠版
readlinkf_posix() {
[ ${1:+x} ] || return 1; p=$1; until [ "${p%/}" = "$p" ]; do p=${p%/}; done
[ -e "$p" ] && p=$1; [ -d "$1" ] && p=$p/; set 10 "$PWD" "${OLDPWD:-}"
CDPATH="" cd -P "$2" && while [ "$1" -gt 0 ]; do set "$1" "$2" "$3" "${p%/*}"
[ "$p" = "$4" ] || { CDPATH="" cd -P "${4:-/}" || break; p=${p##*/}; }
[ ! -L "$p" ] && p=${PWD%/}${p:+/}$p && set "$@" "${p:-/}" && break
set $(($1-1)) "$2" "$3" "$p"; p=$(ls -dl "$p") || break; p=${p#*" $4 -> "}
done 2>/dev/null; cd -L "$2" && OLDPWD=$3 && [ ${5+x} ] && printf '%s\n' "$5"
}
コードゴルフ?
既存のスクリプトに組み込んでも主張しすぎないように、横が長くなりすぎない範囲(80 文字以内)で行数を短くしています。気に入らない人は適当に展開してください。なぜこのような書き方をしているかというと、シェルスクリプトである程度の大きさのソフトを作ろうとした時、ファイルを分割して一部をライブラリ化し .
コマンドで読み込むようにすると思いますが、このときプログラムにシンボリックリンクを貼ると実際のソースコードの位置ではなくシンボリックリンクを作成した場所を基準に読み込んでしまいます。ライブラリを読み込むためにプログラムが実際にあるシンボリックリンク先を探す必要があるため、このコードだけはどうしても起動スクリプトに埋め込まなければなりません。普通に書くと意外と長くなってしまうためこのような書き方をしています。また自由にコピーしてスクリプトに埋め込むことができるようにライセンスは CC0 にしています。著作権を放棄しているので商用利用も可能ですし著作権表示も不要です。
処理内容
シンボリックリンクは無限ループがありえるので 10 回までに制限しています。増やしたい時は set 10 ...
の数値を増やしてください。cd
コマンドを使用して実装していますが cd
コマンドは CDPATH
の影響をうけ意図しない場所に移動してしまうことがあるために、CDPATH
を一時的に無効化しています。また cd
コマンドは PWD
変数と OLDPWD
変数を変更してしまうため、終了時に元に戻しています。作業用の変数は p
一つしか使用しません。この変数だけは関数を呼び出すと変更されてしまうので注意して下さい。その他の値は位置パラメータに保存することで実現しています。ls
コマンドを除き、外部コマンドを呼び出していません。サブシェルも使用していません。そのためその他の類似のコードより高速になっています。
テスト
当初はテストがなくバグを入れてしまったので、ちゃんとテストを行いました。テスト結果 はこちらから参照できます。テストは CoreUtils の readlink -f
コマンドの結果と比較して同じになることを確認しています。当初はユニットテストでやろうとしたのですが、ファイルとシンボリックリンクを作成し、正しい値が何かをテストとして書いてもそれを見てテスト内容が正しいか判断するのは大変です。そのため実際にファイルとシンボリックリンク作成してそれら全てに対して、readlikn -f
の結果と等しいことを確認するようにしました。テスト結果をみてもらえばわかりますが、何のテストをしているのかわかりやすいようにしています。
ルートディレクトリ周りのエッジケーステストが必要なのでルートディレクトリに書き込める必要がありますが、さすがに root でテストを実行したくないので、docker を使用してコンテナの中でテストを行うようにしています。テストは現在使用されていると思われる全てのPOSIX準拠シェル (busybox の ash, bosh, bash, dash, ksh, mksh, posh, yash, zsh) で行っています。busybox はテストが失敗しますが、これは readlinkf
のバグではなく、busybox 実装の readlink
に完全な互換性がないからです。
readlink 版
おまけで readlink
(-f なし)を使用したバージョンも実装しています。macOS のように readlink
はあるけれども -f
は使えないという場合にのみ対応できれば十分な場合はこちらをどうぞ。内容はシンボリックリンクを取得する一行が変わっただけです。厳密な測定はしていませんが POSIX 準拠版より 1.5倍から2倍程度速いようです。
readlinkf_readlink() {
[ ${1:+x} ] || return 1; p=$1; until [ "${p%/}" = "$p" ]; do p=${p%/}; done
[ -e "$p" ] && p=$1; [ -d "$1" ] && p=$p/; set 10 "$PWD" "${OLDPWD:-}"
CDPATH="" cd -P "$2" && while [ "$1" -gt 0 ]; do set "$1" "$2" "$3" "${p%/*}"
[ "$p" = "$4" ] || { CDPATH="" cd -P "${4:-/}" || break; p=${p##*/}; }
[ ! -L "$p" ] && p=${PWD%/}${p:+/}$p && set "$@" "${p:-/}" && break
set $(($1-1)) "$2" "$3"; p=$(readlink "$p") || break
done 2>/dev/null; cd -L "$2" && OLDPWD=$3 && [ ${5+x} ] && printf '%s\n' "$5"
}
改訂履歴
2019/05/05追記 バグがあったので修正しました。(@angel_p_57 さん、コメントありがとうございます。)
https://github.com/ko1nksm/readlinkf にて簡易的ですがテストを追加しています。readlink -f
と挙動をあわせていますが、多少無理矢理感を感じているのでそのうち見直すかもしれません。まだバグが潜んでいるかもしれないのでご利用は自己責任でお願いします。
2020-03-06 テストを見直し特定のシェルで発生するバグを修正しました。(その他の類似のコードと違い)readlink -f
と完全互換(に、もうそろそろなったはず)です。
2020-03-09 使用する変数を1つに減らしたり、処理終了後にカレントディレクトリや PWD
や OLDPWD
の変数を元に戻したり、CDPATH
の影響を受けないようにしたり、テストを改善したりしました。にもにもかかわらずコードはわずか9行で、当初公開の11行よりも短くなりました。解説とコードが合わなくなったので記事を全面的に書き換えました。