yyy

CTFつよくなりたい

NVM_DIRを正しく設定していなかったせいでパスが破壊されていた話

はじめに

特に書くことがなく4ヶ月ぶりのブログです.

最近golangを始めて, export PATH=$GOPATH/bin:$PATH とかでパスを通そうとしたらなぜかパスが通らないという問題が起きました.いろいろ探っていくとどうやら nvm.sh をsourceしている場所より後ろに配置すると通るという謎現象が起きていて,さらに言うと $GOPATH/bin のbinをhogeとか別の名前にすると通るという意味が分からないことになっていたので, nvm.sh を少し読んでみたら原因がわかったという話です.というかブログのタイトル通りなので,あとはダラダラと書いていきます.

nvm.sh

github.com

nvmはNode Version Managerの略でNode.jsのバージョン管理ツールであり,使っている人も多いのではないでしょうか.僕はNode.jsはほぼ書いたことがないのですが,たまに必要になったりすることもあるので一応使っています.

そんなnvmですが,シェルの設定ファイルとかに nvm.sh を実行するように書いて使用します.この nvm.sh の中にnvmコマンドの本体も書かれています.

nvm() {
  if [ $# -lt 1 ]; then
    nvm --help
    return
  fi

  local DEFAULT_IFS
  DEFAULT_IFS=" $(nvm_echo t | command tr t \\t)
"
  if [ "${IFS}" != "${DEFAULT_IFS}" ]; then
    IFS="${DEFAULT_IFS}" nvm "$@"
    return $?
  fi
...........................

起きていた現象について

まず, nvm.sh をsourceしている直前で $GOPATH/binのパスを通してから echo $PATH で正しく反映されていることを確認しました.その後, nvm.sh の直後にも echo $PATH を置いて,パスの頭に追加した $GOPATH/binそっくり消えていることも確認しました.これは冒頭でも書いたようにbinを別名にすると通ります.

そこで, nvm.sh を読むことにしました.この時僕が使っていた nvm.sh は少し古いものでしたが(ver 0.33.1),最新版(ver 0.33.9)を持ってきてもパスが通らないことに変わりはありませんでした.以下,まずは古い nvm.sh(0.33.1) について書きます.

nvm.sh では nvm_process_parametersという関数を実行していてその中でさらに nvm_auto を実行しています.この nvm_auto の中にあった以下の行でパスは更新されていました.

.......
   VERSION="$(nvm_resolve_local_alias default 2>/dev/null || nvm_echo)"
    if [ -n "$VERSION" ]; then
      nvm use --silent "$VERSION" >/dev/null
.......

VERSION には v8.11.1などが入ってきます.次に,nvmのuseというサブコマンドを見てみます.nvmに渡されたサブコマンドはcase文によって分岐していて,useも同様です.

    "use" )
      local PROVIDED_VERSION
      local NVM_USE_SILENT
      NVM_USE_SILENT=0
      local NVM_DELETE_PREFIX
      NVM_DELETE_PREFIX=0
.......

このuseの中に以下のような行を見つけました.

.......
      # Strip other version from PATH
      PATH="$(nvm_strip_path "$PATH" "/bin")"
.......

見るからに原因がこれっぽいです....

nvm_strip_path を見てみると,

nvm_strip_path() {
  if [ -z "${NVM_DIR-}" ]; then
    nvm_err '${NVM_DIR} not set!'
    return 1
  fi
  nvm_echo "${1-}" | command sed \
    -e "s#${NVM_DIR}/[^/]*${2-}[^:]*:##g" \
    -e "s#:${NVM_DIR}/[^/]*${2-}[^:]*##g" \
    -e "s#${NVM_DIR}/[^/]*${2-}[^:]*##g" \
    -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*:##g" \
    -e "s#:${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*##g" \
    -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*##g"
}

このように渡された引数を sed を使って置換しています.第1引数には現在のパスを,第2引数には"/bin"という文字列を渡していて,冒頭で説明した追加しているのに消えているパスというのは /home/yyy/.go/bin です.これは先頭に追加していたので,追加後は /home/yyy/.go/bin:/home/yyy/... という風になるはずです.

また, NVM_DIR はどんな値になっているのか調べてみると NVM_DIR=/home/yyy になっていました.

ということでsedで置換している1行目, ${NVM_DIR}/ は"/home/yyy/"になり, [^/] は否定の文字クラスであり"/"以外全てを表し,直後の*である0回以上の繰り返しによって".go"が当てはまり,その後の ${2-} が"/bin,最後の":"も合わせて `/home/yyy/.go/bin:" が当てはまってしまうことになります.

よって,sedのこの1行目でパスが破壊されていたということになります.

NVM_DIRについて

謎の現象が起きていた原因はもちろん, NVM_DIR を正しく設定していなかったことです.なにを見て追加したかは忘れましたが,とにかく設定ファイルに NVM_DIR は書いてありませんでした....

それでも NVM_DIR が設定されていたのは nvm.sh の以下の箇所が原因でした.

# Auto detect the NVM_DIR when not set
if [ -z "${NVM_DIR-}" ]; then
  # shellcheck disable=SC2128
  if [ -n "${BASH_SOURCE-}" ]; then
    # shellcheck disable=SC2169
    NVM_SCRIPT_SOURCE="${BASH_SOURCE[0]}"
  fi
  # shellcheck disable=SC1001
  NVM_DIR="$(nvm_cd ${NVM_CD_FLAGS} "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" > /dev/null && \pwd)"
  export NVM_DIR
fi
unset NVM_SCRIPT_SOURCE 2> /dev/null

NVM_CD_FLAGS は "-q" になっていて, nvm_cd というのは以下のようになっています.

nvm_cd() {
  # shellcheck disable=SC1001,SC2164
  \cd "$@"
}

また, "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")"nvm.sh があるディレクトリになるので,僕の場合 "$(nvm_cd ${NVM_CD_FLAGS} "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" > /dev/null && \pwd)" は以下のような文字列になるはずです.

