ble.sh: Bash で fish/zsh-syntax-highlighting みたいなカラフルなコマンドライン

GitHub Repository [git@github.com:akinomyoga/ble.sh](https://github.com/akinomyoga/ble.sh)
**2018-03-15** `ble-0.2.0` Release: vi/vim editing mode に対応しました。
**2019-07-10** `ble-0.3.0` Release: 補完機能を大幅に拡張しました。

こんにちは! [GNU Bash](https://www.gnu.org/software/bash/) (`bash`) で [`fish`](http://fishshell.com/) や [`zsh-syntax-highlighting`](https://github.com/zsh-users/zsh-syntax-highlighting) の様に、入力したコマンドラインをその場で色付けする自作の枠組みを紹介します。また、実現方法 (特に `bind -x`) について簡単に説明します。


- この記事は [`Shell Script AdC 2015`](http://qiita.com/advent-calendar/2015/shell-script) 6日目の記事です。
- `AdC 5日目` は @yasuto777 さんの "[気を付けて！！Bashとzshの微妙な違い](http://qiita.com/yasuto777/items/826d757a7d4353a1e259)" でした。"bash ⊂ zsh" の様に聞いて zsh に行くと、bash に慣れすぎているせいで酷い目に遭っちゃいます。
- `AdC 7日目` は @ueokande さんの "[Bashでstdout/stderr/exit codeをキャプチャ](http://folioscope.hatenablog.jp/entry/2015/12/07/000531)"です。プロセス置換とリダイレクトの妙技ですね!

# 要旨
Bash 組込コマンドの `bind -x` を利用して、Bash のコマンドラインに色を付ける事を試みます。様々な問題点を回避するために最終的に Bash スクリプトだけでコマンドラインエディタを実装し、GNU Bash に附属している [GNU Readline ライブラリ](https://cnswww.cns.cwru.edu/php/chet/readline/rltop.html) を置き換えます。Bash の各種の対話的機能 (履歴、補完など) も完全に再実装します。

この記事では、その実現方法について簡単に紹介します。ただ実現方法といっても、かなり(エディタをスクラッチで作成するぐらい)面倒なので各自で実装するのは非現実的です。そこで、要である `bind -x` についてだけ詳しく説明することにしました。`bind -x` は様々な目的に応用できるので、役に立つのではないかと思います。

# 1. `ble.sh` (Bash Line Editor) の使い方
なんでもいいから早く動く物を出せという方は GitHub 上 [akinomyoga/ble.sh](https://github.com/akinomyoga/ble.sh) に実装を公開しています。取り敢えず試すだけなら:

```bash:最新版&#32;(推奨;&#32;GNU&#32;awk/GNU&#32;make&#32;が必要)
[bash]$ git clone https://github.com/akinomyoga/ble.sh.git && make -C ble.sh
[bash]$ source ble.sh/out/ble.sh
```
```bash:v0.3系列
[bash]$ curl -L https://github.com/akinomyoga/ble.sh/releases/download/v0.3.3/ble-0.3.3.tar.xz | tar xJf -
[bash]$ source ble-0.3.3/ble.sh
```
> ble-0.2
> ![ble.sh gif animation](https://github.com/akinomyoga/ble.sh/wiki/images/trial1.gif)
> ble-0.1
> ![ble.sh gif animation](https://github.com/akinomyoga/ble.sh/wiki/images/demo.gif)

常用する場合には、`.bashrc` に以下の様に記述して下さい。詳細なインストール・設定方法・ble.sh 使用時の `bind` の仕方などは [README](https://github.com/akinomyoga/ble.sh/blob/master/README-ja_JP.md) をご参照下さい。

```sh
# bashrc

# 先頭に以下を追加
[[ $- == *i* ]] && source /path/to/ble.sh --noattach

# bashrc の内容

# 最後に以下を追加
((_ble_bash)) && ble-attach
```

# 2. コマンドラインに色をつける方法?
「入力したコマンドラインに色を付けるとは一体どういう事か」という方は `fish` や `zsh-syntax-highlighting` で検索してみて下さい。記事や動画など沢山でてくるのでそちらを見るのが早いかと思います。(上の画像を御覧になるので良いかもしれませんが、画像一枚で語るのも `fish` や `zsh-syntax-highlighting` に失礼かなと…。) Qiita でも検索してみると色々出てきます (抜けがあったら教えて下さいね):

> **fish**
>
- [bashの代わりにFish shellを使ってみる on CentOS6.x - Qiita](http://qiita.com/CsFactoryitter/items/baf387d69e8b5930cdbf) (追記20150211)
- [fish-shellを使ってみませんか - Qiita](http://qiita.com/mtwtk_man/items/dde92d0a6024bc61fa58)
- [初心者がShellを知りFish〜Fishermanを導入するまで - Qiita](http://qiita.com/missi/items/5f111184b50f7081c92f#_reference-567a049d4696390fe500) (追記20150223)
- … fish ではカラフルなのが当たり前なのか、言及している記事が少ない?
>
> **zsh-syntax-highlighting**
>
- [zsh-syntax-highlightingを使ってターミナルを見やすくする - Qiita](http://qiita.com/KazIgu/items/6aa66560c4137ede25a1)
- [zsh-syntax-highlightingでコマンドをハイライトしてみる - Qiita](http://qiita.com/puttyo_bubu/items/0cf94ca5a764aa22827e)
- [zsh,syntax,highlight,oh-my-zsh - zsh-syntax-highlightingをoh-my-zshで利用する - Qiita](http://qiita.com/yoan/items/56be64f65cac1bc351fa)
- [コマンドライン編集機能 Zsh Line Editor を使いこなす - Qiita](http://qiita.com/b4b4r07/items/8db0257d2e6f6b19ecb9)
- [zsh使いでこれ使わないのは結構損してる - Qiita](http://qiita.com/sachin21/items/440cf71952da99fd8832)

さて、Bash で同じ様なことをする為には対話シェルの動作を変更しなければなりません。Bash で対話シェルの動作を変更しようと思ったら2つの方向性が考えられます:

+ Bash のソースを弄る
+ `.bashrc` の設定で頑張る (← 今回はこっち)

一番確実なのは **1 つ目** ―― GNU Bash だか GNU Readline だかのソースコードを弄るという事 ―― なのかもしれませんが、色々面倒です。何より公式版 bash の version が新しくなる度に、マージ・デバグやらをしなければならないと思うとやる気がしません。特に、僕は `bash` の新しい機能が好きなので**これは没**です。

そこで **2 つ目**の `.bashrc` の設定で頑張る (というかスクリプトを書いて頑張る) という方向を考える訳です。でも、スクリプトで頑張ると言ってもできる事は限られています。そんな中で唯一の光明が **bind -x** です。

## 2.1 `bind -x` とは
Bash の組込コマンドです。そもそも `bind` コマンドとは、GNU Readline (Bash の対話シェルのコマンドラインエディタ) におけるキーバインディングを変更する為のコマンドです。例えば、

```bash:~/.bashrc
bind '" ":magic-space'
```

とすると bash のコマンドライン編集時にスペース `" "` を入力した時に Readline コマンドの `magic-space` (スペース挿入&履歴展開) が呼び出される様になります。GNU Readline 専用の設定ファイル `~/.inputrc` でも等価なことができますね。

しかし、`bind` には単純な Readline の設定ではできない強力な機能があります。`-x` オプションです。これを使うと、予め `readline` や `bash` が用意した Readline コマンドではなく、**任意のシェルコマンド**にキーをバインドすることができます。つまり、特定のキーが入力された時に好きなコマンドを呼び出せるのです。

呼び出されるコマンド (というか**シェル関数**) の中では2つのシェル変数 

* `READLINE_LINE`
* `READLINE_POINT`
 
を参照・変更できます。`READLINE_LINE` は現在のコマンドラインの内容で、`READLINE_POINT` は現在のカーソルの位置です。(ただし **単位は文字数ではなく何故かバイト数**: この仕様のせいで世の中の多くの `bind -x` を利用した機能が日本語で変なことになる気が…)。この2つの変数の内容を変更することで、編集内容を変更したりカーソルを移動したりする機能を実装できます。

例えば、現在のコマンドラインの内容を反転 (reverse) する機能を作ってみましょう! 以下の様になります:

```bash:~/.bashrc
# 編集内容を READLINE_LINE 中の文字の順を反転する
function rl_my_reverse {
  local text="$READLINE_LINE" i
  READLINE_LINE= READLINE_POINT=0
  for ((i=${#text}-1;i>=0;i--)); do
    READLINE_LINE+=${text:i:1}
  done
}

# M-a をバインド
bind -x $'"\ea":rl_my_reverse'
```

…コマンドラインの内容を反転して何の訳に立つのかは分かりませんが。

ここで Bash の動作についてもう少し詳しく書いておきます。`bind -x` のキーが入力されたとき Bash は以下のように動作します:

 1. 表示しているプロンプトとコマンドラインを端末画面から消去する
 2. シェル変数 `READLINE_LINE`, `READLINE_POINT` を設定し `bind -x` で登録されたコマンドを呼び出す
 3. コマンドラインの内容を `READLINE_LINE`, `READLINE_POINT` に更新して、再度プロンプトとコマンドラインを端末に描画する

序でに、`bind -x` について説明のある他の記事も探してみましたが余りありませんね…かろうじて以下に `READLINE_LINE`, `READLINE_POINT` の使用が見られます:

>
- [bashのコマンドラインに日時を挿入 - Qiita](http://qiita.com/GHR/items/f871d189b4d8c651e5f6)
- [bash/zsh のヒストリを peco で便利にする - Qiita>](http://qiita.com/comutt/items/f54e755f22508a6c7d78)
- [pecoインストール - Qiita](http://qiita.com/tukiyo3/items/1548e50fe04d44b9a342)
- [おい、peco もいいけど fzf 使えよ - Qiita](http://qiita.com/b4b4r07/items/9e1bbffb1be70b6ce033)

## 2.2 `bind -x` の問題点
さて、任意のコマンドにキーをバインドできるという事は!! `bind -x` を ~~悪用して、~~ 否、有効に活用してシェル関数の中でコマンドライン編集と関係ないことをすれば**何でもできる!! カラフルな表示もできる!!** と思う訳ですが…

`bind -x` による試みは、実はすぐに壁に当たります。

愚直に考えれば、コマンドラインの内容に変更の生じるキー入力全てに介入して、Bash の**オリジナルの描画をカラフルな描画で上書き**してしまえば良いと思う訳です:

```bash:~/.bashrc
# ↓ 介入の雰囲気 (?)
function my_custom_readline_command {
  # オリジナルの readline コマンドを呼び出す
  original-readline-command

  # 追加機能: コマンドラインをカラフルな表示で上書き
  some codes...
}
bind -x '"hoge":my_custom_readline_command'
```

+ (問題点1) しかし、重大な問題点があります…**Readline コマンドをシェルスクリプトから呼び出すことができない**という事です。つまり、`bind -x` のコマンドは既存の機能に対する拡張としてではなくて、完全に新しい機能として実装しなければならないのです。
+ (問題点2) 他にも問題点はあります。Bash がコマンドラインの内容を描画するタイミングです。コマンドラインの内容が描画されるのは `bind -x` のコマンドを実行した後です。つまり、**`bash` の出力をカラフル版で上書きしているつもりが、逆に自分の出力を `bash` にモノクロ版で上書きされ**てしまいます。

## 2.3 Readline さようなら…
上記の問題から半ば諦め気味になります。解決の為にはかなり面倒・無理なことをする必要があります:

+ 問題点1に関しては、Readline コマンドの一つ一つについて同等の機能をシェル関数で**再実装**する必要があります。
+ 問題点2に関しては、かなり無理矢理な方法ですが一応 Bash が何も出力しないようにできます。**全てを空に**するのです: `PS1= READLINE_LINE='' READLINE_POINT=0`。代わりにプロンプトと編集内容は自分で別の変数に覚えておく必要があります。そもそも全ての Readline コマンドを自分で実装するのであれば、Readline の変数に意味のある値を入れておく必要は全くないのです。プロンプトに関しても、正確な描画の為にはプロンプトの末端の座標が必要(特に複数行編集時)で、その為には結局自分で**プロンプトの内容を計算**しなければなりません。ここまで来るとプロンプトを Readline に描画させようと自分で描画しようと大差ないので `PS1` も空でよいのです。

編集機能の再実装からプロンプトの自前計算まで必要となると、かなりやる気が削がれます。それでも、上記のアイディアが実現可能かどうか確かめるという動機のみで、取り敢えずちまちまと実装を行いました。いつしか、軋みながらも何とか動くような物ができました。

改めてみてみると Readline を全く使わない仕様に…。ただ表示に色をつけるだけのつもりで書いていた Bash スクリプトが、いつの間にかに**Bash スクリプトで書かれたコマンドラインエディタ**に成長していたということです。いや、「自分でカラフルな描画をする & Readline コマンドを一つ一つシェル関数で模倣する」などと言い出した時点でこうなるしかないですね。

(タイトルのスクリプトの名前 `ble.sh` はここまで来てからつけたもの。`zle` (Zsh Line Editor) の真似っこで、但しスクリプトで書かれているという事を強調?して。)

しかし、実用とするには色々と問題がありました(※過去形):

- 常用のためには模倣して実装しなければならない Readline コマンドが多すぎる。実装が面倒。
- ちらつきが激しい。(既に `bind -x` の項で説明した様に、毎回 `bind -x` コマンドが呼び出される直前にコマンドラインの内容が全消去される為。)
- 何より遅い。ひたすら遅い。

## 2.4 現在
実は上記のところまでは2年半ぐらい前(2013-06)に到達していました…しかし、既に述べた理由で個人的に普段使いにする気にならないと思ったのでずっと放置していました。

それが今年(2015年)になってまた適当に弄っていた所、*速度の問題はかなり改善し、ちらつきの問題も完全に解決できた*ので、調子に乗って Readline コマンド模倣版も充実させました。個人的には満足して暫く使っていたのですが大した問題もなく今では完全に [`ble.sh`](https://github.com/akinomyoga/ble.sh) 常用です。

# 3. 技術的なこと
Bash スクリプトでコマンドラインエディタを実装するという事から割合あらゆる事をスクリプトで実装しなければなりません。`ble.sh` の実装を例に説明をしたい所ですが、余り細かいことを書いてもつまらないと思うので、~~興味のある人 (自分でも似たような物を作ってみたい人) の為に別の記事(後日)にしますね。~~ 余り反響もなく興味のある人もいなそうなので、知りたいという人が出てきた時に書くことにしました。ここでは適当に項目を列挙:

* 入力
  * `\x00` ～ `\xFF` の 256 種類に `bind -x` し、入力を文字符号化方式 `utf-8` に従ってデコード。
  * 入力文字列に含まれるキーシーケンス (エスケープシーケンス) をデコードしキーの列に。
  * キーの組合せを予め登録されたコマンドに dispatch。
  * キーバインディングを自由に変更・設定できる枠組。
* 出力(描画)
  * `PS1` の内容を独自に解析し展開結果を構築・再現。
  * 各文字を端末上の幅や機能 (半角・全角・タブ・改行・etc) に従って配置。
  * 端末制御シーケンス (エスケープシーケンス) を用いて、カーソル位置や描画属性を変更しつつ文字を出力。同時に実際のカーソル位置の移動も追跡。
* 色付け
  * 入力文字列の構文解析 (bash の文法)。速度重視の為、文法構造の部分更新に対応。
  * 文脈・文法要素に応じた描画属性(着色・下線, etc.)。
  * コマンド名はコマンドの種類・存在に応じて色分け、引数名はファイル名との一致に応じて色分け。
  * 各要素に対する描画属性の設定を自由に設定できる枠組。
* 行エディタ
  * 基本的な移動・文字挿入・削除の実装。
  * 範囲選択・切り取り・コピー・貼り付けの実装。
  * 履歴内の移動・検索の実装。
  * 文脈に応じた補完機能 (色付けの為に行う構文解析結果を参照)。
  * `complete` で設定された補完設定の取り込み。
* 速度
  * `fork/exec` を必要最小限に。外部コマンドは使わず、全部 Bash スクリプトで実装 (初期化、`stty` を除く)。
  * コマンド置換も `fork` で遅いので避ける

更に、コマンドラインエディタが全部スクリプトで書かれているので、拡張もやり放題です。`zsh` の `RPROMPT` だとかの機能も簡単に実装できます。個人的には使わないので未実装ですが。

# 4. 最後に

ただ色を付けるためだけにコマンドラインエディタをスクリプトで書くことになってしまったのですが、もっと賢い方法はなかったのかと反省しないでもありません (もっと簡単な方法があったらコメントで教えてください!)。でも、却って自由自在に対話シェルの振る舞いを変更できるようになったので良かったように感じています。

何れにしても、まだまだ [`ble.sh`](https://github.com/akinomyoga/ble.sh) は伸びしろがある様に思います。自力でこういった物を作るのは骨だという方は、`ble.sh` の方を弄ってくれるととても幸せです!

----

> **2015-12-06** 特に `set -o vi` な人には御協力を御願いしたい次第…自分は普段 `emacs` なので、`vi` のコマンド体系の細かい仕様について勘違いしていることも多々あると思いますし、何よりどれが頻出のコマンドか・どれが重要なコマンドかの感覚を持ち合わせていないので。。

→ **2018-03** @cmplstofB さんの助けを得て Vi/Vim 編集モードを実装しました。