LoginSignup
0
0

More than 1 year has passed since last update.

.pyenv/shims/python を覗いてみる

Posted at

概要

pyenv の入り口がシェルスクリプトなのは知っていたが、そこを改めてよく見たことはなかったので一度見てみる。

内容

which python

$ which python
/Users/yo314159265/.pyenv/shims/python

shims/以下のpythonが本体のようだ。

~/.pyenv/shims/python

~/.pyenv/shims/python
#!/usr/bin/env bash
set -e
[ -n "$PYENV_DEBUG" ] && set -x

program="${0##*/}"

export PYENV_ROOT="/Users/yo314159265/.pyenv"
exec "/usr/local/opt/pyenv/bin/pyenv" exec "$program" "$@"

1行目はshebang。環境でのbashで実行してくれる。
2行目は、set -eで、もしエラーが出たらすぐにスクリプトを止めるようにという意味。
3行目は、[テストコマンドで、真偽値を確認。-nは文字列がヌルでないかの確認。その場合設定される-xは、実行したコマンドを全て出力するかいなか。

なお、

  • -n 文字列がヌルではないか
  • -z 文字列がヌルか(つまり、長さがゼロ)か

また、

  • & 前のコマンドの終了を待たずに実行する
  • && 前のコマンドが正常終了したら実行する

文字列の長さ0がヌル扱いなのと、真偽値が他の言語と逆(真が0で偽がそれ以外)なのは面白いなあと思います。

program="{0##*/}"
${parameter##word}の模様。##はlongest matchingで、#はshortest matching。man#はprefixの除去、%はpostfixの除去。
変数0は、ファイル名。(1以下は実行時の引数。)
総括すると、実行ファイル名がprogramに格納される。

exportで環境変数PYENV_ROOTにファイル名を格納。

そして、$@は引数(1, 2とかの繋がったもの、あるいは0以外)
最後に、/usr/local/opt/pyenv/bin/pyenvを呼び出している。

/usr/local/opt/pyenv/bin/pyenv

$ ls -l /usr/local/opt/pyenv/bin/pyenv
lrwxr-xr-x  1 yo314159265  staff  16 May 30 01:50 /usr/local/opt/pyenv/bin/pyenv -> ../libexec/pyenv

symlinkでした。

/usr/local/opt/pyenv/libexec/pyenv

/usr/local/opt/pyenv/libexec/pyenv
#!/usr/bin/env bash
set -e

if [ "$1" = "--debug" ]; then
  export PYENV_DEBUG=1
  shift
fi

if [ -n "$PYENV_DEBUG" ]; then
  # https://wiki-dev.bash-hackers.org/scripting/debuggingtips#making_xtrace_more_useful
  export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
  set -x
fi

abort() {
  { if [ "$#" -eq 0 ]; then cat -
    else echo "pyenv: $*"
    fi
  } >&2
  exit 1
}

if enable -f "${BASH_SOURCE%/*}"/../libexec/pyenv-realpath.dylib realpath 2>/dev/null; then
  abs_dirname() {
    local path
    path="$(realpath "$1")"
    echo "${path%/*}"
  }
else
  [ -z "$PYENV_NATIVE_EXT" ] || abort "failed to load \`realpath' builtin"

  READLINK=$(type -P greadlink readlink | head -1)
  [ -n "$READLINK" ] || abort "cannot find readlink - are you missing GNU coreutils?"

  resolve_link() {
    $READLINK "$1"
  }

  abs_dirname() {
    local path="$1"

    # Use a subshell to avoid changing the current path
    (
    while [ -n "$path" ]; do
      cd_path="${path%/*}"
      if [[ "$cd_path" != "$path" ]]; then
        cd "$cd_path"
      fi
      name="${path##*/}"
      path="$(resolve_link "$name" || true)"
    done

    echo "$PWD"
    )
  }
fi

if [ -z "${PYENV_ROOT}" ]; then
  PYENV_ROOT="${HOME}/.pyenv"
else
  PYENV_ROOT="${PYENV_ROOT%/}"
fi
export PYENV_ROOT

if [ -z "${PYENV_DIR}" ]; then
  PYENV_DIR="$PWD"
fi

if [ ! -d "$PYENV_DIR" ] || [ ! -e "$PYENV_DIR" ]; then
  abort "cannot change working directory to \`$PYENV_DIR'"
fi

PYENV_DIR=$(cd "$PYENV_DIR" && echo "$PWD")
export PYENV_DIR


shopt -s nullglob

bin_path="$(abs_dirname "$0")"
for plugin_bin in "${bin_path%/*}"/plugins/*/bin; do
  PATH="${plugin_bin}:${PATH}"
done
# PYENV_ROOT can be set to anything, so it may happen to be equal to the base path above,
# resulting in duplicate PATH entries
if [ "${bin_path%/*}" != "$PYENV_ROOT" ]; then
  for plugin_bin in "${PYENV_ROOT}"/plugins/*/bin; do
    PATH="${plugin_bin}:${PATH}"
  done
fi
export PATH="${bin_path}:${PATH}"

PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:${PYENV_ROOT}/pyenv.d"
if [ "${bin_path%/*}" != "$PYENV_ROOT" ]; then
  # Add pyenv's own `pyenv.d` unless pyenv was cloned to PYENV_ROOT
  PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:${bin_path%/*}/pyenv.d"
fi
PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:/usr/local/etc/pyenv.d:/etc/pyenv.d:/usr/lib/pyenv/hooks"
for plugin_hook in "${PYENV_ROOT}/plugins/"*/etc/pyenv.d; do
  PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:${plugin_hook}"
done
PYENV_HOOK_PATH="${PYENV_HOOK_PATH#:}"
export PYENV_HOOK_PATH

shopt -u nullglob


command="$1"
case "$command" in
"" )
  { pyenv---version
    pyenv-help
  } | abort
  ;;
-v | --version )
  exec pyenv---version
  ;;
-h | --help )
  exec pyenv-help
  ;;
