Vim で始める正規表現

ぺりー
15 min readDec 25, 2023

--

本記事は Vim Advent Calendar 2023 25日目と Vim駅伝 2023–12–25 に寄稿しました。

こんにちは!ぺりーです。

2023年下半期は、業務等でリファクタリングをすることが増え、Vim で正規表現を活用する機会が増えました。

今までは正規表現をあまり使えず、都度調べていたので、なんとなく読めるけど正規表現を思いつかなかったり、正しく書けなかったりというレベルでした。

書ければ便利だけど、ChatGPT に聞けばいいじゃんとさえ思っていました。

今思えば正規表現を書き慣れてないだけで、とても損していたなと思います。

Vim で正規表現を使える選択肢があるだけで、リファクタリングの効率も上がる上に、何より気持ちいいです。

正規表現の勉強は気が進まないけど、Vim Life Of Quality が向上するのであれば使いたいという方も多いのではないでしょうか。

Vim の正規表現は、エスケープが多いなど、一般的な正規表現と比較して独特ですが、本記事を読んで、Vim で正規表現使う頻度増やしてもいいかもなと思ってもらえたら嬉しいです。

目次

はじめに

まずは私の正規表現の使い方を紹介します。

ビルトイン関数からプラギンまでほぼ無限の使い道がありますが、私の場合、移動やシンボルの検索、置換を使用したリファクタリング、テキストの整形処理で利用することが多いです。

私はグローバルコマンド( :global )で繰り返し処理をしたり、 grepprg などを使って quickfix に流してからフィルタ等の繰り返し処理をしたりします。

また、作業の効率化のため、最終検索パターンを用いて正規表現を確かめてから置換などの処理を行っています。

- :g/^[:space:]*$/d _
+ /^[:space:]*$
+ :g//d _

それではよく使う正規表現を見ていきましょう。

*本記事は magicignorecase, smartcase ともに有効にした設定を前提にしています。
正規表現が長くなる場合は very magic を有効にし、エスケープ回数を減らすようにしています。

hlsearch, incsearchtraces.vim等で、検索パターンのプレビューが見れるようにするのもおすすめです。

よく使う正規表現

1. *, \+, \=, \{}

  • 0個以上の繰り返しで最長一致する場合は * を使います
    例えば改行以外の文字列全てを表したいときは .* とすれば良いです。
    a* とした場合にマッチするのは "", "a", "aa", "aaa" などで a が0個以上連続した文字列にマッチします。
  • 1個以上の繰り返しで最長一致は \+ を使います
    例えばスペースが一個以上ある場合にマッチしたい場合は \s\+ とすれば良いです。
    "" にマッチさせたくない時に使うと覚えています。
  • 0個か1個以上の繰り返しで最長一致の場合は \= を使います
    users\= の場合は userusers にマッチします。

最短一致もしくは回数指定する場合には \{} を使うことができます。

\{n,m}  n 以上 m 以下の繰り返し。最長一致
\{n,} n 回以上の繰り返し。最長一致
\{,m} 0 以上 m 以下の繰り返し。最長一致
\{n} n 回の繰り返し
\{} 0 回以上の繰り返し。最長一致 (*と同じ)

\{-n,m} n 以上 m 以下の繰り返し。最短一致
\{-n,} n 回以上の繰り返し。最短一致
\{-,m} 0 以上 m 以下の繰り返し。最短一致
\{-n} n 回の繰り返し
\{-} 0 回以上の繰り返し。最短一致

2. \|, []

a,b,c のうちどれかひとつを指定する際には \| を使用して /a\|b\|c を使用できます。

a-z などパターンが長くなってしまう場合には [] を使用して [a-z][abcdefghijklmnopqrstuvwxyz] として列挙することも可能です。

一般的な文字範囲は予め定義されており、定義済みの文字範囲の方が簡潔で、処理も高速です。

a-z であれば /\l で済みます。

大文字になると意味が逆転するので覚えやすいです。

item  matches
\d 数字: [0-9]
\D 数字以外: [^0-9]
\x 16進数の数字: [0-9a-fA-F]
\X 16進数の数字以外: [^0-9a-fA-F]
\s 空白文字<Space> と <Tab>: [ ]
\S 空白文字以外: [^ ]
\l 小文字アルファベット: [a-z]
\L 小文字アルファベット以外: [^a-z]
\u 大文字アルファベット: [A-Z]
\U 大文字アルファベット以外: [^A-Z]
\a アルファベット: [A-Za-z]
\A アルファベット以外: [^A-Za-z]
\d 数字: [0-9]
\D 数字以外: [^0-9]
\x 16 進数字: [0-9A-Fa-f]
\X 16 進数字以外: [^0-9A-Fa-f]
\o 8 進数字: [0-7]
\O 8 進数字以外: [^0-7]
\w 単語を構成する文字: [0-9A-Za-z_]
\W 単語を構成する文字以外: [^0-9A-Za-z_]
\h 単語の先頭の文字: [A-Za-z_]
\H 単語の先頭の文字以外: [^A-Za-z_]

