千株式会社あどべんとかんれんだー15日目は社内でAdvent Calendarやりましょうと言い出しっぺなのに連日遅れて投稿するよしけん (@yoshiken) がお送りましす。(だってアラートが止まらないんだもの)
まえがき
OSのクリーンインストールする際に一度自分の開発環境の見直しをしてみようと思い、メインで使っているPython周りからやるかーと調べてみたら「Pyenvは古い」「Pyenv使ってんのファ○キンジャ○プだけ」「デファクトスタンダードって言葉知ってる?」みたいな記事がたくさんあり、Dockerでバージョン管理をサクサクできる現状Pyenvのメリットないし卒業してvenv使うか〜と立ち上がりました。
立ち上がったところまではいいんですけど、shellはfishを使っているせいかなかなか相性が悪く、特に source venv/bin/activate のところで詰まってしまったときのメモです。
結論
- まずバックアップがある場合は復元
- 
$PATHなどの環境変数をバックアップ
- 
venvディレクトリを$PATHに追加
- 
$PS1にディレクトリ名を追加
- コマンドのハッシュテーブルをクリアにする。
つまり?
→venv以下をコマンドとして最上位に追加してPythonコマンドを上書きしてる
解説
まずは該当コードを見ましょう
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
deactivate () {
    # reset old environment variables
    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
        PATH="${_OLD_VIRTUAL_PATH:-}"
        export PATH
        unset _OLD_VIRTUAL_PATH
    fi
    if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
        PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
        export PYTHONHOME
        unset _OLD_VIRTUAL_PYTHONHOME
    fi
    # This should detect bash and zsh, which have a hash command that must
    # be called to get it to forget past commands.  Without forgetting
    # past commands the $PATH changes we made may not be respected
    if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
        hash -r
    fi
    if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
        PS1="${_OLD_VIRTUAL_PS1:-}"
        export PS1
        unset _OLD_VIRTUAL_PS1
    fi
    unset VIRTUAL_ENV
    if [ ! "$1" = "nondestructive" ] ; then
    # Self destruct!
        unset -f deactivate
    fi
}
# unset irrelevant variables
deactivate nondestructive
VIRTUAL_ENV="/tmp/example/venv"
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
    _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
    unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
    _OLD_VIRTUAL_PS1="${PS1:-}"
    if [ "x(venv) " != x ] ; then
	PS1="(venv) ${PS1:-}"
    else
    if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
        # special case for Aspen magic directories
        # see http://www.zetadev.com/software/aspen/
        PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
    else
        PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
    fi
    fi
    export PS1
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands.  Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
    hash -r
fi
ただのshellですね。
ここから1パーツづつバラしていきます。
初期化
deactivate () {
(中略)
}
見ての通りdeactivate関数を定義してますね。
後述しますが、呼び出し時に初期のPATHなどを別名で保存しています。
これは途中で処理がトチったときでも復元できるようにかと思います。
関数の中身をバラします。
    # reset old environment variables
    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
        PATH="${_OLD_VIRTUAL_PATH:-}"
        export PATH
        unset _OLD_VIRTUAL_PATH
    fi
    if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
        PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
        export PYTHONHOME
        unset _OLD_VIRTUAL_PYTHONHOME
    fi
後述するバックアップの $_OLD_VIRTUAL_PATH,$_OLD_VIRTUAL_PYTHONHOME が存在する場合はそれぞれ $PATH,$PYTHONHOMEに戻します。そのご削除します
    if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
        hash -r
    fi
まずBashかZSHの場合 hash コマンドを呼び出し、コマンド情報をクリアにします
    if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
        PS1="${_OLD_VIRTUAL_PS1:-}"
        export PS1
        unset _OLD_VIRTUAL_PS1
    fi
つづいて $_OLD_VIRTUAL_PS1 が存在する場合はPS1にセットし削除します。
    unset VIRTUAL_ENV
そして $VIRTUAL_ENV に関しては有無を言わせず削除します。
    if [ ! "$1" = "nondestructive" ] ; then
    # Self destruct!
        unset -f deactivate
    fi
みたまんまなんですが、 $1 即ち第一引数に nondestructive がついていない場合は $deactivate を削除します。
nondestructiveってどこで呼んでるの?と思うかもしれませんが、次の行で関数呼び出しを行っています。
以上がdeactivate関数となります
メイン処理
deactivate nondestructive
先程初期化した deactivate 関数を第一引数 nondestructive をつけて呼んでいます。
VIRTUAL_ENV="/tmp/example/venv"
export VIRTUAL_ENV
VIRTUAL_ENV は皆さんの作ったディレクトリによって変動します。
venvを作成したディレクトリ、今回は /tmp/example/ ディレクトリに venv という名前で作成しています。
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
さきほど説明しましたが、バックアップとして $PATH を $_OLD_VIRTUAL_PATH という名前で保存しておきます。
1つ前の手順で $VIRTUAL_ENV にセットしたvenvのディレクトリを `$PATHに追加します。
$PATH にセットすると何がいいの?という方には過去に僕が投稿した記事を参考にどうぞ。
なお、一番左においてあるので名前空間がかぶったとしても一番上に置かれます。
if [ -n "${PYTHONHOME:-}" ] ; then
    _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
    unset PYTHONHOME
fi
$PYTHONHOME が存在している場合は $_OLD_VIRTUAL_PYTHONHOME にバックアップとして保存し、削除します。
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
    _OLD_VIRTUAL_PS1="${PS1:-}"
    if [ "x(venv) " != x ] ; then
	PS1="(venv) ${PS1:-}"
    else
    if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
        # special case for Aspen magic directories
        # see http://www.zetadev.com/software/aspen/
        PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
    else
        PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
    fi
    fi
    export PS1
fi
$VIRTUAL_ENV_DISABLE_PROMPT が存在していない場合、 $PS1 を $_OLD_VIRTUAL_PS1 にバックアップします。
$PS1 はあまり聞いたことないと思いますが、コマンドプロンプトで表示される名前ですね。venvをactivateにした場合venv名をコマンドプロンプトに表示されると思いますがその部分の処理です。
具体的には $VIRTUAL_ENV で指定されたパスを basename コマンドで一番末尾、今回の場合 venvが $PS1 セットされます。
下記投稿を参考にどうぞ。
Bashプロンプトの変更 - Qiita
bashの変数展開によるファイル名や拡張子の取得 - Qiita
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
    hash -r
fi
そして最後にもう一度ハッシュテーブルをクリアにして完了です。
.fishも.cshもほぼほぼ同じような動作を行っています。
確認
実際にvenvを有効にした状態でwhichコマンドを入力してみます
$ which python 
/tmp/example/venv/bin/python
以上のようにvenv内のpythonが最上位に置かれています