* )
  command_path="$(command -v "pyenv-$command" || true)"
  if [ -z "$command_path" ]; then
    if [ "$command" == "shell" ]; then
      abort "shell integration not enabled. Run \`pyenv init' for instructions."
    else
      abort "no such command \`$command'"
    fi
  fi

  shift 1
  if [ "$1" = --help ]; then
    if [[ "$command" == "sh-"* ]]; then
      echo "pyenv help \"$command\""
    else
      exec pyenv-help "$command"
    fi
  else
    exec "$command_path" "$@"
  fi
  ;;
esac

少しずつ見ていく。

#!/usr/bin/env bash
set -e

ここは前に見た通り。エラー時すぐ止める。

if [ "$1" = "--debug" ]; then
  export PYENV_DEBUG=1
  shift
fi

もし、1個目の引数が--debugなら、PYENV_DEBUG1にセットする。(1個目じゃないと駄目なのか)
shiftは、引数のリストをずらす。つまり$2$1になる。なるほど。

if [ -n "$PYENV_DEBUG" ]; then
  # https://wiki-dev.bash-hackers.org/scripting/debuggingtips#making_xtrace_more_useful
  export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
  set -x
fi

PYENV_DEBUGがセットされていれば、PS4にデバッグに有用な情報が出力されるよう。
-xは前に見たコマンドを表示するもの。
なお、(link)

  • PS1: Default interaction prompt
  • PS2: Continuation interactive prompt
  • PS3: Prompt used by “select” inside shell script
  • PS4: Used by “set -x” to prefix tracing output
    (LINENOとかが出力できるのも-xによるのだろう。)
abort() {
  { if [ "$#" -eq 0 ]; then cat -
    else echo "pyenv: $*"
    fi
  } >&2
  exit 1
}

$#は引数の数。$*は引数を一語にしたもの。
- は入力を標準出力から受け取ること。つまり、標準出力からの入力を出力する。
実はこの正確な動作は理解できてない。

if enable -f "${BASH_SOURCE%/*}"/../libexec/pyenv-realpath.dylib realpath 2>/dev/null; then
  abs_dirname() {
    local path
    path="$(realpath "$1")"
    echo "${path%/*}"
  }
else
  [ -z "$PYENV_NATIVE_EXT" ] || abort "failed to load \`realpath' builtin"

  READLINK=$(type -P greadlink readlink | head -1)
  [ -n "$READLINK" ] || abort "cannot find readlink - are you missing GNU coreutils?"

  resolve_link() {
    $READLINK "$1"
  }
  abs_dirname() {
    local path="$1"

    # Use a subshell to avoid changing the current path
    (
    while [ -n "$path" ]; do
      cd_path="${path%/*}"
      if [[ "$cd_path" != "$path" ]]; then
        cd "$cd_path"
      fi
      name="${path##*/}"
      path="$(resolve_link "$name" || true)"
    done

    echo "$PWD"
    )
  }
fi

Enable and disable builtin shell commands.
Syntax
enable [-a] [-dnps] [-f filename] [name …]
Key
-f Load the new builtin command name from shared object filename, on systems that
support dynamic loading.

ここでは、pyenv-realpath.dylibからrealpathを取り込もうとしている。このrealpathを活用して、abs_dirname()が準備される。

if [ -z "${PYENV_ROOT}" ]; then
  PYENV_ROOT="${HOME}/.pyenv"
else
  PYENV_ROOT="${PYENV_ROOT%/}"
fi
export PYENV_ROOT

ここでは、PYENV_ROOTを設定することを目指している。なお、これははじめに設定してある。
そして、改めて、最後の/を除去してある。

if [ -z "${PYENV_DIR}" ]; then
  PYENV_DIR="$PWD"
fi

if [ ! -d "$PYENV_DIR" ] || [ ! -e "$PYENV_DIR" ]; then
  abort "cannot change working directory to \`$PYENV_DIR'"
fi

PYENV_DIR=$(cd "$PYENV_DIR" && echo "$PWD")
export PYENV_DIR

PYENV_DIRを設定する。もしまだPYENV_DIRが設定されていなければ、現在のディレクトリに設定しようとする。設定されていても、ディレクトリでも存在もしなければエラーとなる。
ここで、PYENV_DIRに移動する。どうして再代入するのかはわからない。(SIMLINK対策?わからない。)
なお、PYENV_ROOT=$HOME/.pyenvを.bashrcに記載して欲しいとは言われている。

shopt -s nullglob

オプションをセットしているが、nullglobは該当ファイルがないときに空文字列を生成するかどうか。参考

bin_path="$(abs_dirname "$0")"
for plugin_bin in "${bin_path%/*}"/plugins/*/bin; do
  PATH="${plugin_bin}:${PATH}"
done
# PYENV_ROOT can be set to anything, so it may happen to be equal to the base path above,
# resulting in duplicate PATH entries
if [ "${bin_path%/*}" != "$PYENV_ROOT" ]; then
  for plugin_bin in "${PYENV_ROOT}"/plugins/*/bin; do
    PATH="${plugin_bin}:${PATH}"
  done
fi
export PATH="${bin_path}:${PATH}"

PATHの設定。

  • /usr/local/opt/pyenv/plugins/python-build/bin
    • pyenv-install
    • pyenv-uninstall
    • python-build
  • /usr/local/opt/pyenv/libexec
    • pyenv
    • pyenv---version
    • ....
PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:${PYENV_ROOT}/pyenv.d"
if [ "${bin_path%/*}" != "$PYENV_ROOT" ]; then
  # Add pyenv's own `pyenv.d` unless pyenv was cloned to PYENV_ROOT
  PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:${bin_path%/*}/pyenv.d"
fi
PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:/usr/local/etc/pyenv.d:/etc/pyenv.d:/usr/lib/pyenv/hooks"
for plugin_hook in "${PYENV_ROOT}/plugins/"*/etc/pyenv.d; do
  PYENV_HOOK_PATH="${PYENV_HOOK_PATH}:${plugin_hook}"
done
PYENV_HOOK_PATH="${PYENV_HOOK_PATH#:}"
export PYENV_HOOK_PATH

PYENV_HOOK_PATHは大体/usr/local/opt/pyenv/pyenv.dとかを指すのだろう。

shopt -u nullglob

nullglobをアンセット。

command="$1"
case "$command" in
"" )
  { pyenv---version
    pyenv-help
  } | abort
  ;;
-v | --version )
  exec pyenv---version
  ;;
-h | --help )
  exec pyenv-help
  ;;
* )
  command_path="$(command -v "pyenv-$command" || true)"
  if [ -z "$command_path" ]; then
    if [ "$command" == "shell" ]; then
      abort "shell integration not enabled. Run \`pyenv init' for instructions."
    else
      abort "no such command \`$command'"
    fi
  fi

  shift 1
  if [ "$1" = --help ]; then
    if [[ "$command" == "sh-"* ]]; then
      echo "pyenv help \"$command\""
    else
      exec pyenv-help "$command"
    fi
  else
    exec "$command_path" "$@"
  fi
  ;;
esac

ここで、引数により呼び出されるファイルが示される。

  • ""
    • pyenv---version
    • pyenv-help
  • -v | --version
    • exec pyenv---version
  • -h | --help
    • exec pyenv-help
  • *
    • exec "\$command_path" "\$@"
    • もし、次に--helpがくれば、pyenv helpもしくはpyenv-help

command_pathは、"$(command -v "pyenv-$command" || true)"で得られる。これは、PATH/usr/local/opt/pyenv/libexecを追加しているからこそできることだろう。

ちょっと寄り道して、pyenv globalを見てみることにする。

/usr/local/opt/pyenv/libexec/pyenv-global(寄り道)

/usr/local/opt/pyenv/libexec/pyenv-global
set -e
[ -n "$PYENV_DEBUG" ] && set -x

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  echo system
  exec pyenv-versions --bare
fi

versions=("$@")
PYENV_VERSION_FILE="${PYENV_ROOT}/version"

if [ -n "$versions" ]; then
  pyenv-version-file-write "$PYENV_VERSION_FILE" "${versions[@]}"
else
  OLDIFS="$IFS"
  IFS=: versions=($(
    pyenv-version-file-read "$PYENV_VERSION_FILE" ||
    pyenv-version-file-read "${PYENV_ROOT}/global" ||
    pyenv-version-file-read "${PYENV_ROOT}/default" ||
    echo system
  ))
  IFS="$OLDIFS"
  for version in "${versions[@]}"; do
    echo "$version"
  done
fi

shebangとコメントは削除した。
少しずつ見ていく。

set -e
[ -n "$PYENV_DEBUG" ] && set -x

恒例のもの。

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  echo system
  exec pyenv-versions --bare
fi

コンプリートオプションが設定されていた場合の動作。

versions=("$@")
PYENV_VERSION_FILE="${PYENV_ROOT}/version"

引数をversionsに納め、VERSION_FILEを指定。

if [ -n "$versions" ]; then
  pyenv-version-file-write "$PYENV_VERSION_FILE" "${versions[@]}"
else
  OLDIFS="$IFS"
  IFS=: versions=($(
    pyenv-version-file-read "$PYENV_VERSION_FILE" ||
    pyenv-version-file-read "${PYENV_ROOT}/global" ||
    pyenv-version-file-read "${PYENV_ROOT}/default" ||
    echo system
  ))
  IFS="$OLDIFS"
  for version in "${versions[@]}"; do
    echo "$version"
  done
fi

ここで、設定するか表示するかの違い。
設定する場合は、pyenv-version-file-writeを使って、PYENV_VERSION_FILEに書き込んでいる模様。
ここで、versions=("$@")および"${versions[@]}"に注意。実は、Python 2系と3系の二つを指定できる模様。
そのために配列での操作にあんっているようだ。

/usr/local/opt/pyenv/libexec/pyenv-version-file-write

/usr/local/opt/pyenv/libexec/pyenv-version-file-write
#!/usr/bin/env bash
# Usage: pyenv version-file-write <file> <version>

set -e
[ -n "$PYENV_DEBUG" ] && set -x

PYENV_VERSION_FILE="$1"
shift || true
versions=("$@")

if [ -z "$versions" ] || [ -z "$PYENV_VERSION_FILE" ]; then
  pyenv-help --usage version-file-write >&2
  exit 1
fi

# Make sure the specified version is installed.
pyenv-prefix "${versions[@]}" >/dev/null

# Write the version out to disk.
# Create an empty file. Using "rm" might cause a permission error.
> "$PYENV_VERSION_FILE"
for version in "${versions[@]}"; do
  echo "$version" >> "$PYENV_VERSION_FILE"
done

少しずつファイルに追記している。ん。追記している?
よくみると、直前にリダイレクトで> "$PYENV_VERSION_FILE"と初期化を行っていた。これで毎回1行しか書かれない。
これでバージョンが指定されるのか。

再び~/.pyenv/shims/pythonへ。(最終行)

~/.pyenv/shims/python
exec "/usr/local/opt/pyenv/bin/pyenv" exec "$program" "$@"

そうか、元々は、commandexecの場合を見たかったのだ。

そして/usr/local/opt/pyenv/libexec/pyenv-exec(最終章)

shebangとコメントは省く。

/usr/local/opt/pyenv/libexec/pyenv-exec
set -e
[ -n "$PYENV_DEBUG" ] && set -x

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  exec pyenv-shims --short
fi

PYENV_VERSION="$(pyenv-version-name)"
PYENV_COMMAND="$1"

if [ -z "$PYENV_COMMAND" ]; then
  pyenv-help --usage exec >&2
  exit 1
fi

export PYENV_VERSION
PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")"
PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}"

OLDIFS="$IFS"
IFS=$'\n' scripts=(`pyenv-hooks exec`)
IFS="$OLDIFS"
for script in "${scripts[@]}"; do
  source "$script"
done

shift 1
if [ "${PYENV_BIN_PATH#${PYENV_ROOT}}" != "${PYENV_BIN_PATH}" ]; then
  # Only add to $PATH for non-system version.
  export PATH="${PYENV_BIN_PATH}:${PATH}"
fi
exec "$PYENV_COMMAND_PATH" "$@"

少しずつ見ていく。

set -e
[ -n "$PYENV_DEBUG" ] && set -x

お決まりの文句。

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  exec pyenv-shims --short
fi

コンプリーションズのオプションが設定されていた時の動作。

PYENV_VERSION="$(pyenv-version-name)"
PYENV_COMMAND="$1"

if [ -z "$PYENV_COMMAND" ]; then
  pyenv-help --usage exec >&2
  exit 1
fi

VERSIONとCOMMANDの取得。今回のコマンドは、pythonのはず。
なお、pyenv-version-namePYENV_VERSIONをよむか、pyenv-version-file-readを読んだりして取得している。

export PYENV_VERSION
PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")"
PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}"

ここで、pyenv-whichを使ってCOMMAND_PATHを取得している。(後でみる。)

OLDIFS="$IFS"
IFS=$'\n' scripts=(`pyenv-hooks exec`)
IFS="$OLDIFS"
for script in "${scripts[@]}"; do
  source "$script"
done

IFSは、"internal field separator". link
続けて、pyenv-hooks execをこのIFSで呼ぶことができている。そして、exec以下のファイルが実行されることになる。(前処理かなあ)

shift 1
if [ "${PYENV_BIN_PATH#${PYENV_ROOT}}" != "${PYENV_BIN_PATH}" ]; then
  # Only add to $PATH for non-system version.
  export PATH="${PYENV_BIN_PATH}:${PATH}"
