無題の備忘録

IT技術について調べたことや学んだこと、試したこと記録するブログです。Erlang、ネットワーク、 セキュリティ、Linux関係のものが多いです。

「実践Vim」を読んで学んだこと - 第5章 コマンドラインモード

Vimの歴史と語源

人に歴史ありというが、Vimにも歴史ありだ。Vim の起源は vi に遡ることができるが、その vi で、モードを持った編集というパラダイムが生み出された。素晴らしい。

そして、vi はさらに ex と呼ばれるラインエディタへと遡ることができると言うのだ。そして、この ex が Vim で使える Exコマンドということだ。昔から連綿と続くエディタの改善の歴史が感じられて、なんとも感慨深い。

exが登場するまではメインフレームコンピュータと端末間の通信速度が惜しかったらしいが、ex が登場したあと、ファイルの変更をリアルタイムで目で確認できるようになり、画面編集モードは、:visualコマンドか、これを省略して:viというコマンドを入力することで有効になったらしい。これが、vi の名前の由来ということだ。

そして、Vimは、「vi improved」を意味しているとのことだ。メインフレームの時代からこれまで考えられてきたテキストを編集するためのアイデアと歴史の淘汰を得て、本当に有用なものだけが残ったものが Vim と言えないだろうか。

ex を感じられるコマンドラインのモードの使い方を学ぼう。

コマンドラインモード

ここでもモードの概念だ。いろいろなモードを使い分けて Vim を操作する。

: キーを押すと、Vimコマンドラインモードに切り替わる。このモードでは、コマンド名を入力し、<CR>を押して、それを実行する。シェルのコマンドのような。コマンドラインモードからノーマルモードへは、<Esc>を押すだけでいつでも復帰できる。

そして、ここで実行するコマンドは、Exコマンドと呼ばれる。/を押して、検索プロンプトが表示されたり、<C-r>=をしてExpressionレジスタにアクセスする際にも、実はコマンドラインモードが有効になっていた。

テキストを編集する主なExコマンドを下記に示す。

コマンド 動作
:[range]delete [x] 指定した行を削除(してレジスタ x に登録)
:[range]yank [x] 指定した行を(レジスタ x に)ヤンク
:[line]put [x] 指定した行の後にレジスタ x からテキストをプット
:[range]copy {address} 指定した行を、{address}で指定される行の下にコピー
:[range]move {address} 指定した行を、{address}で指定される行の下に移動
:[range]join 指定した行を連結
:[range]normal {commands} 指定した行に対してノーマルモードの{commands}を実行
:[range]substitute/{pattern}/{string}/[flags] 指定した行で、{pattern}があればそれを{string}に置換
:[range]global/{pattern}/[cmd] 指定した行のうち、{pattern}がマッチするすべての行で、Exコマンド[cmd]を実行

Exコマンドの特徴は、ノーマルモードを使った何かをするよりも、適用範囲が広いことだ。ノーマルモードのコマンドは現在の文字や現在の行を対象にすることが多いが、Exコマンドは特定の行に対して実行できる。別の言い方をすると、カーソル移動をしなくてもコマンドを実行できるということだ。

行指定、範囲指定でExコマンドを実行

Exコマンドの多くには、行の[range]を指定できる。指定したい範囲の始点となる行番号と終点となる行番号を使って指定する。

<html>
<head>
<meta charset="utf-8">
<title>実践Vimを読んで学んだこと</title>
</head>
<body>
<h1>
例文を書くよ
</h1>
</body>
</html>

:printを使って指定した範囲の確認をする。:printは指定した行を出力するコマンドだ。 :1 を入力した後、:print もしくは省略した :p を実行すると、1 <html> が表示される。これは行番号を指定して1行目を print した例だ。

これは、1つのコマンドで実行することもできる。:1pだ。カーソルはどこにあっても1行目を表示してくれる。

:pは他のコマンドに置き換えることができる。例えば、:3dを実行すれば、3行目 <meta charset="utf-8"> が削除される。これをもしノーマルモードでするなら、3Gを入力して3行目に移動し、ddでカーソル行を削除しなくてはならない。

では、範囲指定をしてみよう。

:2,5pを実行すると、これにより2行目から5行目のhead部分が表示される。範囲しては一般的に:{start},{end}という形式で指定する。

<head>
<meta charset="utf-8">
<title>実践Vimを読んで学んだこと</title>
</head>

{start}, {end}はアドレスで、アドレスに今まで行番号を指定してきたが、パターンやマークと呼ばれるものを指定することもできる。

例えば、現在行を表すのに「.」記号が使える。またファイル末尾を示すのに「$」記号が使え、:.,$pで現在行からファイル末尾までを print できる。

その他、「%」記号には特殊な意味があり、現在のファイルすべての行を表す。この記号は置換操作によく使われる。

:%s/h1/span/

これは、各行にある「h1」を「span」に置換する操作を実行する。

次はパターンによる指定例だ。

:/<body>/,/<\/body>/p を実行すると、下記の部分が表示される。

  6 <body>
  7 <h1>
  8 例文を書くよ
  9 </h1>
 10 </body>

このコマンドも、:{start},{end} という範囲指定の一般形式に従っている。ここでの{start}は、「//」というパターンで、{end}アドレスは「/<\/body>/」というパターンだ。パターンによる指定は、行番号による指定よりも変更に強い。

指定したい範囲に、, を含めたくないとき、下記のようにオフセットを使うことができる。

:/<body>/+1,/<\/body>/-1p を実行すると、下記の部分が表示される。 と は含まれない。

  7 <h1>
  8 例文を書くよ
  9 </h1>

一般的にオフセットは、下記の形式をとる。

:{address}+n

nを省略した場合のデフォルト値は1である。{address}にはこれまで見てきた行番号、マーク、パターンを指定できる。

コピー/移動

:copy もしくは短縮形の :t を使うと、ドキュメントのある行を別の場所へコピーできる。また、:moveコマンドを使うと、ドキュメント内の別の場所へ移動できる。

例として、下記のような数字とアルファベットのアイテムリストがあるとする。

* Item list number
  * item 1 
  * item 2
  * item c
* Item list alphabet
  * item a
  * item b 

Item list alphabet の最後に item c を追加しようとしたとき、4行目の item c をコピーして追加する場合は、item b の行にカーソル位置がある場合、:4copy.を実行すると、4行目がコピーされて現在行の1行下に追加される。copyは省略してcoとしても良い。

* Item list number
  * item 1 
  * item 2
  * item c
* Item list alphabet
  * item a
  * item b 
  * item c

上の例のように、4行目は Item list alphabet の最後にだけある方が適切であるという場合は、:4move$コマンドを実行すれば良い。これは5行目をファイルの末尾に移動するコマンドだ。movem と省略しても良い、 :4m$だ。

* Item list number
  * item 1 
  * item 2
* Item list alphabet
  * item a
  * item b 
  * item c

これらを一般化すると、コピーは :[range]co{address}, 移動は:[range]m{address}という書式で表せる。

範囲選択してノーマルモードのコマンドを実行する

ドットの公式で繰り返し操作できることを何度も述べてきた。繰り返し操作が多いときには、これから説明するような方法も検討したい。

第1章のドットの公式の説明のように、行末に「;」を追加したいとしよう。下記のファイルのように数十行以上に変更を加えたいという場合は、ドットの公式では面倒だ。

var foo = 1
var bar = 'a'
var baz = 'z'
var foobar = foo + bar
var foobarbaz = foo + bar + baz
...(100行くらい続く)

ここで学ぶ方法は、:normalコマンドを使うものだ。

先に操作を見てみよう。下記の操作で、ドットコマンドを繰り返さなくても、すべての行末に「;」を追加することができる。

  1. A; 最初は前のやり方と同じ。最初の行を変更する。
  2. jVG 下の行に移動してビジュアルモードに変更しファイル末尾まで選択する。
  3. :'<,'>normal . で、ビジュアルモードで選択した各行に対して、ノーマルモード.コマンドを実行する。

3 ステップの :'<,'>は、直前に行ったビジュアルモードの選択を示す。Vim: を入力した時点で、:'<,'> までは補完してくれるので、実際にはnormal . を押すだけでよい。

ここでは、ドットコマンドを使ったが、ノーマルモードのコマンドは使える。他の例も紹介すると、:%normal A; というコマンドでも行末に「;」を追加できる。これは、%がファイル全体を示し、それらの範囲にノーマルモードA;、つまり行末に「;」を追加しろという命令になっている。

