venvのactivateって何をactivateにしてるの？という疑問を解説

千株式会社あどべんとかんれんだー15日目は社内でAdvent Calendarやりましょうと言い出しっぺなのに連日遅れて投稿するよしけん (@yoshiken) がお送りましす。(だってアラートが止まらないんだもの)

## まえがき

OSのクリーンインストールする際に一度自分の開発環境の見直しをしてみようと思い、メインで使っているPython周りからやるかーと調べてみたら「Pyenvは古い」「Pyenv使ってんのファ○キンジャ○プだけ」「デファクトスタンダードって言葉知ってる？」みたいな記事がたくさんあり、Dockerでバージョン管理をサクサクできる現状Pyenvのメリットないし卒業してvenv使うか〜と立ち上がりました。

立ち上がったところまではいいんですけど、shellはfishを使っているせいかなかなか相性が悪く、特に `source venv/bin/activate` のところで詰まってしまったときのメモです。

## 結論

1. まずバックアップがある場合は復元
2. `$PATH`などの環境変数をバックアップ
3. `venv`ディレクトリを$PATHに追加
4. `$PS1` にディレクトリ名を追加
5. コマンドのハッシュテーブルをクリアにする。

つまり？
→**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パーツづつバラしていきます。

### 初期化

```shell
deactivate () {

(中略)

}

```

見ての通りdeactivate関数を定義してますね。

後述しますが、呼び出し時に初期のPATHなどを別名で保存しています。

これは途中で処理がトチったときでも復元できるようにかと思います。

関数の中身をバラします。

```shell
    # 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`に戻します。そのご削除します

```shell
    if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
        hash -r
    fi

```


まずBashかZSHの場合 `hash` コマンドを呼び出し、コマンド情報をクリアにします

```shell
    if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
        PS1="${_OLD_VIRTUAL_PS1:-}"
        export PS1
        unset _OLD_VIRTUAL_PS1
    fi
```
つづいて `$_OLD_VIRTUAL_PS1` が存在する場合はPS1にセットし削除します。


```shell
    unset VIRTUAL_ENV
```

そして `$VIRTUAL_ENV` に関しては有無を言わせず削除します。


```shell
    if [ ! "$1" = "nondestructive" ] ; then
    # Self destruct!
        unset -f deactivate
    fi
```

みたまんまなんですが、 `$1` 即ち第一引数に `nondestructive` がついて**いない**場合は `$deactivate` を削除します。

nondestructiveってどこで呼んでるの？と思うかもしれませんが、次の行で関数呼び出しを行っています。

以上がdeactivate関数となります

### メイン処理

```shell
deactivate nondestructive
```

先程初期化した `deactivate` 関数を第一引数 `nondestructive` をつけて呼んでいます。


```shell
VIRTUAL_ENV="/tmp/example/venv"
export VIRTUAL_ENV
```

`VIRTUAL_ENV` は皆さんの作ったディレクトリによって変動します。
venvを作成したディレクトリ、今回は `/tmp/example/` ディレクトリに `venv` という名前で作成しています。


```shell

_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
```

さきほど説明しましたが、バックアップとして `$PATH` を `$_OLD_VIRTUAL_PATH` という名前で保存しておきます。

1つ前の手順で `$VIRTUAL_ENV` にセットしたvenvのディレクトリを `$PATHに追加します。

`$PATH` にセットすると何がいいの？という方には過去に僕が投稿した記事を参考にどうぞ。

[自作のコマンドを作成した - Qiita](https://qiita.com/yoshiken/items/2b8e6c24d6b95e65b625)

なお、一番左においてあるので名前空間がかぶったとしても一番上に置かれます。

```shell
if [ -n "${PYTHONHOME:-}" ] ; then
    _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
    unset PYTHONHOME
fi
```

`$PYTHONHOME` が存在している場合は `$_OLD_VIRTUAL_PYTHONHOME` にバックアップとして保存し、削除します。


```shell
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](https://qiita.com/zaburo/items/9194cd9eb841dea897a0)
[bashの変数展開によるファイル名や拡張子の取得 - Qiita](https://qiita.com/mriho/items/b30b3a33e8d2e25e94a8)

```
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
    hash -r
fi
```

そして最後にもう一度ハッシュテーブルをクリアにして完了です。

.fishも.cshもほぼほぼ同じような動作を行っています。

### 確認

実際にvenvを有効にした状態でwhichコマンドを入力してみます

```
$ which python 
/tmp/example/venv/bin/python
```

以上のようにvenv内のpythonが最上位に置かれています