fi
exec "$PYENV_COMMAND_PATH" "$@"

そして最終的にexec "$PYENV_COMMAND_PATH" "$@"される。

さて、ここでpyenv-whichが./pyenv/versionを読み出しているところが見られれば良いのだが。

そして/usr/local/opt/pyenv/libexec/pyenv-which(最終章最終回)

少しずつ見ていく。

/usr/local/opt/pyenv/libexec/pyenv-which
set -e
[ -n "$PYENV_DEBUG" ] && set -x

決まり文句。

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  exec pyenv-shims --short
fi

if [ "$2" = "--nosystem" ]; then
  system=""
else
  system="system"
fi

特定オプション処理。

remove_from_path() {
  local path_to_remove="$1"
  local path_before
  local result=":${PATH//\~/$HOME}:"
  while [ "$path_before" != "$result" ]; do
    path_before="$result"
    result="${result//:$path_to_remove:/:}"
  done
  result="${result%:}"
  echo "${result#:}"
}

":${PATH//\~/$HOME}:"はreplace all。
remove_from_pathそのままの処理をしている。

PYENV_COMMAND="$1"

if [ -z "$PYENV_COMMAND" ]; then
  pyenv-help --usage which >&2
  exit 1
fi

PYENV_COMMANDの格納。ここでは、pythonのはず。

OLDIFS="$IFS"
IFS=: versions=(${PYENV_VERSION:-$(pyenv-version-name)})
IFS="$OLDIFS"

for version in "${versions[@]}" "$system"; do
  if [ "$version" = "system" ]; then
    PATH="$(remove_from_path "${PYENV_ROOT}/shims")"
    PYENV_COMMAND_PATH="$(command -v "$PYENV_COMMAND" || true)"
  else
    PYENV_COMMAND_PATH="${PYENV_ROOT}/versions/${version}/bin/${PYENV_COMMAND}"
  fi
  if [ -x "$PYENV_COMMAND_PATH" ]; then
    break
  fi
done

${PYENV_VERSION:-$(pyenv-version-name)}:-はもしunsetなら後者を使うということ。
ここでやっと、
PYENV_COMMAND_PATH="${PYENV_ROOT}/versions/${version}/bin/${PYENV_COMMAND}"
が見つかる。

OLDIFS="$IFS"
IFS=$'\n' scripts=(`pyenv-hooks which`)
IFS="$OLDIFS"
for script in "${scripts[@]}"; do
  source "$script"
done

hooksの処理。(おそらく前処理)

if [ -x "$PYENV_COMMAND_PATH" ]; then
  echo "$PYENV_COMMAND_PATH"
else
  any_not_installed=0
  for version in "${versions[@]}"; do
    if [ "$version" = "system" ]; then
      continue
    fi
    if ! [ -d "${PYENV_ROOT}/versions/${version}" ]; then
      echo "pyenv: version \`$version' is not installed (set by $(pyenv-version-origin))" >&2
      any_not_installed=1
    fi
  done
  if [ "$any_not_installed" = 1 ]; then
    exit 1
  fi

  echo "pyenv: $PYENV_COMMAND: command not found" >&2

  versions="$(pyenv-whence "$PYENV_COMMAND" || true)"
  if [ -n "$versions" ]; then
    { echo
      echo "The \`$1' command exists in these Python versions:"
      echo "$versions" | sed 's/^/  /g'
      echo
      echo "Note: See 'pyenv help global' for tips on allowing both"
      echo "      python2 and python3 to be found."
    } >&2
  fi

  exit 127
fi

そして、ここでpathが出力される。↓

if [ -x "$PYENV_COMMAND_PATH" ]; then
  echo "$PYENV_COMMAND_PATH"

完。

おまけ

$ ~/.pyenv/versions/3.10.0/bin/python
Python 3.10.0 (default, Jun  6 2022, 16:40:39) [Clang 11.0.3 (clang-1103.0.32.59)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

感想

楽しかった。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0