13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VimAdvent Calendar 2021

Day 20

:global と :normal コマンドを組み合わせてマクロの代替として使う

Last updated at Posted at 2021-12-25

本記事は、Vim Advent Calender 2021 その1 の20日の代理投稿です。
昨日の記事は @uyo さんの Vim Meme、明日の記事は tmrekk さんの Vim初心者のQuickfixによる検索・置換入門です。

そもそも、マクロってなに

簡単な説明になりますが、 任意の一連のキータイプを記録し、再現する機能です。
具体的には q{char}(任意の操作)q(任意の操作) の部分を記録し、@{char} で記録した操作を再現します。
詳しくは、:h recoding を参照してください。

ところで

私はなんだかマクロ記録中に Vim に「早くしろ」と急き立てられている気分になってしまい、妙にマクロが使いにくく感じてしまってました。
なのでどうにかして他の方法を使って、マクロでできるような一気にテキストを編集するというのをやりたいなと思いました。
結果として、コマンドをつかって一気に色々テキストを編集するという方向で落ち着きました。

具体的には?

コマンドライン経由でマクロと同等のことをし得る方法として挙げられるのは以下のような方法です。

  1. レジスタを直接編集する
  2. :s コマンドを使う
  3. :g/:v コマンドと :norm コマンドを組み合わせる

本記事では私が多用している、最後の「:g/:v コマンドと :norm コマンドを組み合わせる」という方法について紹介していきたいと思います。

まず :g:v コマンドについて

:g:v はそれぞれ

  • :g ... :global コマンドの省略形
  • :v ... :vglobal コマンドの省略形

で、以下のような機能をもつコマンドです:

  • :g

:[range]g/{pattern}/[command] のように使い、[range] で指定された範囲の行のうち、{pattern} にマッチする行のみに対して [command] を実行するコマンドです。
このコマンドは :g! のように ! 付きで使うこともでき、この場合は :g と異なり [range] で指定された範囲の行のうち、{pattern} にマッチしない行のみに対して [command] を実行するコマンドです。
:h :global で help を確認できます。

  • :v

:g! と等価なコマンドです。
:[range]v/{pattern}/[command] のように使い、[range] で指定された範囲の行のうち、{pattern} にマッチしない行のみに対して [command] を実行するコマンドです。
:h :vglobal で help を確認できます。

[range] については :h :range で説明されている各種が、 {pattern} については :h pattern-overview などで確認できる Vim の正規表現が指定でき、加えて指定できるコマンドもよりどりみどりなので、この :g/:vコマンドはかなり応用が効きます。
が、慣れてないと色々ややこしいかもしれないので、最初はとりあえずよく使うイディオムとして、

  • :%g/^/{command}
    バッファの全行に対して {command} を実行する
  • :'<,'>g/^/{command}
    選択された範囲の全行に対して {command} を実行する

の二種類を覚えておくのが良いかと思います。
{pattern} に指定されている ^ は行頭を表す正規表現で任意の行にマッチするため、こういう風に使えます。

次に :norm コマンドについて

:norm コマンドは :normal コマンドの省略形で、ノーマルモードでの入力をエミュレートできるコマンドです。
このコマンドは :norm {keys} のようにして使い、引数として与えた {keys} をエミュレートする入力として解釈します。
また、このコマンドは :norm! のように ! をつけて実行することもできます。
! をつけたときとつけなかったときの違いとしては

  • つけたとき
    vimrc 等で設定しているマッピングがすべて適用される。
    つまり、本当に自分がタイプしたのと同じような挙動をしてくれる。
  • つけなかったとき
    vimrc 等で設定しているマッピングは適用されない。
    つまり、設定なしの素の Vim でタイプしたのと同じような挙動をしてくれる。

という違いがあります。

:h :normal で help を確認できます。

プラグイン等でこのコマンドを使用するときは、基本的にユーザーのマッピングの影響を受けてプラグインの挙動が壊れることのないように、:norm! として使われますが、自身でコマンドを叩く場合は、むしろ普段使っているマッピングを活かすという意味で :norm のように ! 抜きで使うのが良いでしょう。

ただ、本記事の以下で例示されているコマンドにおいては、例示したコマンドの普遍性を優先し、:norm! としています。

制御文字について

本題に入るその前に、少し話はそれますが、制御文字の入力方法について少し書きたいと思います。
:norm コマンド経由でマクロの代替をさせようとしている以上、Insert モード→Normal モードへの移行で使われる <ESC> や、改行を挿入するときの <CR>(<C-m>)、あるいはレジスタの値を入力するための <C-r> などの制御文字をコマンド引数に与える必要が出てくるので、この話題は避けては通れません。

Vim においては、制御文字は直接入力するのと文字列中でエスケープして表現する二種類の方法で表現できます。
直接入力はInsertモード/Command-lineモードで <C-v> のあとに直接続けて入力するとできます。
文字列中でのエスケープは"\<C-{char}>""\<ESC>" のようにして書きます。

