キーボードマクロで遊ぼう2011年12月25日 06:06

こんにちは。Vim Advent Calendar 2011 25日目の@plasterです。Vimって素敵なエディタですよね。

今日の記事のネタのメモをどこに保存していたか忘れてlocatefindgrepを重ねた挙句、 Vim起動して[CTRL-O]ですぐ見つかりました。知らなかったひとは今すぐ:h CTRL-Oしましょう。

エディタは生産性の源だと思います。

さて今日は、その生産性を効率的にむだづかいする方法の紹介です。いいんです、実用性などに縛られないからこその愛なのです。目指せ極北!

というわけで、かんたんなアスキーアート生成と、数列の生成と、迷路ソルバやります。全部キーボードマクロで

フィボなっち

まずはアスキーアートの生成から。見出しの「フィボなっち」というのは

(●´ー`●)
(●´ー`●)
(●´ー`●)(●´ー`●)
(●´ー`●)(●´ー`●)(●´ー`●)
(●´ー`●)(●´ー`●)(●´ー`●)(●´ー`●)(●´ー`●)
...

1行あたりの(●´ー`●)の個数が 1, 1, 2, 3, 5, ……とフィボナッチ数列になっているテキストです。

特になっちに思い入れがあるわけではないのですが、なんとなく和むAAなので、ちょっと気に入っています。 このテキストの入力を Vimで自動化してみましょう。

最初の2行は、そのまま入力します。
  • O(●´ー`●)[Esc]Yp
Oで新しく1行あけつつ挿入モードに入り、テキストを入力して[Esc]でモードを抜けます。 フィボナッチ数列は最初の2項が基本なので、Ypでコピペして2行にしておきます。

ステップ

今、バッファが

(●´ー`●)
(●´ー`●)

となっていて、2行目にカーソルがあるはずですね。ここで ykjpgJと入力すれば、次のようなバッファになります。

(●´ー`●)
(●´ー`●)
(●´ー`●)(●´ー`●)

カーソルは3行目です。ykjpgJを打つたびに、最後の2行をつなげたものが最終行に追加されます。たかだか6回の打鍵ですが、自動化してしまいましょう。

キーボードマクロの記録と呼び出し

qaでマクロの記録を開始します。その後qで記録を終了し、@aでマクロの呼び出しができます。たとえばqaykjpgJqと入力すれば、ykjpgJを実行しつつ記録するわけです。

