Bashで自分自身のフルパスを取得する(☓$0 ○$BASH_SOURCE)
$0
だと呼出元パスになってしまう場合がある。
成果物
結論
"$(realpath "${BASH_SOURCE:-0}")"
用途
プロジェクトの構造を保ちつつ、シンボリックリンクから実行する。
ソフトウェはしばしば、実行ファイルとは別にシンボリックリンクファイルを環境変数PATH
が通ったところに配置する。特に拡張子を省いて短いコマンド名にすることがある。シンボリックファイルから実行したとき、元パスを辿ることで、コード内において元パスからの相対パスを壊さずに参照できる。
環境によってrealpath
コマンドが存在しない場合もある。するとシンボリックリンクファイルから元ファイルパスを辿れなくなる。むしろ、元パスを辿りたくないこともある。そのときは以下。
"$(cd "$(dirname "${BASH_SOURCE:-0}")"; pwd)/$(basename "${BASH_SOURCE:-0}")"
変数に代入してDRYに書くと以下。
THIS="${BASH_SOURCE:-0}" HERE="$(cd "$(dirname "$THIS")"; pwd)" THIS="$HERE/$(basename "$THIS")" echo "$THIS"
1回目のTHIS
だけで完結しそうに思える。だがカレントディレクトリで実行したときは相対パス./
で表示されてしまう。よって絶対パスで表示するには2行目、3行目が必要。
回避した罠
$0は呼出元パスになってしまう
検証コードを書く。a.sh
ファイルで期待するのはa.sh
ファイルのフルパスである。
cd /tmp/work
a.sh
echo "\$0: $0" echo "\$BASH_SOURCE: $BASH_SOURCE"
b.sh
. a.sh
chmod 755 a.sh chmod 755 b.sh
実行する。
./b.sh
$0: ./b.sh $BASH_SOURCE: /tmp/work/a.sh
期待値a.sh
のフルパスを取得できたのは$BASH_SOURCE
だけ。$0
は呼出元のb.sh
パスとなってしまった。しかも相対パスであり絶対パスでない。
cd ~
/tmp/work/b.sh
$0: /tmp/work/b.sh $BASH_SOURCE: /tmp/work/a.sh
$0
には以下2点の問題があることが判明した。
よって必ず絶対パスが欲しいなら、以下のようにする。
$(cd $(dirname $0); pwd)
pwd
を使って絶対パスを取得する。ただしファイル名が消えてしまう。そこで以下のようにする。
$(cd $(dirname $0); pwd)/($basename $0)
なお、これは$BASH_SOURCE
においても同じだった……。
クォートすべきである
さもなくばエラーになりうる。エラーになるのはファイルパスにスペースが入っているのにクォートされていないときだ。
スペースはbashのメタ文字である。もしスペースがあるのにクォートされていなければ、別コマンド・別引数として扱われてエラーになってしまう。それを防ぐためクォートする。
"$(cd "$(dirname "$0")"; pwd)/$(basename "$0")"
クォートはダブルクォートのみ有効。シングルクォートだと展開できずリテラル値になってしまうため。
OK
${BASH_SOURCE:-$0}
NG
${BASH_SOURCE:-0}
引数を示す$0
, $1
などが特別なわけではない。${0}
, ${1}
などのようにも書ける。${0:$BASH_SOURCE}
とも書ける。
今回のことには関係ない。ただしBASHを使うときの罠になる。${:-}
パラメータ展開 Parameter Expansion
${:-}
はBASHにおけるパラメータ展開のひとつ。${A:-B}
のとき、A
が空ならB
を返す。
だがこれには罠がある。offset展開のときに負数を指定したときと区別がつかなくなる。
${parameter:-word} ${parameter:offset}
S=abc echo "${S: -1}" # c echo "${S:-1}" # abc
offsetで負数を指定するときは、:
の後にスペースを入れねばならない。さもなくば空のときデフォルト値を返すものだと解釈される。
前者はoffset。後者はS
が空なら1
を返す。今回はS
は空でなくabc
が入っているため、S
を返した。では、S
が空ならどうなるか?
S= echo "${S: -1}" # (空) echo "${S:-1}" # 1
展開 | コマンド |
---|---|
offset | echo "${S: -1}" |
default | echo "${S:-1}" |
cd
やpwd
に-P
オプションを付与することでシンボリックリンクを物理パスへと解決できる。
ただ、今回はシェルスクリプトパス自体がリンクであることを想定している。それはrealpath
コマンドでないと解決できない。realpath
コマンドならディレクトリであっても解決できると思われる。よって-P
オプションは不要。realpath
コマンドのみで対応する。
"$(realpath "${BASH_SOURCE:-$0}")"
検証
ターミナルで実行すると
bash
になる
ターミナルで実行するとbash
になる
echo "$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)/$(basename "${BASH_SOURCE:-$0}")"
/tmp/work/bash
/tmp/work/bash
はカレントディレクトリ+bash
実行ファイル名と思われる。有意義なものではない。シェルスクリプトファイル内において使わねばファイルパスは取得できない。
手動版
スクリプトファイル作成。
cd /tmp/work
vim answer.sh
answer.sh
#!/usr/bin/env bash echo "$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)/$(basename "${BASH_SOURCE:-$0}")"
chmod +x answer.sh
実行。
./answer.sh
/tmp/work/answer.sh
相対パスや、親ディレクトリ以外のカレントディレクトリだった場合などはどうなるか。以下テストコードで検証する。
テスト項目
上記のanswer.sh
コードに対してテストする。期待値は自分自身の絶対パスが取得できること。テスト条件項目は以下の通り。
cd |
実行コマンド | テスト必要性 |
---|---|---|
自ディレクトリ | 相対パス | ○ |
自ディレクトリ | 絶対パス | ○ |
親ディレクトリ | 相対パス | ○ |
親ディレクトリ | 絶対パス | ☓ |
子ディレクトリ | 相対パス | ○ |
子ディレクトリ | 絶対パス | ☓ |
別ディレクトリ | 相対パス | ☓ |
別ディレクトリ | 絶対パス | ○ |
自、親、子はプロジェクト内参照する想定。別ディレクトリは環境変数PATHに通していないコマンドを実行する想定。
手動版の検証
手動版の検証
カレントディレクトリと同じパスで相対パス指定
./answer.sh
/tmp/work/answer.sh
カレントディレクトリと同じパスで絶対パス指定
/tmp/work/answer.sh
/tmp/work/answer.sh
カレントディレクトリの親パスで相対パス指定
cd /tmp
./work/answer.sh
/tmp/work/answer.sh
カレントディレクトリの子パスで相対パス指定
mkdir -p /tmp/work/child cd /tmp/work/child ../answer.sh
/tmp/work/answer.sh
カレントディレクトリと異なるパスで絶対パス指定
cd ~
/tmp/work/answer.sh
/tmp/work/answer.sh
テストコード
全自動。ソースコードも作成してテストまでする。
test.sh
#!/usr/bin/env bash Main() { local name="answer.sh" local file="$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)/$name" echo '#!/usr/bin/env bash' > "$file" echo 'echo $(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)/$(basename "${BASH_SOURCE:-$0}")' >> "$file" chmod 755 "$file" Test1() { # カレントディレクトリと同じパスで絶対パス指定 cd "$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)" "$file" } Test2() { # カレントディレクトリと同じパスで相対パス指定 cd "$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)" ./"$name" } Test3() { # カレントディレクトリの親パスで相対パス指定 local path="$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)" local parent_name="$(basename $path)" cd "$(cd "$(dirname "$path")"; pwd)" "./$parent_name/$name" } Test4() { # カレントディレクトリの子パスで相対パス指定 local cur="$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)" local child_name="child" mkdir -p "$cur/$child_name" cd "$cur/$child_name" "../$name" } Test5() { # カレントディレクトリと異なるパスで絶対パス指定 cd ~ local cur="$(cd "$(dirname "${BASH_SOURCE:-$0}")"; pwd)" "$file" } Test1 Test2 Test3 Test4 Test5 } Main
./test.sh
/tmp/work/answer.sh /tmp/work/answer.sh /tmp/work/answer.sh /tmp/work/answer.sh /tmp/work/answer.sh
OK!
所感
自分自身のパスを取得する。たったそれだけのことに、これほど多数の罠がある。それがBASH。まさに魔境! シェル芸人への道は険しい。
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
$ uname -a Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux