1. ホーム
  2. bash

[解決済み] POSIXのshでスクリプトディレクトリを取得するには?

2023-07-09 20:57:15

質問

bashスクリプトで以下のようなコードを書いています。今私はそれをPOSIX shで使いたいと思っています。どのように私はそれを変換することができますか?

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"

どのように解決するのですか?

POSIX-shellの( sh ) の対応する $BASH_SOURCE$0 . 背景情報については、下を参照してください。

注意事項 : 決定的に違うのは である場合、スクリプトは ソース (に読み込まれます)。 現在の というシェルに . ) を指定すると のスニペットは ではなく は正しく動作しません。 の説明を以下に示します。

を変更したことに注意してください。 DIRdir であるため、以下のスニペットでは はすべて大文字の変数名を使用しないほうがよいからです。 というのは、環境変数や特殊なシェル変数との衝突を避けるために、すべて大文字の変数名を使わない方が良いからです。

また CDPATH= の代わりにプレフィックス > /dev/null に置き換えられる。 $CDPATH にはヌル文字列が設定されており、これにより cd は決して エコー 何もしない

最も単純なケースでは、これで( OPのコマンドに相当する ):

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)

もしあなたが を解決したい場合は 結果 ディレクトリのパスを最終的なターゲットに解決します。 シンボリックリンク を追加する。 -Ppwd コマンドに変換します。

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)

警告 : これは ではありません。 を見つけるのと同じです。 を見つけるのと同じです。 :

例えば、あなたのスクリプトが foo にシンボリックリンクされているとします。 /usr/local/bin/foo の中で $PATH の中にありますが、その本当のパスは /foodir/bin/foo .

上記はまだ /usr/local/bin を報告します。なぜならシンボリックリンクの解決 ( -P ) が適用されるからです。 ディレクトリ , /usr/local/bin というように、スクリプトそのものではなく

スクリプト自身の真の生成ディレクトリを見つけるために を調べる必要があります。 スクリプトの へのパスを であるかどうかを確認します。 シンボリックリンク であるかどうかを調べ、もしそうなら、最終的なターゲットファイルまで(一連の)シンボリックリンクをたどり、そのディレクトリパスを ターゲット ファイルの正規のパスからディレクトリのパスを抽出します。

GNUの readlink -f (もっと良い readlink -e ) は、あなたのためにそれを行うことができますが readlink は POSIX ユーティリティではありません。

macOS を含む BSD プラットフォームには readlink ユーティリティもありますが、macOS ではこのユーティリティは -f の機能をサポートしていません。とはいえ がいかに簡単な作業になるかを示すために もし readlink -f が利用可能です。 :

dir=$(dirname "$(readlink -f -- "$0")") .

実は、そこには no を解決するための POSIX ユーティリティがあります。 ファイル シンボリックリンク . これを回避する方法はありますが、面倒ですし、完全に堅牢というわけではありません。

を次のようにします。 POSIX準拠のシェル関数 は、GNU の readlink -e が行う であり 合理的に堅牢なソリューション その は2つの稀なエッジケースにおいてのみ失敗します。 :

  • を埋め込んだパス 改行 (非常に稀)
  • リテラル文字列を含むファイル名 -> (これも稀です)

という名前のこの関数で rreadlink と定義されています。 が定義されている場合、次のようにスクリプトの本当のディレクトリのパスを決定します。 :

dir=$(dirname -- "$(rreadlink "$0")")

注意 : もしあなたが、(POSIX以外の) readlink ユーティリティが存在すると仮定するならば - macOS, FreeBSD, Linux をカバーすることになります - 似たような、しかしより単純な解決法が、以下のものにあります。 この回答 を参照してください。

rreadlink() ソースコード - 場所 以前 を呼び出す をスクリプトで呼び出します。

