ちょっと話題の記事

Macがzshになるなら、ZLEを習得するっきゃない!

2019.06.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、平野です。

WWDC 2019にて、macOS Catalinaではzshがデフォルトのシェルとして採用されることが発表されました。

https://support.apple.com/en-ca/HT208050

もちろんデフォルトが変わるというだけで、使い慣れたシェルを使い続けることができますが、 せっかくなのでそれにかこつけて、zshの機能の一つであるZLEをご紹介したいと思います。 zshというと「補完がすごい!」と紹介されることが多いように感じますが、 補完の機能は使いこなすのが難しくて、正直私には手に負えないと感じています。 一方ZLEは上辺をちょっと理解しただけで、 めちゃくちゃ簡単にインタラクティブシェルに機能追加ができちゃいます!! これを機会にzshを使い始めてもいいのよ?

なお、あくまでもzshの機能の紹介であり、bashとzshの比較とかそういう内容ではありません。 同じようなことは他のシェルでもできるかもしれませんが、 zshではこうやったらできるよ!という記事です。

ZLE

ZLEは正確にはZsh Line Editorといい、コマンドラインに打ち込む ls -lなどの文字列(バッファという)をプログラム的に編集する機能です。 言葉で説明するとややこしいですが、この記事を読んで頂ければ多分理解して頂けるかと思います。

簡単な例

まずはとにかく具体例からです。 下記を.zshrcに追記してください。

function my_edit_func() {
    BUFFER="${BUFFER}xyz"
    CURSOR+=1
    zle redisplay
}
zle -N my_edit_func
bindkey "^j" my_edit_func

source ~/.zshrc.zshrcを再読み込みして、 コマンドライン編集のところでctrl+jを押します。 すると、現在のバッファの末尾にxyzが追記され、カーソルの位置が一つ右に移動します。

操作前
$ abc
  ^   <- カーソル位置
操作後
$ abcxyz
   ^

この内容では役には立ちませんが、 「バッファをプログラム的に編集する」の意図は伝わるかと思います。

コードの説明

コードの内容と挙動からほとんど読み取れるかと思いますが、簡単に説明すると

  • BUFFER
    • コマンドラインとして編集している文字列が格納される変数
    • この変数に任意の文字列を入れると、実際にコマンドラインの文字列も置き換わる
  • CURSOR
    • カーソルがある位置が格納される変数
    • この変数に数値を入れると、実際にコマンドラインのカーソル位置が移動する
  • zle redisplay
    • 画面のリフレッシュ
    • 簡単な編集なら不要だが、つけておくと無難
  • zle -N my_edit_func
    • my_edit_funcをZLEウィジェットというものとして登録する
    • おまじない的に必ずつけるものだと思えば良い
  • bindkey "^j" my_edit_func
    • ctrl+jにウィジェットmy_edit_funcを紐づける

関数内はお好きにどうぞ

my_edit_func関数の中は普通にzshの関数ですので、コマンドを使ってたいてい何でもできます。

下記は、現在のバッファ内容を一度実行し、その出力結果で置き換えるものです。 カーソルの位置は最後に移動させています。

function my_edit_func() {
    BUFFER=$(echo ${BUFFER} | sh)
    CURSOR=${#BUFFER}
    zle redisplay
}
zle -N my_edit_func
bindkey "^j" my_edit_func
操作前
$ echo $LANG
     ^
操作後
$ ja_JP.UTF-8
             ^

複雑な処理は外部プログラムで

シェルスクリプトは小さいStringをコネコネといじるのに非常に不向きなので、 細かい処理を色々行いたい場合は何かしらのプログラミング言語に任せてしまうのが良いです。

下記のような外側部分を作れば、あとはhogehoge.pyで好きにコネコネした結果でバッファを置き換えることができます。 pythonでなくとも、1行目にカーソルの位置を、2行目以降にバッファ文字列を 標準出力で出すようなプログラムであればどんなものでもOKです。

function my_edit_func() {
    local result
    result=$(python hogehoge.py "${BUFFER}" $CURSOR)
    CURSOR=$(sed -n '1p' <<< "${result}")
    BUFFER=$(sed '1d' <<< "${result}")
    zle redisplay
}
zle -N my_edit_func
bindkey "^j" my_edit_func

カーソル移動ウィジェット

CURSORでカーソル位置を操作することができるので、 BUFFERを変えなければ、単純なカーソル移動ウィジェットとして機能します。

下記は、バッファの真ん中にカーソルを持ってくるウィジェットです。 先頭や末尾にはすぐ飛ぶことができるので、真ん中に飛べてもいいんじゃない?的な機能です。

function my_edit_func() {
    CURSOR=$((${#BUFFER} / 2))
    zle redisplay
}
zle -N my_edit_func
bindkey "^j" my_edit_func
操作前
$ vim hogehoge.txt fugafuga.txt
  ^
操作後
$ vim hogehoge.txt fugafuga.txt
                ^

バッファ内容を大幅に変えるような大胆なものは不要でも、 カーソルの移動手段を増強して、単純に目的の場所に早く飛べるようにすると便利です。 バッファ内容によって的確なところに飛ぶような仕掛けを作ると結構捗ります。

既存ウィジェットを利用する

補完の動作に少しだけ変更を加えたいときがあるかもしれません。 補完をいちから自分で実装するのはさすがに大変なので、既存の補完を利用したいです。

zshではタブを押した時の補完もexpand-or-completeウィジェットで行っています1。 なので、自作ウィジェットの中でそれを呼んでやればOKです。 下記は、単語途中でタブが押されても、スペースを挿入して無理やりその位置で補完を実行する例です。

function my_edit_func() {
    if [[ "${RBUFFER:0:1}" != " " ]]; then
        BUFFER="${LBUFFER} ${RBUFFER}"
    fi
    zle expand-or-complete
    zle redisplay
}
zle -N my_edit_func
bindkey "^j" my_edit_func
操作前
$ ls /Apple
         ^
操作後
$ ls /Applications/ le
                   ^

LBUFFERRBUFFER)は現在のカーソル位置よりも左(右)のバッファ文字列です (RBUFFERはカーソルの足元の文字を含みます)。 現在のカーソルの足元がスペースでなければスペースを挿入し、そのあとに補完を行います。

Undo

ZLEの機能ではなくbashやzshの標準機能ですが、ZLEで変換したけど間違った!という時はUndoができます。

ctrl+x u (ctrl+x を押してから u を押す)

で変換する前の状態に戻ります。 ただし戻るのはバッファの状態なので、 バッファに変更のない移動だけのウィジェットは取り消せません。

まとめ

バッファをプログラム的に編集するZLEについて紹介しました。

BUFFERCURSORへ値を代入するだけという、 非常に分かりやすいインタフェースでほとんどどんなカスタマイズも可能になってしまいます! 特にカーソル移動系はお手軽なわりに便利なものも作れるのでオススメです。

また発展形として、ウィジェット内でfzfなどのインタラクティブなアプリを組み込むとできることの幅がグッと広がって、 もはや操作感がまったく違うインタラクティブシェルにしてしまうことも可能です。 下記の記事なども参考に、魔改造されてみてはいかがでしょうか?2

[ターミナル]fzfを使った自作インタラクティブアプリを作ってみよう!〜git addを快適に〜

以上、誰かの参考になれば幸いです。


  1. bindkeyコマンドを実行すれば、割り当て一覧が表示される 
  2. 節度を持って!