やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

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行目が必要。

回避した罠

$BASH_SOURCEはBash専用である  shやzshなど他のシェルには存在しない。それらでも自分自身のフルパスが欲しいときは$0を使うしかない。
$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パスとなってしまった。しかも相対パスであり絶対パスでない。

 別のディレクトリから実行してみると$0絶対パスになった。

cd ~
/tmp/work/b.sh
$0: /tmp/work/b.sh
$BASH_SOURCE: /tmp/work/a.sh

 $0には以下2点の問題があることが判明した。

  • $0は呼出元ファイルパスである(自分自身でなく)
  • $0相対パスになることがある(絶対パスでなく)

相対パスになってしまうことがある  カレントディレクトリが自分自身の親であるとき相対パスになってしまう。

 よって必ず絶対パスが欲しいなら、以下のようにする。

$(cd $(dirname $0); pwd)

 pwdを使って絶対パスを取得する。ただしファイル名が消えてしまう。そこで以下のようにする。

$(cd $(dirname $0); pwd)/($basename $0)

 なお、これは$BASH_SOURCEにおいても同じだった……。

クォートすべきである  さもなくばエラーになりうる。エラーになるのはファイルパスにスペースが入っているのにクォートされていないときだ。

 スペースはbashのメタ文字である。もしスペースがあるのにクォートされていなければ、別コマンド・別引数として扱われてエラーになってしまう。それを防ぐためクォートする。

"$(cd "$(dirname "$0")"; pwd)/$(basename "$0")"

 クォートはダブルクォートのみ有効。シングルクォートだと展開できずリテラル値になってしまうため。

${}内における$の記述  $BASH_SOURCEBASH_SOURCEと書く。だが:以降にある$00と書けない。0というリテラル値になってしまう。

OK

${BASH_SOURCE:-$0}

NG

${BASH_SOURCE:-0}

 引数を示す$0, $1などが特別なわけではない。${0}, ${1}などのようにも書ける。${0:$BASH_SOURCE}とも書ける。

${:-} パラメータ展開 Parameter Expansion  今回のことには関係ない。ただしBASHを使うときの罠になる。

 ${:-}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}"

シンボリックリンクが解決できない * https://stackoverflow.com/questions/24112727/relative-paths-based-on-file-location-instead-of-current-working-directory

 cdpwd-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。まさに魔境! シェル芸人への道は険しい。

対象環境

$ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux