私の愛用しているFishのプラグインのひとつにexpandが存在します。
このプラグイン、まあすごいものでして個人ランキングでトップ3に入るほどです。
このプラグインは何?
至って簡単、プレースホルダーのないスニペットプラグインです。
たとえば config.fish に以下の記述をしたとしましょう。
expand-word -p '^nil$' -e "echo '>/dev/null ^&1'"
expand-word -p '^github$' -e "echo https://github.com/"
ここでターミナルを開き、pオプションで入力した正規表現にマッチする文字列を入力し、Tabを入力すると
# Before (カーソル位置は各コマンドラインの末尾にあるとする)
$ echo hoge fuga nil
$ git clone github
# After
$ echo hoge fuga >/dev/null ^&1
$ git clone https://github.com/
このようにeオプションで指定したコマンドの出力で置換してくれるのです。
加えて、このプラグインは fzf
や peco
と連携して置換内容を決定できる機能もあります。
expand-word -p '^git$' -e 'echo git@github.com:'
expand-word -p '^git$' -e 'echo https://github.com'
expand-word -p '^git$' -e 'echo https://gits.github.com'
expand-word -p '^git$' -e 'echo https://rawgithubusercontent.com'
# Tabを入力するとfzfやpecoが起動、上記4つから選択できる
$ git clone git
もちろん単語の指定や出力コマンドなどは自由に変えることができるので、一般的なスニペットプラグイン以上に発展的な使い方ができる代物です。
特に入力文字数が多くなりがちな対話シェルにおいては非常に有用なプラグインだと思っています。他にきちんと説明している記事がないのが驚きというくらいオススメです。
……と、結構推してはいるのですが。
私としては「まだまだ可能性があるのでは?」と思うところがあるのです。
ということで以下のような活用案を考えてみました。
もっと便利に:置換対象を広げる
問題
まず私としては気に入らない部分がひとつ。
空白を含む置換キーに対応していないのです。
$ expand-word -p '^t1 t2 t3$' -e "echo OK!"
# Before
$ echo t1 t2 t3
# After
$ echo t1 t2 t3
何に困るかはいつか述べるとして、とりあえず対応させましょう。
調査
調べてみると、以下の2つは等価になります。
expand-word -p 'regex' -e 'cmd'
expand-word -c "expand:match 'regex'" -e 'cmd'
expand:match
とは何者でしょうか。
function expand:match -d 'Matches a pattern against the current word' -a pattern
commandline -t | grep -E -q "$pattern"
end
commandline -t
、つまり空白区切りで得られる現在位置のトークンを利用していることが分かりますね。これが上記問題の原因と思われます。
加えて、実際に置換する処理にあたる expand:choose-next
にもこんな記述が。
if test -n "$replacement"
commandline -t -r "$replacement"
end
やはりこちらも置換対象は現在位置のトークンになっています。こりゃだめなわけだ。
実装
定義部分の代替として commandline -bc
を使ってみます。行頭から現在のカーソル位置までを取得するコマンドですね。
function expand:morematch -a pattern
# expand:replaceに利用するため、一致部分を出力するようにする
commandline -bc | grep -E -o "$pattern"
end
そして commandline -t -r
に代わる部分も新たに実装。
# commandline -bc -r はエラーになる
# commandline -b -r した後に commandline -C NUM することで再現する
function expand:replace -a before after
test -n "$before"; or return
test -n "$after"; or return
set -l bc (commandline -bc)
set -l pattern "$before\$"
if string match -qr "$pattern" "$bc"
set -l new_bc (string replace -r "$pattern" "$after" "$bc")
commandline -b -r (string replace "$bc" "$new_bc" (commandline -b))
commandline -C (string length $new_bc)
end
end
これらを用いて各種ソースを修正。
# plugin-expand/functions/expand:execute.fish
set -g __expand_replacements
+ set -g __expand_matched
for expansion in $__expand_expanders
set -l expansion (echo $expansion)
if eval "$expansion[1]" > /dev/null
if set -l replacements (eval "$expansion[2]" | sed '/^\s*$/d')
set __expand_replacements $__expand_replacements $replacements
+ set __expand_matched $__expand_matched (eval "$expansion[1]")
end
end
# plugin-expand/functions/expand:choose-next.fish
if test -n "$replacement"
- commandline -t -r "$replacement"
+ expand:replace "$__expand_matched[1]" "$replacement"
end
そして結果はこちら。
# Before
$ expanded-word -c "expand:morematch 't1 t2 t3\$'" -e "echo OK!"
$ echo t1 t2 t3
# After
$ echo OK!
やったぜ。
さらに便利に:プレースホルダーに対応させる
上述した通り、このプラグインはスニペットプラグインのようでありながらもプレースホルダーに対応していません。あくまで「カーソル位置の単語を指定のコマンドで置換する」ぐらいの役割しかないためです。
しかしこういう場面を考えてみましょう。
# After
$ git clone htpps://github.com/
これ、以下のようなプレースホルダーが用意できればよりよくなるのではないでしょうか?
# なんらかのキーを押すと{n: ...}に順次進んでいく
$ git clone https://github.com/{1: username}/{2: reponame}.git
実装
まずはプレースホルダーの書式を考えてみましょう。
一般的なのは上記のように括弧で囲むか、$1
のように変数っぽくする方法があります。
ただし後者については awk
のように他コマンドで使用することもありますし、隣接する文字列との区別ができなくなる可能性もあります。ここはFishの文化を利用して滅多に使われないであろう [[]]
で囲った部分をプレースホルダーにしましょう。
$ git clone https://github.com/[[]]/[[]].git
次にプレースホルダーへのジャンプについて考えましょう。
どのキーを押すことでジャンプするのかはユーザーの設定に任せるとして、ここではただジャンプする機能だけを考えてみます。
前述の通り、カーソル位置までのコマンドライン文字列は commandline -bc
で取得でき、その後のカーソルジャンプは commandline -C
で実現できます。
function jump_to_first_placeholder
# 最初の[[]]の中ににジャンプするだけ
set -l bc (commandline -bc)
if string match -q "*[[]]*" "$bc"
# プレースホルダーがあるならジャンプ
commandline -C (string replace -r ']].*$' '' "$bc" | string length)
end
end
function jump_to_next_placeholder
# 次の[[]]の中にジャンプするだけ
set -l b (commandline -b)
set -l bc (commandline -bc)
# ブレースホルダー全体を含んだ状態にしておく
# TODO: [[plugi|n]] のようにプレースホルダー内の末尾にカーソルがない場合に対応させる
string match -qr '.*]]$' "$bc"
or set -l bc "$bc]]"
set -l after_c (string replace "$bc" '' "$b")
if string match -q "*[[]]*" "$after_c"
set -l offset (string length "$bc")
set -l distance (string replace -r ']].*' '' "$after_c" | string length)
commandline -C (math "$offset + $distance")
else
# 次にジャンプする場所がなければ最初のプレースホルダーへ
jump_to_first_placeholder
end
end
ついでにプレースホルダーを外す作業も必要ですね。
function exist_placeholder
string match -qr '(\[{2)|(\]{2})' (commandline -b)
end
function remove_placeholders_force
# コマンド実行直前に実行することを想定
commandline -b -r (string replace -ar '(\[{2)|(\]{2})' '' (commandline -b))
end
とりあえず物は揃ったのでキーバインドを設定してみましょう。
# Ctrl-j (\n) でスニペット展開とプレースホルダーの移動
bind \n expand:execute jump_to_placeholder
function jump_to_placeholder
if set -qg __placeholder_jumped
jump_to_next_placeholder
else
set -g __placeholder_jumped
jump_to_first_placeholder
end
end
# Enter や Ctrl-m (\r) で全プレースホルダー除去してからコマンド実行
# プレースホルダーがない場合はそのままコマンドを実行
bind \r remove_placeholders execute
function remove_placeholders
if exist_placeholder
remove_placeholders_force
set -e -g __placeholder_jumped
end
end
初期値とかホルダーの説明とかほしいところですが、とりあえずはこんな感じでいいでしょう。
うまくいけたらプラグイン化するかもしれません。