cd -q /home/yyy/.nvm > /dev/null && \pwd

.nvm絶対パスを取得して,この結果を NVM_DIR に格納していると思うのですが, -q オプションをつけるのとつけないので比較してみると, -q を付けた場合は格納される値がホームディレクトリの絶対パスになります.そもそも -q オプションなんて cd にはないと思うのですが,よくわかりません.

NVM_CD_FLAGS は以下の箇所で定義されています.

# Make zsh glob matching behave same as bash
# This fixes the "zsh: no matches found" errors
if [ -z "${NVM_CD_FLAGS-}" ]; then
  export NVM_CD_FLAGS=''
fi
if nvm_has "unsetopt"; then
  unsetopt nomatch 2>/dev/null
  NVM_CD_FLAGS="-q"
fi

このコメントを見る限り, NVM_CD_FLAGS は必要なものだと思います.これを除いて NVM_DIR="$(nvm_cd "$(dirname "${NVM_SCRIPT_SOURCE:-$0}")" > /dev/null && \pwd)" とすると, NVM_DIR には.nvmの絶対パスが入ってくれるのですが....

最新版のnvm.sh

現在(2018/04/20)の最新版は 0.33.9 です.この nvm.sh では上記の箇所が以下のように変わっていました.

# Change current version
PATH="$(nvm_change_path "$PATH" "/bin" "$NVM_VERSION_DIR")"
nvm_change_path() {
  # if there’s no initial path, just return the supplementary path
  if [ -z "${1-}" ]; then
    nvm_echo "${3-}${2-}"
  # if the initial path doesn’t contain an nvm path, prepend the supplementary
  # path
  elif ! nvm_echo "${1-}" | nvm_grep -q "${NVM_DIR}/[^/]*${2-}" \
    && ! nvm_echo "${1-}" | nvm_grep -q "${NVM_DIR}/versions/[^/]*/[^/]*${2-}"; then
    nvm_echo "${3-}${2-}:${1-}"
  # use sed to replace the existing nvm path with the supplementary path. This
  # preserves the order of the path.
  else
    nvm_echo "${1-}" | command sed \
      -e "s#${NVM_DIR}/[^/]*${2-}[^:]*#${3-}${2-}#g" \
      -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*#${3-}${2-}#g"
  fi
}

nvm_strip_path ではなく nvm_change_path を使っています.これも同様に,sedの1行目に当てはまってしまっていたのでパスが消えていたという事でした.

まとめ

結局 NVM_CD_FLAGS がなぜ必要なのかわからず,バグなのかどうかもわからずじまいでした.issueでも立てたほうがいいんですかね....

NVM_DIR を正しく設定しましょうという話でした.