(●´ー`●)
(●´ー`●)
(●´ー`●)(●´ー`●)
(●´ー`●)(●´ー`●)(●´ー`●)

以後、最終行にカーソルがある状態で@aを入力するたびに1行ずつ増えていきます。

(●´ー`●)
(●´ー`●)
(●´ー`●)(●´ー`●)
(●´ー`●)(●´ー`●)(●´ー`●)
(●´ー`●)(●´ー`●)(●´ー`●)(●´ー`●)(●´ー`●)

3@aとか打てば、3行ふえます。99@aとかももちろんアリ!ですが、すごいことになるので注意しましょう。万一やってしまって Vim が暴走しちゃったら、落ち着いて[CTRL-C]です。

フィボナッチ数列

コピペでひたすらテキストを増やしていく例を紹介しましたが、もちろん普通にフィボナッチ数列を作ることもできます。

……とその前に、[CTRL-A]を紹介します。あと、レジスタとマクロの関係を説明します。

CTRL-A

たとえば、バッファに

1

とある状態で、1の部分にカーソルを持っていって[CTRL-A]すると

2

になります。もう一度[CTRL-A]すると

3
になります。[CTRL-A]するたび1ずつ増えるね!ってことです。 10[CTRL-A]とすれば10ふえます。
13

マクロとレジスタ

さきほどはqaqを使ってキーボードマクロを記録しました。記録されたマクロは@aで呼び出すことができるわけですが、 実はレジスタに入っている内容ならば、なんでも呼び出して実行することができます

Vimしらない人がこの記事を読んでくださってるのか謎ですが念のためにひとことで説明しておくと、レジスタっていうのは、Vimにおけるクリップボードみたいなものです。 テキストをヤンクや削除したりキーボードマクロを記録したりすると、とにかく レジスタ というものに記憶されるんだと覚えておけば間違いありません。

ヤンク内容を実行できる機能は、バッファに「自動で実行したい内容(の一部)」が書かれているようなときに大変役立ちます。キーボードマクロで遊ぶときの基盤技術ともいえます。 たとえばバッファに

10

という行があって、この行の行頭にカーソルがあるとします。 y$すると無名レジスタ"10が入ります。 この状態で@"と入力すると、10を入力したのと同じことになります。続けて[CTRL-A]を入力すれば、全体では10[CTRL-A]したのと同じことになり、この行のテキスト1010インクリメントされて、次のように変化しているはずです。

20

この先の展開が丸見えの方もいらっしゃるかと思います。 準備できたので、フィボナッチ数列の生成をしてみましょう。

最初の2行はそのまま入力してしまいます。

  • O1[Esc]Yp

バッファは次のような2行になり、2行目にカーソルがあるはずです。

1
1

ステップ

フィボなっち のときと同様に、ykjpgJします。

1
1
11

カーソルは最下行の2桁目、最初の1の直後にあるはずです。この状態で、Dしましょう。

1
1
1

ご存知のとおりDは、カーソル位置から行末までを削除します。 これは1行未満の削除なので、小削除用レジスタ-に削除内容が入ります。 ここで@-することで、さきほど削除された1がVimに入力されている状態になります。さらに[CTRL-A]すると、残った部分がインクリメントされます。

1
1
2
全部1ばかりでわかりづらかったので、キーボードマクロを記録しがてら、もう1ステップ見てみましょう。3行めにカーソルがある状態でqaykjpgJすると:
1
1
2
12

続けてD

1
1
2
1

カーソルは4行目にあり、小削除用レジスタ-には2が入っています。 ここで@-[CTRL-A]と入力すれば、2[CTRL-A]するのと等価なので、4行目が2インクリメントされて

1
1
2
3

となります。qを入力してマクロ記録を終了して、以後@aと打つたびに、つぎつぎとフィボナッチ数がふえていきます。 10@aとかも余裕です。

1
1
2
3
5
8
13
21
34
55
89
144
233
377

迷路を解こう

ちょっと前、、、といっても2年近く経ってしまったようなのですが、迷路ソルバが流行ってましたよね。たしか、Vimスクリプトで解いてた方もいらしたように記憶しています。

私も実は anarchy golfmaze solving にあるものをVimで解いたことがあるので、その中身を紹介します。 ただし、

  • Vimといってもキーボードマクロで実装します。
  • 実装の都合上、以下の制限があります。
    • ナイーブな「深さ優先探索」するので、 迷路の形状によっては必ずしも最短経路になりません
    • 外壁が矩形になっていることを前提とします。
    • スタート地点(として扱うことのできる外壁の空き)が、 外壁の上辺または左辺にあることを前提とします。

(以下の設問および解答は anarchy golf からの引用です。)

こういう迷路があったとき (「#」が壁、空白が通行可能箇所)

############################# ###########
#   #   #       #     #     #     #     #
# # # ### ##### ### ### ### ##### # #####
# # # #   #   #       # #   #     #     #
# # # # ### # ####### # ##### ### ### # #
# # #   #   #   # #   # #     # # #   # #
### # ### # ### # # ### # ##### # ##### #
#   #     #   #   # #   # #   # # #   # #
# ########### ##### ### # ### # # # # # #
#   #       #     # #   # #     # # # # #
# # ##### ####### # # ### # ##### # # # #
  #       #   # # #     #       #   # # #
# ########### # # ##### ####### ####### #
# #   #       #   #   #   #   #     #   #
# # # ### ####### # ##### # ##### # # ###
# # #   #     #         #       # # #   #
# ##### ### ### ####### ####### # ##### #
# #       #   #     #   #     # #   # # #
# # ##### ### ##### # ### ### # # # # # #
#       #           #     #   #   #     #
#########################################

「.」で入口から出口までの経路を示すのがミッションです。

#############################.###########
#   #   #.......#     #     #.....#     #
# # # ###.#####.### ### ### #####.# #####
# # # #...#   #.......# #   #.....#     #
# # # #.### # #######.# #####.### ### # #
# # #...#...#   # #...# #.....# # #   # #
### #.###.#.### # #.### #.##### # ##### #
#   #.....#...#   #.#   #.#   # # #   # #
# ###########.#####.### #.### # # # # # #
#   #       #.....#.#   #.#     # # # # #
# # ##### #######.#.# ###.# ##### # # # #
..#       #   # #.#.....#.......#   # # #
#.########### # #.#####.#######.####### #
#.#   #       #  .#   #...#   #...  #   #
#.# # ### #######.# #####.# #####.# # ###
#.# #   #     #...      #.......#.# #   #
#.##### ### ###.####### #######.#.##### #
#.#.......#   #.....#   #     #.#.  # # #
#.#.#####.### #####.# ### ### #.#.# # # #
#...    #...........#     #   #...#     #
#########################################

設問に入口と出口の区別はないのですが、 とにかく「外壁のあいてる2箇所をつなぐ経路を出しましょう」ということですね。

解き方の方針

  1. スタート位置にカーソルを持っていき、
  2. カーソル位置の文字が空白 なら、以下 3. ~ 8. を順に実行する。 そうでなければ、何もしない。
  3. カーソル位置の文字を.で上書きする。
  4. カーソルをひとつ右に移動し、2. を再帰的に実行し、カーソルをひとつ左に移動する。
  5. カーソルをひとつ下に移動し、2. を再帰的に実行し、カーソルをひとつ上に移動する。
  6. カーソルをひとつ左に移動し、2. を再帰的に実行し、カーソルをひとつ右に移動する。
  7. カーソルをひとつ上に移動し、2. を再帰的に実行し、カーソルをひとつ下に移動する。
  8. カーソル位置に空白 を書き込む。

なお、途中でカーソルの移動が失敗したら(つまり出口にたどり着いていたら!)、大域脱出して実行終了ということにします。

実装

まずバッファに次のようなテキストを1行、書いておきます。

  • yl@=@0==" "?"r.l@1hj@1kh@1lk@1jr ":""

この行でddして、テキストをレジスタ1に入れます。 あとは、カーソルを迷路の入口

                             ↓ココ
############################# ###########
#   #   #       #     #     #     #     #

に持ってきて@1すれば、迷路が解けます。1行で解けるんですね、Vimってすごい!

実行してみた様子のキャプチャを以下に貼ります:
キーボードマクロな迷路ソルバ実行デモ

解説

肝になっているというかキモいのが、expression用レジスタ=です。 「式を評価して、その中身がレジスタに入っているものとして扱う」という代物で、ふつうは

  • (ノーマルモードで)"=7*6[Enter]p
  • (挿入モードで)[CTRL-R]=7*6[Enter]

のように、即席で計算式を書いて、その計算結果を文書中に張り付けたいときに活用すると便利なものです。 ところで、これももちろんレジスタなので、計算結果の値をマクロとして直接実行できます。それが@=なのです。(式にどんなものが書けるのか知りたい方は、今すぐお手元のVimで:h expressionしましょう!)

今回の@1で何が起こるか順を追って説明すると:

  1. ylでカーソル位置をヤンクし、レジスタ0に入れる。
  2. @=が来たので、続けて入力されるものを式とみなしてその値をマクロ実行する。
  3. @0==" "?"r.l@1hj@1kh@1lk@1jr ":""を評価し、その値をマクロ実行する。
    • カーソル位置が空白文字だった場合、 式の値は文字列"r.l@1hj@1kh@1lk@1jr "になるので、 r.l@1hj@1kh@1lk@1jr を実行する。これは前述「解き方の方針」3. ~ 8. に相当。
    • そうでなければ、式の値は空文字列""になるので、何も実行しない。

という具合に、素直な再帰実行しています。なお、マクロ実行はコマンドにひとつでも失敗すると即座に中止されます。どんなに深い再帰をしていても全部中断、つまり大域脱出してくれます。今回はこれがぴったりハマりました。

キーボードマクロとanarchy golf

私が expression用レジスタ のこのような使い方を知ったのは anarchy golf の Tower of hanoiVimnnさん に完敗したときです。 まさかキーボードマクロで素直な再帰が実現できるとは微塵も思っていなかったので目から鱗だったというか、その後の Vimゴルフをまったく違ったものにしてしまう、(少なくとも私の中では)強烈な事件でした。

今更ですが anarchy golf についてひとこと説明しますと、お題を満たすプログラムのソースを投稿して、ソースの短さを競うというものです。プログラムが正しいかどうかは、既定の(最大)3通りの入力に対して、正しい出力を出すこと、で判定されています。

Vimの場合は、「入力が予めバッファに入っている状態で、正しい出力の形にバッファ内容を変更する」ためのキー入力をプログラムとして与えます。それを1バイトでも短くするには、しばしば、キーボードマクロが大変有効なのです。

つまり何が言いたいかというと、キーボードマクロで遊ぶのに anarchy golf は最高ってことですね。 クリスマスに年越しに、Vimゴルフっていいものですよ。

  • 0が0回の繰り返しじゃなくて行頭移動なことにブチキレそうになったり
  • [CTRL-J][Enter]の挙動の違いに気づかずハマったり
  • :sで置換対象がなくてもエラーにしないために最後につけるeがもったいなかったり

いろいろ言い足りないことがある気がするのですが、またの機会ということで。 それではみなさん、(Vimと)よいクリスマスをお過ごしくださいませー。私はこれから寝ます(キリッ