rreadlink() ( # Execute the function in a *subshell* to localize variables and the effect of `cd`.

  target=$1 fname= targetDir= CDPATH=

  # Try to make the execution environment as predictable as possible:
  # All commands below are invoked via `command`, so we must make sure that `command`
  # itself is not redefined as an alias or shell function.
  # (Note that command is too inconsistent across shells, so we don't use it.)
  # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have
  # an external utility version of it (e.g, Ubuntu).
  # `command` bypasses aliases and shell functions and also finds builtins 
  # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that
  # to happen.
  { \unalias command; \unset -f command; } >/dev/null 2>&1
  [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too.

  while :; do # Resolve potential symlinks until the ultimate target is found.
      [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." >&2; return 1; }
      command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path.
      fname=$(command basename -- "$target") # Extract filename.
      [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/'
      if [ -L "$fname" ]; then
        # Extract [next] target path, which may be defined
        # *relative* to the symlink's own directory.
        # Note: We parse `ls -l` output to find the symlink target
        #       which is the only POSIX-compliant, albeit somewhat fragile, way.
        target=$(command ls -l "$fname")
        target=${target#* -> }
        continue # Resolve [next] symlink target.
      fi
      break # Ultimate target reached.
  done
  targetDir=$(command pwd -P) # Get canonical dir. path
  # Output the ultimate target's canonical path.
  # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path.
  if [ "$fname" = '.' ]; then
    command printf '%s\n' "${targetDir%/}"
  elif  [ "$fname" = '..' ]; then
    # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied
    # AFTER canonicalization.
    command printf '%s\n' "$(command dirname -- "${targetDir}")"
  else
    command printf '%s\n' "${targetDir%/}/$fname"
  fi
)

堅牢で予測可能なように、この関数では command を使用して、シェル組み込み関数や外部ユーティリティのみが呼び出されるようにします(エイリアスや関数の形式でのオーバーロードは無視します)。

それは は以下のシェルの最近のバージョンでテストされています。 bash , dash , ksh , zsh .


処理方法 ソース付き の呼び出しをどのように扱うか。

tl;dr :

使用方法 POSIXの機能のみ :

  • あなたは できない スクリプトのパスを決定する の中で をソースとする 呼び出し (ただし zsh として動作することは通常ありません。 sh ).
  • あなたは できる を検出します。 スクリプトがソースされている場合のみ、スクリプトがソースされているかどうかを検出できます。 直接 シェルから (シェルプロファイル/初期化ファイルのような) チェーン のソース) を比較することにより $0 をシェルの実行ファイル名/パスと比較します (ただし zsh では、前述のように $0 は本当に現在のスクリプトのパスである)。対照的に (ただし zsh を除く)、スクリプトのソースが 他のスクリプト であり、それ自体が 直接呼び出される を含む。 その スクリプトのパスを $0 .
  • これらの問題を解決するために bash , ksh そして zsh 非標準 機能 その する は、ソース付きのシナリオであっても実際のスクリプトのパスを決定し、スクリプトがソースされているかどうかも検出できるようにします。 bash , $BASH_SOURCE 常に には、ソースされているかどうかに関わらず、 実行中のスクリプトのパスが含まれます。 [[ $0 != "$BASH_SOURCE" ]] はスクリプトがソースされているかどうかをテストするために使用することができます。

なぜこれができないかを示すために からのコマンドを解析してみましょう。 ウォルターAさんの回答 :

    # NOT recommended - see discussion below.
    DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )

  • (余談が2つ。
    • 使用方法 -P を二回使うのは冗長です。 pwd .
    • コマンドは、サイレンシングが欠落している cd の潜在的な標準出力が、もし $CDPATH がたまたま設定されていた場合)。
  • command -v -- "$0"
    • command -v -- "$0" は、もう一つのシナリオをカバーするように設計されています。 がソースされている場合です。 スクリプトが 対話型 シェルから $0 には通常 単なるファイル名 を含んでいます。 sh ) で、この場合 dirname は単に . を返すだけです (これは dirname のない引数が与えられると必ずそうなるからです。 パス コンポーネントがない場合)。 command -v -- "$0" を通して、そのシェルの絶対パスを返します。 $PATH ルックアップ ( /bin/sh ). ただし ログイン シェルは、いくつかのプラットフォーム (例: OSX) では、ファイル名の前に -$0 ( -sh ) である場合、この場合 command -v -- "$0" は意図したとおりに動作しません( 空の文字列 ).
    • 逆に command -v -- "$0" は2つの誤動作をすることがあります。 -ソースのシナリオ で、シェルの実行可能な sh 直接 は、スクリプトを引数として呼び出されます。
      • もしスクリプト自身が ではなく を実行可能にしてください。 command -v -- "$0" を返すかもしれません。 空の文字列 として動作する特定のシェルに依存します。 sh として動作するシェルに依存します。 bash , ksh そして zsh は空文字列を返します。 dash エコー $0

        の POSIX 仕様。 command command -v に適用された場合 ファイルシステムのパス を返します。 実行可能ファイル - というのは bash , ksh そして zsh の目的によって暗示されていると主張することができます。 command ;不思議なことに dash は、通常最も準拠したPOSIX市民ですが、ここでは標準から逸脱しています。対照的に ksh は、ここでは唯一のモデル市民であり、実行可能なファイルのみを報告するものであるため を付けて報告します。 絶対 (正規化されていないとはいえ) パスを指定します。
      • もしスクリプトが が実行可能であり、かつ $PATH であり、呼び出しはその 単なるファイル名 (を使用します(例. sh myScript ), command -v -- "$0" はまた 空の文字列 ただし dash .
    • スクリプトのディレクトリが はできません。 を決定することができません。 ソース - なぜなら $0 ではその情報を含まないからです (ただし zsh として動作することは通常ありません。 sh として動作する) - この問題に対する良い解決策はありません。
      • を返す シェル実行ファイルの ディレクトリのパスを返すことは、限られた用途にしか使えません。 スクリプトの ディレクトリではありません。 テスト を決定するために かどうか を判断します。
        • より信頼性の高い方法は、単純にテストすることです。 $0 を直接テストすることです。 [ "$0" = "sh" ] || [ "$0" = "-sh" ] || [ "$0" = "/bin/sh" ]
      • しかし、スクリプトのソースが次のものである場合は、それも機能しません。 別の スクリプト (それ自身は が直接 が呼び出される)、なぜなら $0 は単に ソース スクリプトのパスが含まれます。
    • の有用性が限定的であることを考えると command -v -- "$0" の有用性が限られていることと、2つの を壊さないことです。 -を壊してしまうという事実があります。 使わないに一票 で、これは私たちに残ります。
      • すべて なし -ソースシナリオ をカバーしています。
      • ソース付き の呼び出しでは スクリプトのパスを決定することはできません。 であり、せいぜい限られた状況下では であるかどうかを検出することができます。 を検出できる程度です。
        • ソーシングされたとき 直接 である場合 (シェルプロファイル/初期化ファイルから)。 $dir を含むことになります。 . を含むか、シェルの実行ファイルが単なるファイル名として起動された場合、( dirname を単なるファイル名 常に を返します。 . ) を返し、それ以外の場合はシェル実行ファイルのディレクトリパスを返します。 . は、カレントディレクトリからの非ソース起動と確実に区別することができません。
        • からソースされた場合 他のスクリプト (からソースされている場合(それ自体もソースされていない)。 $0 には その スクリプトのパスが含まれており、ソースとなるスクリプトはそれが事実であるかどうかを知る方法がありません。

背景情報です。

POSIX が定義している の動作を定義しています。 $0 に対して、シェル スクリプト ここで .

本来は $0 はスクリプトファイルのパスを反映させる必要があります。 指定された を意味する。

  • に依存してはいけません。 $0 を含む 絶対 パス .
  • $0 には 絶対 パス の場合のみです。

    • あなた 明示的に を指定します。 絶対 のパスを指定します。
      • ~/bin/myScript (スクリプト自体が実行可能であると仮定して)
      • sh ~/bin/myScript
    • で実行可能なスクリプトを呼び出した場合 単なるファイル名 というように、実行可能であることが必要です。 であることが必要です。 $PATH を変換しています。 myScript を絶対パスに変換して実行します; 例えば
      • myScript # executes /home/jdoe/bin/myScript, for instance
  • それ以外の場合 $0 はスクリプトを反映します。 パス 指定された :

    • 明示的に sh をスクリプトで呼び出す場合、これは 単なるファイル名 (例, sh myScript ) または 相対パス (例えば sh ./myScript )
    • 実行スクリプトを起動する場合 直接 であれば、これは 相対的 のパス(例. ./myScript - があることに注意してください。 単なるファイル名 を指定すると、スクリプト の中で $PATH ).

実際には bash , dash , ksh そして zsh はすべてこの挙動を示します。

これに対して の値は POSIX では義務付けられていません。 $0 である場合 ソーシング スクリプト (特別な組み込みユーティリティである . ("dot")) を使っているので に依存することはできません。 とか、実際には の動作はシェルによって異なります。

  • したがって、やみくもに $0 を使用し、標準的な動作を期待することはできません。
    • 実際には bash , dash そして ksh 去る $0 手つかず というのは、スクリプトをソーシングするときに $0 には 呼び出し元の $0 の値、より正確には $0 の値、より正確には、それ自身がソースされていないコールチェーン内の最も最近の呼び出し元の値です; 従って $0 のパスはシェルの実行ファイルを指すかもしれません。 別の (直接起動される) スクリプトのパスを指します。
    • これに対して zsh は、一人反対派として、実は をしています。 を報告しています。 現在の スクリプトのパスを $0 . 逆に $0 は、スクリプトがソースされているかどうかの表示を行いません。
    • 要するに、POSIXの機能だけを使って、手元のスクリプトがソースされているかどうか、手元のスクリプトのパスが何であるか、そして $0 と現在のスクリプトのパスの関係はどうなっているのか。
  • もし、この状況に対応する必要があるのなら にアクセスし、特定のシェルを識別する必要があります。 特定の 非標準の機能 :
    • bash , ksh そして zsh はすべて 自身の を提供し、実行中のスクリプトのパスを取得する方法を提供します。

完全を期すために の値は $0 その他 コンテキスト :

  • シェル内部 機能 POSIXでは、以下のように規定されています。 $0 が残る 変更なし したがって、それがどのような値であったとしても の外側 を持つ場合、その値は 内側 を含むようになります。
    • 実際には bash , dash そして ksh はそのように動作します。
    • また zsh は一人反対派で 関数の という名前で報告します。
  • を受け付けたシェルで コマンド文字列を -c オプションで を指定すると、起動時に 最初のオペランド (非オプション引数) を設定します。 $0 ;例えば
    • sh -c 'echo \$0: $0 \$1: $1' foo one # -> '$0: foo $1: one'
    • bash , dash , ksh そして zsh はすべてそのような振る舞いをします。
  • その他 シェルで ではなく を実行することで スクリプトファイル , $0 はシェルの 親プロセス が渡した - 通常、これは シェルの の名前かパスです (例: sh または /bin/sh ); これを含む。
    • an 対話型 シェル
      • 洞窟 一部のプラットフォーム、特に OSX では、常に ログイン シェルを作成します。 を前置する - の中に入れる前に、シェル名に $0 を追加することで、_login シェルであることをシェルに知らせることができます。したがって、デフォルトでは、このシェルは $0 が報告されます。 -bash でなく bash ではなく、OSX 上の対話型シェルで使用されます。
    • を読み込むシェルです。 コマンドを 標準入力
      • は、stdin を介してスクリプトファイルをシェルにパイプする場合にも当てはまります (例, sh < myScript )
    • bash , dash , kshzsh はすべてそのように振る舞います。