Bash で (fish みたいな) カラフルなコマンドライン (ble.sh)

  • 33
    いいね
  • 0
    コメント

こんにちは! GNU Bash (bash) で fishzsh-syntax-highlighting の様に、入力したコマンドラインをその場で色付けできないか? という個人的な試みが成功して実用できるようになったので、この記事ではそのスクリプトと手法の概要について紹介しようと思います。

(手法を紹介するといっても、各自で実装するにはかなり面倒な方法ですが。みんな実用的なテクニックを紹介している中で申し訳ないです…一応 bind -x について説明を詳しく書きましたので見て下さい!!)


概要: "Bash Line Editor" written in Bash Scripts

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

なんでもいいから早く動く物を出せという方はこちら→ git@github.com:akinomyoga/ble.sh.git に実装 (ble.sh) があります(GNU/Linux と Cygwin の上でしか試していないですが…他の環境で試した方がいらしたら様子を教えて頂けると幸いです)。取り敢えず試すだけなら:

最新版 (推奨; GNU gawk/GNU make が必要)
[bash]$ git clone https://github.com/akinomyoga/ble.sh.git && make -C ble.sh
[bash]$ source ble.sh/out/ble.sh
安定版v0.1系列 (古い)
[bash]$ curl -L https://github.com/akinomyoga/ble.sh/releases/download/v0.1.7/ble-0.1.7.tar.xz | tar xJf -
[bash]$ source ble-0.1.7/ble.sh

ble.sh gif animation

常用する場合には、.bashrc に以下の様に記述して下さい。詳細なインストール・設定方法・ble.sh 使用時の bind の仕方などは README をご参照下さい。

# bashrc

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

# bashrc の内容

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

1 コマンドラインに色をつける方法?

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

fish

zsh-syntax-highlighting

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

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

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

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

1.1 bind -x とは

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

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

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

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

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

  • READLINE_LINE
  • READLINE_POINT

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

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

~/.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 の使用が見られます:

1.2 bind -x の問題点

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

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

愚直に考えれば、コマンドラインの内容に変更の生じるキー入力全てに介入して、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 にモノクロ版で上書きされてしまいます。

1.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 コマンドが呼び出される直前にコマンドラインの内容が全消去される為。)
  • 何より遅い。ひたすら遅い。

1.4 現在

実は上記のところまでは2年半ぐらい前に到達していました…しかし、上記の理由で個人的に普段使いにする気にならないと思ったのでずっと放置していました。

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

2 技術的なこと

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

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

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

3 最後に

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

何れにしても、まだまだ ble.sh は伸びしろがある様に思います。自力でこういった物を作るのは骨だという方は、ble.sh の方を弄ってくれるととても幸せです! 特に set -o vi な人には御協力を御願いしたい次第…自分は普段 emacs なので、vi のコマンド体系の細かい仕様について勘違いしていることも多々あると思いますし、何よりどれが頻出のコマンドか・どれが重要なコマンドかの感覚を持ち合わせていないので。。

この投稿は Shell Script Advent Calendar 20156日目の記事です。