🔁

Vim での一括置換を楽に行う裏技

2023/06/30に公開

はじめに

ファイル内の特定の文字列を一括置換する場面はよくあります。そんなとき Vim では :substitute コマンド(短縮形: :s)を用いて

:%s/(置換前)/(置換後)/g

というコマンドを叩くのがセオリーです (cf. :h :s)。

しかしこれ、まあまあ面倒くさくありませんか?

  • 冒頭に % を書かないといけない
    • 冒頭の % は置換範囲を表し、ファイル全体を置換する、という意味を持ちます (cf. :h :%)。
    • % を書かない場合、カーソルのある行だけが置換対象となります。
    • でも、カーソル行だけを置換対象としたいことって…ファイル全体を置換するケースに比べればそこまで多くないですよね。
  • 末尾に g を書かないといけない
    • 末尾の g は置換コマンドのフラグを表し、その行にあるすべての置換対象を置換する、という意味を持ちます (cf. :h :s_g)。
    • g を書かない場合、1行に2箇所以上マッチする場所があったとしても最初の1箇所しか置換されません。
    • でも、最初の1箇所だけを置換対象としたいことって…(略)
  • スラッシュを3回も書かないといけない [1]
    • 置換前後の文字列のデリミタなので仕方ありません。仕方ありませんが、それはそれとして面倒ですよね。

私は普段、以下の手順で文字列置換を行っています。

  1. 対象の文字列を検索する

    • 基本的には / コマンドで検索します。

      /(置換前)
      
    • * コマンド(カーソル直下の単語を検索)や、VISUAL モードの選択範囲を検索できる vim-asterisk などのプラグインを使うこともよくあります。

  2. 以下のコマンドで一括置換する

    :%s//(置換後)/g
    
    • 置換前の箇所に空文字列を指定した場合、直前に検索していた文字列が指定されます。

できれば、この2番目の一括置換を

:s (置換後)

ぐらいの手間で済ませたいのです。:s<Space>(置換後)<CR> と打つだけで全ての用が済むなら、置換操作も対して面倒ではありません。
しかし :s は組み込みコマンド。通常の方法でコマンドの定義を書き換えることはできません。どうすればいいでしょうか。

解決法

以下の設定を .vimrc に書くだけ。

.vimrc
cnoreabbrev <expr> s getcmdtype() .. getcmdline() ==# ':s' ? [getchar(), ''][1] .. "%s///g<Left><Left>" : 's'

するとあら不思議、 :s<Space><Space> はスペースキーを表す)と打つだけで

:%s//|/g

というコマンド列(| はカーソル位置)に展開されるではありませんか!その状態で置換後の文字列をタイプしエンターを押せば、ファイル上のすべての検索対象が置換後の文字列に置き換わるというわけです。

解説

上で述べた設定は何をしているのか、そのからくりを説明します。

大まかに言えば Vim の短縮入力 (abbreviation) の応用です。これはスペースキーやエンターキーなどをトリガーにして、直前の単語を特定の文字列に展開する機能です。詳しくは以前書いた以下の紹介記事を参照してください。

https://zenn.dev/monaqa/articles/2020-12-22-vim-abbrev

上の記事で紹介されているテクニックを応用すれば

cnoreabbrev <expr> s getcmdtype() .. getcmdline() ==# ":s" ? "%s///g<Left><Left>" : "s"

と書くだけで「:s<Space> と打つと自動で :s// |/g へと展開される」ようになるはずです。<Left> によってカーソルの位置を移動しているところがミソですね。

しかしこれでは、置換後の文字列にトリガー文字であるスペースが1つ入ってしまいます。いちいちスペースを消さなければならないのは少し不便です。

そこで、展開される前に入力文字を1つ受け取る getchar() 関数を呼ぶことで、スペースが入るのを抑制しています (cf. :h getchar())。 getchar() 関数の戻り値(押した文字の unicode コードポイント)自体は要らないので、[getchar(), ''][1] として戻り値を握り潰しています。

おわりに

Vim の abbrev 機能は便利だよ、という話でした。他にも色々と応用がききそうなので、みなさんも試してみてください。

脚注
  1. 今回の記事の本筋からはそれますが、:s コマンドの区切り文字としてスラッシュ以外の ASCII 記号を使うこともできます。置換前後の文字列にスラッシュが含まれるときに知っておくと便利ですが、普通はスラッシュを使うことが多いと思います。 ↩︎

Discussion