これらの例は、Ex コマンドの適用範囲が広いことの恩恵を表す例になっている。

Exコマンドも繰り返し!

.コマンドは直前のノーマルモードコマンドを繰り返すが、Exコマンドも直前の繰り返し操作が可能だ。直前のExコマンドを実行したい場合は、@:を使う。

Exコマンドでもタブ補完

シェルと同じように、<Tab>キーによるコマンドの自動補完ができる。

colder  colorscheme                                                                                                                            
:col  

上記の例は、:colまで入力したあと、<Tab>キーを押した例だ。続けて<Tab>キーを叩けば、プロンプトは colder→colorscheme→それからもともと入力していたcolに戻る。

履歴からコマンドを実行

コマンドラインで入力したコマンドをVimは覚えていてくれる。そして、それらをコマンドラインに呼び戻す方法を2つ用意している。1つは過去のコマンドラインをカーソルキーでスクロールする方法。もう一つはコマンドラインウィンドウで呼び出す方法だ。

まず:キーを押してコマンドラインモードに切り替える。何も入力しないまま、<Up>矢印キーを押すと、コマンドラインには直前に実行したExコマンドが表示される。<Down>キーを押すと履歴の表示を戻れる。

フィルターもついていて、:helpと途中まで入力してから、<Up>キーを押すと、:helpから始まるExコマンドの履歴を表示してくれる。

デフォルトでは、Vimは最後の20個のコマンドを記録する。vimrcに以下の行を追加することで、その最大値を変更することができる。

set history=200

シェルでコマンドを実行

Vimを終了しなくても、外部プログラムを簡単に呼び出せる。一番のメリットは、コマンドの標準入力にバッファの内容を送り込んだり、外部コマンドの標準出力をバッファに取り込んだりできることだ。

コマンドラインモードでは、ビックリマークをコマンドに前置することで、シェルでその外部プログラムを実行できる。

:!ls /
bin   cdrom  etc   initrd.img      lib    lost+found  mnt  proc  run   snap  swapfile   sys  usr  vmlinuz
boot  dev    home  initrd.img.old  lib64  media       opt  root  sbin  srv   swapfile2  tmp  var  vmlinuz.old

続けるにはENTERを押すかコマンドを入力してください

コマンドラインモードでは、%記号は現在編集中のファイル名の省略表現である。そのため、現在編集中のファイルに対して外部コマンドを実行することができる。たとえば、Rubyのコードを記述中なら、動作の確認をしたい場合、下記のようなコマンドで実行できる。

:!ruby %

ここまでは、シェルのコマンドを単に1回実行するだけだったが、インタラクティブなシェルを使うこともできる。

:shell

exitコマンドを実行することで、vimに戻ることができる。

また、同じことをする別の方法もある。Vimを使ってテキストを編集中にシェルに戻りたくなったら、Ctrl-zキーを押す。すると、Vimはバックグランドに移行して、シェルが表示される。シェルのjobsコマンドで現在何が停止中かわかる。

$ jobs
[1]+  停止                  vim

Vimに復帰したい場合は、fgコマンドで戻れる。

標準入力/出力をバッファに接続する

コマンドからの出力をVimで現在編集中のバッファに挿入したい場合、:read !{cmd} コマンドを使うことで行える。これは {cmd}; からの出力を現在のバッファに挿入するものだ。

例えば、ルートのディレクトリとファイルの一覧を挿入するには、下記のようなコマンドを実行すれば良い。

:read !ls /

逆に、バッファの内容を標準入力として{cmd}に渡したい場合は、:write !{cmd} を実行すれば良い。例えば、さっき挿入したルートのディレクトリとファイルの一覧 grep に渡して、libを検索すると、下記のような結果になる。

:write !grep lib                                                                                                                               
lib                                                                                                                                            
lib64

ただし、:writeはビックリマークの位置によってコマンドの意味がかわるので、注意が必要だ。

:write !sh
:write ! sh
:write! sh

最初の2つについては、バッファの各行を sh に渡して実行することを意味するが、最後の例は、バッファを sh という名前のファイルに上書き保存することを意味する。注意が必要だ。