Vimの読み取り専用レジスタ("% "# ": ".)の実践的な使い道
編集中のファイル名をコマンドラインに打ち直したり、さっき実行した置換をもう一度組み立て直したりしていました。Vimはこの種の値を編集の進行に合わせて自分でレジスタへ書き込んでいて、"%にはいま開いているファイルの名前が、"#には直前に見ていたファイルの名前が、":には最後に実行したExコマンドが、".には最後に挿入したテキストが入っています。読み出し方さえ知っていれば、打ち直す場面がまとめて減ります。
目次(8項目)
| キー | これだけ覚える |
|---|---|
"% | カレントファイルの名前 |
"# | 直前に編集していたファイル(alternate file)の名前 |
": | 直近に実行したExコマンドラインの内容 |
". | 直近に挿入したテキスト |
4つのレジスタがいつ書き換わるか
この4つは、ふだん自分では値を入れず、Vimが編集の進行に合わせて書き込むレジスタです。読み出せるのはpやP、:put、そして挿入モードとコマンドラインのCtrl-rからで、名前付きレジスタのように自由に代入できるわけではありません。モードごとの読み出し方の違いはレジスタの貼り付け方にまとめてあります。書き換わるきっかけはレジスタごとに違います。
"%:バッファの名前が変わるたび。:editで別のファイルを開いたときのほか、:fileで名前を付け替えたときや:cdでカレントディレクトリを移したときにも変わります"#:別のファイルへ移った瞬間に、それまで見ていたファイルの名前が入ります"::コマンドラインを実行したとき。ただし1文字も打たずにマッピングだけで実行した場合は変わりません".:挿入モードを抜けたとき。cwやoのように挿入をともなう変更コマンドも対象です
中身が空のまま貼り付けようとすると、レジスタごとに別のエラーが返ります。起動直後のVim 9.1で試したところ、".pはE29: No inserted text yet、":pはE30: No previous command line、"#pはE23: No alternate file、名前の付いていないバッファでの"%pはE32: No file nameでした。番号が分かれているので、どれがまだ空なのかはエラーの側だけで判別できます。
"% はカレントディレクトリを基準に短くなる
"%に入るのは、コマンドラインで打った文字列そのままではありません。Vimはバッファ名をカレントディレクトリから見た表記に直して持ちます。vim /private/tmp/proj/sub/note.mdのように絶対パスで開いても、そのファイルがカレントディレクトリの下にあればsub/note.mdに短縮され、外にあるファイルだけが絶対パスのまま残りました。同じバッファを開いたまま:cd subを実行すると"%はnote.mdに変わります。Vim 9.1とNeovim 0.12.4で同じ結果でした。
:echo @% " sub/note.md(絶対パスで開いても短くなる)
:cd sub
:echo @% " note.md(基準が変わると表記も変わる)
加工したいときはexpand()に修飾子を渡します。%:pで絶対パス、%:hでディレクトリ部分、%:tでファイル名だけ、%:rで拡張子を除いた形、%:eで拡張子だけが取れます。修飾子は左から順に適用されるので、%:t:rは「ファイル名だけにしてから拡張子を落とす」という意味になります。挿入モードのCtrl-r %はレジスタの中身をそのまま入れるだけなので、修飾子を効かせたいときはCtrl-r =からexpressionレジスタを経由します。
:echo expand('%') " sub/note.md
:echo expand('%:h') " sub
:echo expand('%:t') " note.md
:echo expand('%:r') " sub/note
:echo expand('%:e') " md
:echo expand('%:t:r') " note
:echo expand('%:p') " /private/tmp/proj/sub/note.md
"# と Ctrl-^ は同じファイルを指している
"#が持つのはオルタネートファイル、つまり直前に見ていたファイルの名前です。コマンドラインからは:e #で戻れますが、往復するだけならCtrl-^(端末によってはCtrl-6)のほうが速く、バッファ番号を覚える必要もありません。c.txtを開いていて"#がa.txtだったところでCtrl-^を押すと、a.txtへ移ると同時に"#がc.txtへ入れ替わりました。押すたびに往復できるのは、この入れ替えがあるためです。
ただし"#は読み取り専用ではありません。:help registersが読み取り専用に分類しているのは":と".と"%の3つで、"#は別項目として書き込めると明記されています。実際に:let @# = 'a.txt'はそのまま通り、オルタネートファイルがa.txtに変わりました。既存のバッファに当たらない文字列を渡すとE94: No matching buffer for ...が返ります。バッファ番号でも受け付けるので、オルタネートを書き換えるプラグインの前後で退避しておくと元へ戻せます。
let altbuf = bufnr(@#) " いまのオルタネートを番号で退避
" ここでバッファを触るプラグインを呼ぶ
let @# = altbuf " 元に戻す
": はコマンドラインを打ったときだけ更新される
":はテキストとして貼り付けるより、@ :で実行するほうが出番が多くなります。1行目で:s/foo/BAR/を実行してからj @ :、続けてj @ @と押したところ、3行とも置換されました。@ @が直前の@を繰り返すので、2回目からはキーが1つ減ります。貼り付けたときに先頭のコロンは付いてこないため、":pで取り出した文字列はそのまま別のコマンドの材料にできます。
落とし穴が1つあります。コマンドラインを1文字も打たずに実行した場合、":は更新されません。:nnoremap <F5> :s/foo/MAP/<CR>を定義してF5を押すと置換自体は走りますが、直後に@:を表示させても中身は1つ前に手で打ったnohlsearchのままでした。:help quote_:にも、コマンドラインが丸ごとマッピングから来た場合は変わらないと書かれています。Vim 9.1とNeovim 0.12.4で同じ挙動でした。
". はテキストだけを持ち、ドットコマンドとは別物
.(ドットコマンド)と".は名前が似ていますが、持っているものが違います。apple pieの行でc wと押してMANGOと入力したあと、次の行の行頭で.を押すとbananaがMANGOに置き換わりました。3行目で".Pを押した場合はcherryが消えず、行頭にMANGOが挿入されるだけです。ドットは変更操作ごと繰り返し、".は入力した文字列だけを渡します。
MANGO pie
MANGO split
cherry tart
MANGO pie
MANGO split
MANGOcherry tart
挿入モードのCtrl-aも、このレジスタと同じテキストを入れます。Ctrl-@のほうは入れたあと挿入モードを抜けるところまでが1つのコマンドになっていて、手元でもCtrl-@を押した時点でノーマルモードへ戻りました。似た行を続けて足すならCtrl-a、1回だけ入れて終わるならCtrl-@という分かれ方になります。
最初のうちは"#の存在に気づかず、2つのファイルを行き来するのに毎回:lsでバッファ番号を確かめていました。Ctrl-^を指に覚えさせてからは、実装とテストの往復でファイル名を見ることがなくなっています。逆にいまだに手が止まるのが".で、直前に打った文字列だけが欲しい場面でつい.を押してしまい、残したかった単語まで巻き込んで書き換えてしまいます。押す前に、操作を繰り返したいのか文字列だけ欲しいのかを一拍考える癖をつけているところです。
書き込めるレジスタが混ざっている
「読み取り専用」という呼び名は、この周辺のレジスタ全部には当てはまりません。6つのレジスタに:letで文字列を代入して確かめたところ、代入がその場で失敗したのは"%と":と".だけでした。"/は直前の検索パターンをそのまま書き換えられ、"_は代入が通るのに読み出すと必ず空になります。書き換えたいのか守りたいのかで、同じ「読み取り専用」の枠でも扱いが分かれます。
| レジスタ | :let @x = 'ZZZ' の結果 |
|---|---|
"% | E354: Invalid register name: '%'(Vim・Neovimとも) |
": | E354: Invalid register name: ':'(Vim・Neovimとも) |
". | NeovimはE354。Vim 9.1はE15: Invalid expression |
"# | 通る。当たるバッファが無いとE94 |
"/ | 通る。検索パターンがZZZになる |
"_ | 通るが、読み出すと空 |
いまの中身を一覧で確かめるなら:registersが早く、引数にレジスタ名を並べれば必要なものだけ出せます。"_は名前を指定しても最初から出てきません。":の欄に出るのは:registersそのものではなく1つ前のコマンドで、表示している時点ではまだ自分自身が記録されていないためです。
:registers % # : . / _
Type Name Content
c ". hello
c ": e#
c "% main.js
c "# test.js
c "/ two
まとめ
この4つは覚えて使うものというより、手が止まった瞬間に思い出すものです。ファイル名を打とうとしたら"%、さっきのExコマンドをもう一度打とうとしたら@ :、直前に打った文字列をもう一度打とうとしたら".を試す価値があります。ほかのレジスタとの位置づけはレジスタ全体の一覧に、削除とヤンクの履歴が入る側は番号付きレジスタにまとめてあります。正式な定義は公式ヘルプ(:h registers)にあります。