~/.python_history に空白文字が正しく記録されない

この記事は、[上智大学エレクトロニクス研究部Advent Calendar](https://qiita.com/advent-calendar/2020/sophia_ele_lab)第4日目の記事です。

エレラボでは、Pythonユーザーが多く、僕のそのうちの一人です。そんな愛してやまないPythonですが、最近 `~/.python_history` の形式がおかしくなってしまっていることに気が付きました。これは一大事です。なんとかして解決しないと…！

## 事象

MacOS Catalinaで `~/.python_history` ファイルの空白文字が `\040` に、 `\` が `\134` に、 水平タブ が `\011` 置き換わってしまう。
また、 `~/.python_history` の先頭に `_HiStOrY_V2_` という謎のおまじないが書き込まれる。

```bash
❯ head -n 5 ~/.python_history
_HiStOrY_V2_
import\040secrets
[secrets.token_urlsafe(16)\040for\040_\040in\040range(10)]
[secrets.token_urlsafe(12)\040for\040_\040in\040range(10)]
[secrets.token_urlsafe(8)\040for\040_\040in\040range(10)]
```

## 対処方法

### GNU版の `readline` をリンクできるように設定した状態で Pythonのビルドを行う。

```bash
export CFLAGS="-I$(brew --prefix readline)/include -I$(brew --prefix openssl)/include -I$(xcrun --show-sdk-path)/usr/include"
export LDFLAGS="-L$(brew --prefix readline)/lib -L$(brew --prefix openssl)/lib"
# Pythonのビルド
```
(上記は `Homebrew` を用いて GNU版の(以下純正と表記) `readline` をインストールした場合)

### 書き換わってしまった `~/.python_history` は、以下のように行頭を削除し、 エスケープされた文字を元の文字に置き換える。

```bash
cp ~/.python_history ~/.python_history.bak && cat ~/.python_history.bak | sed -e '1d' | unvis > ~/.python_history
```

ただし、行頭の `_HiStOrY_V2_` を消すと、原因となっているPythonインタプリタを実行し、終了した時点ですべての履歴を消される。原因となっているPythonインタプリタと `readline` がリンクされたPythonインタプリタを共存して使いたい場合、行頭を残すために以下のように編集すると良いだろう。

```bash
cp ~/.python_history ~/.python_history.bak && cat ~/.python_history.bak | unvis > ~/.python_history
```

### `/usr/bin/python3` を使わないようにする
`xcode-select --install` によってインストールされるPythonには、 `libedit` がリンクされている。`readline` がリンクされているPythonインタプリタを常用する場合、`/usr/bin/python3` を使わないことをおすすめする。


## 再現手順

以下のように、 `LDFLAGS` を設定せずにPythonのビルドを行うと、 XCode SDKに含まれる `readline` のフリをした `libedit` がPythonの `readline` モジュールとしてリンクされてしまう。

```bash
wget https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgz
tar -xvzof Python-3.8.1.tgz
mv Python-3.8.1 Python-3.8.1.tgz ~/Downloads
cd Python-3.8.1
./configure --enable-optimizations --prefix=$HOME/Downloads
make
make test  # この時点で `~/.python_history` が書き換わる
./python.exe
>>> import secrets
>>> [secrets.token_urlsafe(8) for _ in range(10)]
```

このように`libedit` がリンクされたPythonを開き、コマンドを実行すると、 空白文字が `\040` に置き換わってしまうことがわかる。

```bash
❯ tail -n 2 ~/.python_history
import\040secrets
[secrets.token_urlsafe(8)\040for\040_\040in\040range(10)]
```

## 原因
そもそも、なぜ `XCode SDK` に純正の `readline` が入っていないのか？それは、 `readline` がGPLライセンスで、 `libedit` がBSDライセンスだからだと[言われている](https://metareal.blog/2008/04/11/building-readline-enabled-python-on-mac/)。Appleとしても自社のコードを守るためにGPLライセンスのものを極力同梱したくないだろう。そんな理由で使われている `libedit` は上記の通り `readline` と互換性がなく、このような問題を引き起こしている。
それを回避するためには、純正の `readline` を `homebrew` でインストールし、冒頭の通りライブラリのパスさえしっかりと設定する。この設定を行えば問題は解消する。

```bash
export CFLAGS="-I$(brew --prefix readline)/include -I$(brew --prefix openssl)/include -I$(xcrun --show-sdk-path)/usr/include"
export LDFLAGS="-L$(brew --prefix readline)/lib -L$(brew --prefix openssl)/lib"
# Pythonのビルド
```

ちなみに、 `Homebrew` で `readline` をインストールした場合、 `caveats` に `readline` を使いたいなら `LDFLAGS`, `CFLAGS` をいじるように警告が出るようになっている。

```
readline is keg-only, which means it was not symlinked into /usr/local,
because macOS provides the BSD libedit library, which shadows libreadline.
In order to prevent conflicts when programs look for libreadline we are
defaulting this GNU Readline installation to keg-only.
For compilers to find readline you may need to set:
  export LDFLAGS="-L/usr/local/opt/readline/lib"
  export CPPFLAGS="-I/usr/local/opt/readline/include"
```

## そもそも `readline` って何？？
[**この記事を参考にしてください**](https://qiita.com/tomtsutom0122/items/60f0ab22771a8c65247a)
~~この記事はアドベントカレンダーのものなので続きは別の日に公開します。~~
~~`readline` の解説記事を 明日(12/5)に公開予定します。ぜひご購読お願いします!!~~

他にもネタが思いつく限りどんどん投稿していこうと思います。他の部員も面白い投稿をたくさんしてくれているので、ぜひいいね/購読よろしくお願いします！
[上智大学エレクトロニクス研究部Advent Calendar](https://qiita.com/advent-calendar/2020/sophia_ele_lab)
[上智大学エレクトロニクス研究部の公式サイト](https://selelab.com)

## 参考文献
- https://metareal.blog/2008/04/11/building-readline-enabled-python-on-mac/
- https://stackoverflow.com/questions/17824898/why-does-readline-read-history-file-give-me-ioerror-errno-2-no-such-file-or#comment40603751_17824899
- https://ohbarye.hatenablog.jp/entry/2019/04/14/211850