例えば <C-r> であれば

  • 直接入力:
    <C-v><C-r> と入力する(^R のような見た目の文字が挿入されるはずです。)
  • 文字列中でエスケープ:
    "\<C-r>" と書く

とします。

制御文字を直接ソースコードに埋め込むのは良くないので、プラグインを作成するときなどは制御文字を "\<C-{char}>" の形式で書きます。つまり、 :norm コマンドに制御文字を含む引数を与えるときは :execute コマンドと組み合わせて :execute "norm! i...\<ESC>" のようにして実装します。
しかし、この文字列中でエスケープするというやり方は一度、多くても数回程度しか使わないコマンドで使うには少々冗長で面倒です。なので、コマンドラインから使う場合は制御文字も直接入力で埋め込んでしまうことをおすすめします。

ただ、記事において文章中に制御文字を含めておくことはできないので、本記事の以下に例示されているコマンドにおいてはすべて文字列中でエスケープして制御文字を表現するというやり方で表記します。

2つを組み合わせる

さて、すこし脱線しましたが、ここからが本編です。
もう皆さんお気づきだとは思いますが、上で紹介した :g/:v コマンドと、:norm コマンドを組み合わせて :[range]g/{pattern}/norm {keys} のように使うことでマクロの代替のようなことをさせることができます。
あとは実践あるのみです。
具体例を見ていきましょう。

Case1. switch 文内の case 文を錬成する

switch (fruit) {
APPLE
ORANGE
GRAPE
}

バッファをこうした状態で

APPLE
ORANGE
GRAPE

の3行を選択したあと

:'<,'>g/^/execute "norm! Icase \<ESC>A:\<CR>break;"

とします。
(ちなみに、制御文字を直接入力するとこのような感じの表示になります)
image.png

すると、バッファの内容がこうなります。

switch (fruit) {
case APPLE:
    break;
case ORANGE:
    break;
case GRAPE:
    break;
}

(インデントのつき方は設定によって多少異なるかもしれません。)

まず、[range]'<,'> となっているので、:g コマンドの対象となる範囲は選択した3行となります。そして、指定されているパターンは ^ なのでその3行はすべてコマンド適用対象になります。(前述したとおりですね。)
あとは残りの execute "norm! Icase \<ESC>A:\<CR>break;" です。この部分が一見カオスですが、解きほぐしてみてみると大して複雑なことはしていません。普段やっている編集作業と同じです。
APPLE の行を例にして説明すると、まず、

APPLE

となっていたのが Icase \<ESC> の部分で

case APPLE

となり、続く A: でこの行の末尾にコロンが挿入されこうなり、

case APPLE:

最後の \<CR>break; のところで

case APPLE:
    break;

になります。(break; にインデントがついているのは Vim の自動インデント機能が発動してるからです。)
これが残りの2行にも適用され、上記のような結果になっています。
便利ですね!

Case2. クラスの Setter を錬成する

class Player {
public:
    power
    life
    money
private:
    int power_, life_, money_;
};

バッファをこうした状態で

    power
    life
    money

の3行を選択したあと

:'<,'>g/^/execute "norm! ^diwaint set_\<C-r>\"(int \<C-r>\");"

とします。
(制御文字を直接入力するとこのような感じになります)
image.png

すると、バッファの内容がこうなります。

class Player {
public:
    int set_power(int power);
    int set_life(int life);
    int set_money(int money);
private:
    int power_, life_, money_;
};

(インデントのつき方は設定によって多少異なるかもしれません。)

まず、[range]'<,'>{pattern}^ なので、Case1 と同様、コマンド適用対象となるのは選択した3行です。
残りは execute "norm! ^diwaint set_\<C-r>\"(int \<C-r>\");"の部分です。 これも Case1 のときのように解きほぐしてみましょう。
power の行を例にしてみると、まず

    power

^diw のところで

    

となり、もともとあった power" レジスタに挿入されます。続く int set_

    int set_

となり、その後の \<C-r>\"" レジスタの中身を挿入していています。 (注:実際に入力するのは <C-r>" というキーシーケンスです。:norm! のコマンドが " で囲まれているため、 レジスタ貼り付け時の " がエスケープ必須になっているので \<C-r>\" という表記になっています。)" レジスタにはさっき diw で削除した power が入っているので

    int set_power

となります。残りの (int \<C-r>\"); の部分で引数の部分が入力され、

    int set_power(int power);

となります。
同じ操作が残りの2行にも適用されて、上記のような結果になります。
いやー、便利ですね!

もっと具体例を挙げられると良かったのかもしれませんが、私がまとまった文章を書き慣れてないのと、時間もあまり残っていないというのがあり、これぐらいで終わりにしたいと思います。

まとめ

マクロを使って編集する代わりに、:g/:v:norm を組み合わせても一気にテキストを編集できる場面があります。便利です。
(便利しか言ってないな私)

13
5
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
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?