Visual Studio Codeの統合シェルをMSYS2のBashにしたら.bash_profileが読み込まれなかった


Windows7前提。

**ガッツリ勘違いしてたのでガッツリ修正済み。**

## 問題

Visual Studio Codeの統合シェルをMSYS2のbashにしたところ、.bashrcの読み込み時にエラーが発生した。

anyenvコマンドが見つからないとか言われたのだが、色々試してみると、.bashrcは読み込まれているが、.bash_profileが読み込まれていないことがわかった。

anyenvのイニシャライズは.bash_profileで行っていた。

-   [riywo/anyenv: all in one for \*\*env](https://github.com/riywo/anyenv)

.bash_profileに以下の部分がある。

```bash
if [ -d "$HOME/.anyenv/bin" ]; then
  PATH="$HOME/.anyenv/bin:$PATH"
fi
eval "$(anyenv init -)"
```

.bashrc内でanyenvコマンドを実行しており、そこでエラーが発生していたのだ。

## シェルのモード

このあたりについては、言及した記事がQiitaにも多数ある。

-   [ログインシェルとインタラクティブシェルと~/\.bashrc達の関係 \- Qiita](http://qiita.com/incep/items/7e5760de0c2c748296aa)
-   [bashのログインシェルとインタラクティブシェル \| kanonjiのブログ](http://kanonji.info/blog/2013/06/20/bash%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%B7%E3%82%A7%E3%83%AB%E3%81%A8%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%82%B7%E3%82%A7%E3%83%AB/)

ここで、Visual Studio Codeのsetting.jsonの記述を確認してみる。

```json
"terminal.integrated.shell.windows": "c:\\msys64\\usr\\bin\\bash.exe",
```

`-l`オプションの指定が無く、かつ`-c`オプションもないので、**ログインシェルではないインタラクティブシェルである**ということになる。

この場合、.bash_profileが読み込まれず、.bashrcが読み込まれるのは、正常な動作だ。

### 全部.bashrcに書けばよいのかというと

そういうわけにもいかない。

特に`export PATH`を.bashrcに書くと、bashを実行するたびに環境変数PATHがじゃんじゃん長くなっていく。Windowsでそれはやばすぎる。

-   [Tech TIPS：環境変数のサイズやPATHの長さ制限に注意 \(1/2\) \- ＠IT](http://www.atmarkit.co.jp/ait/articles/1510/30/news041.html)

よって、このへんの処理は.bash_profileに書く必要がある。

しかし、統合シェルでanyenvが使えないのも困るし、いちいち`exec $SHELL -l`で手動ログインするのも面倒くさい。

## setting.jsonをログインするように書けばいいじゃん

こんな感じにする。

```json
  "terminal.integrated.shell.windows": "c:\\msys64\\usr\\bin\\bash.exe",
  "terminal.integrated.shellArgs.windows": [ "-l" ],
```

すると、ログインシェルとして実行されるので、.bash_profileも読み込まれる。一件落着

……と思いきや

## ワークスペースを作業ディレクトリとして開いてくれない

VSCodeでワークスペースを開いている場合、統合シェルの作業ディレクトリは、通常、ワークスペースのルートディレクトリになる。

> In Visual Studio Code, you can open an integrated terminal, **initially starting at the root of your workspace.**
>
> （[Integrated Terminal in Visual Studio Code](https://code.visualstudio.com/docs/editor/integrated-terminal)）

例えば、統合シェルをGit Bashにすると、この仕様通りの挙動になる。

```json
"terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe",
"terminal.integrated.shellArgs.windows": [ "-l" ],
```

ところが、MSYS2 Bashの場合だと、全部**環境ルート**、すなわち/c/msys64/home/<ユーザー名>になってしまう。

ちなみに、`terminal.integrated.cwd`という設定項目があるのだが、これは環境ルートやワークスペースルートなどの、自動的に選択される作業ディレクトリ**以外**を開きたい場合に設定するもの。

```json
// 端末を起動する明示的な開始パスです。これはシェル プロセスの現在の作業ディレクトリ (cwd) として使用されます。特にルート ディレクトリが cwd に適していない場合に、ワークスペースの設定で役立ちます。
"terminal.integrated.cwd": "",
```

今回はワークスペースルートを開いてほしいので必要ないのだが、いちおうユーザー設定とワークスペース設定の両方で設定してみた。そしてやっぱり無駄だった。

そして、元通り`-l`オプションを付けずに起動した場合、きちんとワークスペースルートを開いてくれる。

要するに、MSYS2 Bashのログイン時の動作によって、環境ルートに飛ばされているわけだ。

### 改めて、Bashログイン時の挙動を確認

>   **bash** が対話的なログインシェルとして起動されるか、 **--login** オプション付きの非対話的シェルとして起動されると、*/etc/profile* ファイルが存在すれば、 **bash** はまずここからコマンドを読み込んで実行します。 このファイルを読んだ後、 **bash** は *~/.bash_profile*, *~/.bash_login*, *~/.profile* をこの 順番で探します。 **bash** は、この中で最初に見つかり、かつ読み込みが可能であるファイルから コマンドを読み込んで実行します。
>
>   （[Man page of BASH](https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html)）

で、これを参考にMSYS2の/etc/profileを確認してみたのだが、作業ディレクトリを変更するような処理は行われていなかった。.bash_profileおよび.bashrcも念のため改めて確認したが、やはり`cd`など書いていない。

### Terminal Here 拡張

詰まったので、根本的な解決にはなっていないが、拡張機能で手当てすることにした。

-   [Terminal Here - Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Tyriar.vscode-terminal-here)

>   Exposes the command `terminalHere.create` that creates a terminal **at the current file's directory**.

読んで字のごとく、**ワークスペースルートではなくエディタで開いているファイルの親ディレクトリ**で統合シェルを開くための拡張である。なので、これでワークスペースルートを開こうとすると、ルート直下のファイルを開いてから実行せねばならず、いささかめんどっちい。

が、今回の用途では好都合なことに、この拡張は**ユーザー設定の規定の動作で統合シェルを起動した後、`cd`コマンドで作業ディレクトリを切り替える**。すなわち、これ以上.bash_profile絡みの問題に振り回されることがない。

### 暫定対応：ログインしない
その後また改めてbashの起動オプションを確認してみたところ、`-l`を省略し、`--init-file`で~/.bash_profileを読み込めばなんとかなるような気がした。

```setting.json
    "terminal.integrated.shellArgs.windows": [
            "--init-file",
            "~/.bash_profile"
        ],
```

すると、いちおうワークスペースルートで起動した！

~/.bash_profileで~/.bashrcを`source`しているので、自分で書いた設定は一通り読み込まれたようだ。

ただ、環境変数`PATH`を確認してみると`/usr/bin`と`/mingw64/bin`がないようだったので、~/.bash_profileで`PATH`を`grep`して自動的に追加するようにしてみた。

```~/.bash_profile
A_PATH=(/usr/bin /mingw64/bin)
for V_PATH in ${A_PATH[@]}; do
  if ! echo "$PATH" | grep -Eq ":?$V_PATH:?"; then
      export PATH="$V_PATH:$PATH"
  fi
done
```

これで、とりあえずパスが通ってなくて困るということはない。
相変わらず「ログインすると環境ルートに飛ばされる」という根本的な問題は何も解決していないのだが、当面これで運用してみることにした。

------

## オマケ：terminal.integrated.shellArgs.windowsに気付いてなかった頃の試行錯誤

コメントいただいた[ubnt_intrepid](http://qiita.com/ubnt_intrepid)さんありがとうございます。

まあ対策としては的はずれなのだが、調査した内容は何かの役に立つからも知れないので残しておく。

## 対策

.bashrcは読み込まれるのだから、.bashrcでログイン処理をすればよい。

Visual Studio Code以外で「非ログインシェルかつインタラクティブシェル」としての実行ができなくなるとまずいので、どうにかして**Visual Studio Codeから実行した場合のみログインする**処理を書きたい。

## Visual Studio Codeから実行したかどうか確認する

### ターミナルエミュレータ名（$TERM変数）を確認する

最初は、ターミナルエミュレータで判定すればいいのではないかと考えた。

例えばConEmuだったらログインシェルとして実行してくれるからなにもしなくていい。Visual Studio Codeの場合だけ……というところで、Visual Studio Codeで使ってるターミナルエミュレータってなんやねんとなった。

作法的には、ターミナルエミュレータがシェルを実行する際、環境変数$TERMにターミナルエミュレータ名を格納して渡しているはずである。

そこで、統合シェルで$TERM変数を出力してみる。

```shell-session
$ echo $TERM
cygwin
$ tset - # 同じ意味
cygwin
```

ファッ！？

ちなみに、ConEmuだと同じくcygwin、minttyではxtermとなった。

あとで気付いたが、minttyはOptionsでこれを変更できる。

![mintty>Options>Terminal](http://i.imgur.com/CsfqYqR.png)

Visual Studio CodeにはCygwinが組み込まれており、これでbashを実行しているということなのだろう。これでは判定ができない。

### bashから親プロセスを確認する

ターミナルエミュレータがなんだろうが、Visual Studio Codeがbashを呼び出していることは間違いないのだから、プロセスレベルでは親子関係が成立しているはずだ。

そこでまず、bash上で親プロセスのプロセスID(PPID)を確認してみる。

```shell-session
$ echo $$ # このbash自身のプロセスIDを出力
12104
$ ps -f # 実行中プロセスの親プロセスID(PPID)を出力
     UID     PID    PPID  TTY        STIME COMMAND
      MM   12104       1 cons0    18:31:10 /usr/bin/bash
      MM   11112   12104 cons0    18:37:47 /usr/bin/ps
```

**いきなり詰んだ。**

このbashすなわちPID:12104の親プロセスIDは1となっている。これは親プロセスが存在しない（または終了している）場合の表示であるっぽい。

-   [ある子プロセスから、親プロセスのPIDを知る方法 \- satakesatakeの日記 \- nextstageoneグループ](https://nextstageone.g.hatena.ne.jp/satakesatake/20120215/1329289284)

bashは親の顔を忘れているのだ。親不孝者だ。

### ProcessExplorerでプロセスの親子関係を確認

-   [「Process Explorer」プロセスの親子関係を階層表示できるタスクマネージャー \- 窓の杜ライブラリ](http://forest.watch.impress.co.jp/library/software/prcsxplorer/)

bashから直接親プロセスを調べることができなかったので、まずはGUIアプリで確認してみる。

結論を言うと、こういったプロセスツリーになっていた。

```
Code.exe
  ┗Code.exe
      ┗Code.exe
          ┗winpty-agent.exe
              ┗bash.exe
                  ┗bash.exe
```

winpty-agent.exeというプロセスが、統合シェルで実行したbashの親プロセスであることがわかる。

### bashから、親プロセスがwinpty-agent.exeであるか確認する

統合シェルで実行したbashの世界には、親プロセスは存在しない。bashに訊いても親の顔は知らんという答えしか返ってこない。

なので、それを知ってる人に訊く必要がある。**コマンドプロンプトだ。**

wmicを利用して、cmd.exe経由で、そのbashのPPIDを出力してもらう。

-   参考：[バッチファイル \- 【bat】taskkill で親プロセスを終了させる方法\(27425\)｜teratail](https://teratail.com/questions/27425)

```shell-session
$ cmd.exe /c "wmic process where ^(processid^=12104^) get parentprocessid /value" | grep -o '[0-9]\+'
3896
```

そして、この親プロセスがwinpty-agent.exeであるかを確認する。

```shell-session
$ cmd.exe /c tasklist | grep 3896
winpty-agent.exe              3896 Console                    1      4,764 K
```

間違いなくwinpty-agent.exeであることがわかった。

### .bashrcで判定処理を入れる

あとは、.bashrcで判定すればよい。

```bash
# Visual Studio Codeの統合シェルはログインシェルではないため.bash_profileが読み込まれない。
# そのため、bashの親プロセスを判定し、統合シェルの場合のみログインを実施する。
if shopt login_shell | grep -q off; then # （インタラクティブシェルであり）ログインシェルではない場合
    ParentPID=$(cmd.exe /c "wmic process where ^(processid^=$$^) get parentprocessid /value" | grep -o '[0-9]\+') # このbashのPPIDを取得
    if cmd.exe /c tasklist | grep $ParentPID | grep -q 'winpty-agent.exe'; then # 親プロセスがwinpty-agent.exeである場合
        echo "detect winpty.exe. exec bash as login-shell"
        exec -l $SHELL
    fi
else
    echo "login-shell: USER[$USER]"
fi
```

bashがインタラクティブシェルとして実行された場合、.bashrcが読み込まれる。

ログインシェルではない場合、親プロセスがwinpty-agent.exeであるか判定し、そうであればログインシェルとして再度実行する。

ログインシェルである場合は、メッセージを表示する。Visual Studio Code統合シェルで実行した場合、.bashrcが再度読み込まれて、メッセージが表示されることになる。

これで、統合シェルでも自動でログインすることができた。