[] 内で定義済み文字範囲は使用できないため、[] ではなく \| を使用するか、[] 内で文字クラスを使うこともできます。

[:alnum:]     isalnum ASCII の英数字
[:alpha:] isalpha ASCII の英字
[:blank:] スペースとタブ
[:cntrl:] iscntrl ASCII コントロール文字
[:digit:] 10 進数字、'0' から '9'
[:graph:] isgraph スペース以外の ASCII 印字可能文字
[:lower:] 小文字英字 ('ignorecase' がオンのときはすべての英字)
[:print:] スペースを含む印字可能文字
[:punct:] ispunct ASCII の句読点
[:space:] 空白文字: スペース、タブ、復帰コード、改行コード、垂直タブ、改ページ
[:upper:] 大文字英字 ('ignorecase' がオンのときはすべての英字)
[:xdigit:] 16 進数字: 0-9, a-f, A-F
[:return:] <CR> 文字
[:tab:] <Tab> 文字
[:escape:] <Esc> 文字
[:backspace:] <BS> 文字
[:ident:] 識別子文字 ("\i" と同じ)
[:keyword:] キーワード文字 ("\k" と同じ)
[:fname:] ファイル名の文字 ("\f" と同じ)

3. [^]

文字範囲の補集合は [] の先頭に ^ をつけることで表現できます。

先述した通り、定義済み文字範囲を使う方が簡潔で処理も速いため、定義済み文字範囲であればそれを使う方が良いです。

例えば a-z 以外であれば /[^a-z] と書くこともできますが /\L の方が良いということです。

4. \<,\>

文字ではなく単語にマッチさせるときは \<,\> によって囲みます。

以下の例で war にだけマッチさせたい時は \<war\> というように \<\> によって囲むことで単語として指定することができます。

war ←これだけマッチ
ward
warn
wart
warm
warry
warier
warrant
etc...

war にカーソルがある場合には * で単語の後方検索を行ってくれます
単語検索したくない場合は g* で文字検索ができます。

片方だけを使用して、wa から始まる単語にマッチさせたり、ar で終わる単語にマッチさせることもできます。

5. \_x

改行を含んだパターンは \n で探せます。

定義済み文字範囲と改行を指定したい時は \_x という形で指定することができます。

例えばスペースと改行の場合は、 \_s とすることでスペースと改行のどちらにもマッチさせることができます。

func f1() error {
_, err := f2()
if err != nil {
return err
}
return nil
}

err := f2() から return err までマッチさせる場合には下記のように指定することができます。

/err.*\_s\+if\s+err.*\_s.*

6. /{pattern}/{offset}

正規表現というより検索に限定したユースケースですが、文字オフセットを使うことで任意の位置にジャンプすることができます。

例えば関数名 service.GetUser()service.GetUsers() に変更し、返り値を3つに変更し、第一返り値の変数名を user から users に変更するとします。

- user, err := service.GetUser(ctx)
+ users, count, err := service.GetUsers(ctx, limit)

/user では [uU]ser の1文字目の [uU] にカーソルが飛びますが、 //e+1 とオフセットを指定することで [uU]serr の後に移動することができるので s を足すだけで済みますね。

users, err := service.GetUsers(ctx)

*説明のため変数名 user が固定されている場合に限ります。

さらにオフセットに ; を使用すると連続して検索することもできます。
次に ; を使って users, err からusers, count, err に修正してみます。

/users,\s\?\zserr/e+1;?GetUsers

これで users, <cursor>err となったので、 count を追加することもできました。
*\zsについては次で説明します。

/GetUsers(ctx/e+1

最後に limit を第二引数に追加して完了です。

今回はジャンプを利用して修正しましたが、置換する場合は置換コマンドのフラグ c を使うことが多いです。

そのほかのオフセットの指定の仕方は下記の通りです。

[num]     [num]行下、1 桁目に移動
+[num] [num]行下、1 桁目に移動
-[num] [num]行上、1 桁目に移動
e[+num] [num]文字右に、マッチ部分の終わりの場所から移動
e[-num] [num]文字左に、マッチ部分の終わりの場所から移動
s[+num] [num]文字右に、マッチ部分の初めの場所(start)から移動
s[-num] [num]文字左に、マッチ部分の初めの場所(start)から移動
b[+num] [num] 上記 s[+num] と同じ (begin の b)
b[-num] [num] 上記 s[-num] と同じ (begin の b)
;{pattern} さらに検索する。

オフセットは下記のようにオペレータと併用することもできます。

/foo<CR>   "foo" を検索
c//e<CR> マッチした文字列を変更
bar<Esc> 置換する文字を入力
//<CR> 次のマッチへ移動
c//e<CR> マッチした文字列を変更
beep<Esc> 今度は別の文字で置換

7. \zs, \ze

\zs は肯定の戻り読み( \%(pattern\)\@<= と同じ)で直前のパターンが左側に存在する場合のみマッチし、 \ze は肯定の先読み( \%(pattern\)\@= )でパターンが右側に続く場合のみマッチします。

\zs, \ze もカーソル位置を調節できるという点では文字のオフセットと似ているかもしれません。

文字のオフセットではカーソル位置までが操作の対象になりますが、\zs, \ze は正規表現中の任意の場所でカーソル位置・マッチ位置を指定することができるためとても使いやすいです。

例えば、Value: の後に意図せず変数を書いてしまい、それを変更したいとします。

Value: "hogehoge",
Value:"fugafuga",
Value: varvalue,
Value: "Mr."+varnameB,

/Value:\s\= では最初にマッチした V にカーソルが移ってしまいますが、/Value:\s\=\zs とすることで、( n, N を使っても) " または変数名にカーソルを移動でき、編集にスムーズに入ることができます。

\ze\zs と合わせて使うことで全体にマッチした文字列の一部分だけに絞ってマッチすることができます。

下記の例では '' を抜いてマッチするには /"\zs[^"]\+\ze" と指定すれば良いです。

