LoginSignup
2
3

More than 3 years have passed since last update.

Fishのスニペットプラグインが物凄く便利

Last updated at Posted at 2020-05-30

私の愛用しているFishのプラグインのひとつにexpandが存在します。
このプラグイン、まあすごいものでして個人ランキングでトップ3に入るほどです。

このプラグインは何?

至って簡単、プレースホルダーのないスニペットプラグインです。

たとえば config.fish に以下の記述をしたとしましょう。

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オプションで指定したコマンドの出力で置換してくれるのです。

加えて、このプラグインは fzfpeco と連携して置換内容を決定できる機能もあります。

config.fish
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

とりあえず物は揃ったのでキーバインドを設定してみましょう。

config.fish
# 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

初期値とかホルダーの説明とかほしいところですが、とりあえずはこんな感じでいいでしょう。

うまくいけたらプラグイン化するかもしれません。

参考リンク

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3