abc
'abc' ←こっちのabc(''抜き)にだけマッチ

8. \%[]

任意文字にマッチするアトムです。

例えばExコマンドの functionfun が必須で続く ction が任意ですが、 /\<fun\%[ction]\> と書くことができます。

この場合、fun, func, funct, functio, function にマッチします。

9. \(\)

パターンをまとめることができ、部分正規表現としてカウントされ、\1, …, \9 として参照して使うことができます。

置換を例にすると、:%s/\_^\(function\|func\)\(!\)\?/def\2 というようにパターンをまとめたり、後から参照するために部分正規表現を使うことができます。

- func MyFunc1(text)
+ def MyFunc1(text)

- func! MyFunc2(text)
+ def! MyFunc2(text)

- function MyFunc3(text)
+ def MyFunc3(text)

- function! MyFunc4(text)
+ def MyFunc4(text)

10. \%(\)

\(\) による部分正規表現は9個までしか使えませんでした。

後から参照しない部分正規表現であれば \(\) の代わりに \%(\) を使うことができ、より高速に処理することができます。

例えば、先の例では一つ目の部分正規表現を後から参照していないため、次のように書き直すことができます

- :%s/\_^\(function\|func\)\(!\)\?/def\2
+ :%s/\_^\%(func\|function\)\(!\)\?/def\1

特に、関数のシグネチャを丸ごと書き換える時(例えば引数に型をつける)など、部分正規表現を多く参照する場合は、 \(\) の代わりに \%(\) を使うことで、一度に編集できる幅が広げることができます。

また長い正規表現であっても参照対象とそれに対応する番号を探しやすくなります。

練習問題

問題を解いて復習してみます。

1. 行の先頭/末尾の空白にマッチ

/^\s\+
/\s\+$

2. ,の後ろの2つ以上のスペースにマッチ

/,[[:blank:]]\{2,}

3. ファイルの拡張子にマッチ

/\.\zs[^\.]*$

4. 小数にマッチ

+1.01
-1.0
/[+-]\?\d*\.\d\+

5. 代入にマッチ

Goで構造体のフィールドに代入する場合を想定しました

A.B = C
A.B.D = E.F
a = b
/\w\+\(\.\w\)*\s*=\s*.\+

6. フィールドを置き換える

置換コマンドと正規表現を使って書き換えます。

下の例にあるように、jsonのkey, valueを入れ替え、valueにはシングルクォート( ' )をつけてみます

{
"name": "candy",
"price" : "100",
"unit":"yen",
"stock": "1",
}
// 変更↓
{
"candy": "'name'",
"100": "'price'",
"yen": "'unit'",
"1": "'stock'",
}
/\v(.)(\w{-})(.)\s*(.)\s*("\w{-}")
:%s//\5\4 \1'\2'\3

7. 改行して追記する

下記のコードのエラーを返却する直前に transaction.Rollback() を入れてみます

  err := f1()
if err != nil {
+ transaction.Rollback()
return err
}

func f2() error {
err := f1()
if err != nil {
+ transaction.Rollback()
return err
}
resp, err := f3()
if err != nil {
+ transaction.Rollback()
return err
}
return nil
}
:'<,'>s/\verr.*\_s+if\s+err.*(\_s+)\zs\ze.*/transaction.Rollback()\1

終わりに

正規表現が冗長になっても、覚えやすさと打ちやすさを優先するのが正規表現を特訓するコツだと思いました。

LSPがないVim9 scriptへの移行時には正規表現が大活躍しました。

正規表現周りについては Vim help の usr_27pattern を読むのがよかったです。
こちらの本を参考にさせていただきました。

本記事を読んでVimで正規表現ちょっと使ってみようかなと思ってもらえたら嬉しいです。

--

--

ぺりー

Satoru Kitaguchi. Backend Engineer at eureka, Inc.