Vimのバッファ一覧(:ls)の読み方と絞り込み方
:lsを実行すると、開いているバッファが記号付きで一覧表示されます。慣れないうちは記号の並びが暗号に見えますが、実際には桁の決まった5つの欄があるだけで、どの欄に何が出るかを知ると読む速度が一気に上がります。この記事では記号の意味に加えて、それぞれの記号を手元でどう作れば再現できるか、一覧が長くなったときの絞り込み方までを扱います。動作の確認にはVim 9.1.1752とNVIM v0.12.4を使い、どちらも設定を読まない状態(-u NONE)で起動しました。
目次(7項目)
バッファ一覧を表示する
:ls
:buffers
:files
3つは同じコマンドの別名で、出力に違いはありません。いちばん短い:lsを覚えれば足ります。
1 # "app.js" line 1
2 %a + "notes.txt" line 1
3 "config.yml" line 0
:ls の出力。%a が今表示中、# が直前、+ が未保存の変更ありを表す1行は4つの部分でできています。左からバッファ番号、5桁ぶんのフラグ欄、二重引用符で囲んだバッファ名、右端のline Nです。番号はVimを起動している間ずっと変わらず、途中のバッファを消しても詰め直されません。右端のline Nは最後にカーソルがあった行で、そのバッファに戻ったときにカーソルが置かれる位置でもあります。上の例でconfig.ymlだけline 0なのは、登録されただけで一度も開かれていないためです。
一度も開いていないバッファは、:baddで登録したときや、コマンドライン引数にファイルを並べて起動したときにできます。手元で:badd never.txtを実行してから:lsを打つと、その行だけline 0でフラグ欄も5桁すべてが空白でした。同じバッファのgetbufinfo()を見るとlnumもloadedも0で、表示上の空欄が内部の値とそのまま対応しています。
記号の意味
| 記号 | 意味 | この状態の作り方 |
|---|---|---|
u | リストから外れたバッファ | :setlocal nobuflisted、または:bdeleteした後 |
% | カレントウインドウにあるバッファ | いま編集しているバッファに必ず付く |
# | 代替バッファ(:e #やCtrl-^で戻る先) | 直前まで編集していたバッファに付く |
a | アクティブ(ロード済みで窓に表示中) | 分割した窓に出しておく |
h | 隠れバッファ(ロード済みだが非表示) | :set hiddenして別のファイルへ移る |
- | 変更不可('modifiable'オフ) | :setlocal nomodifiable |
= | 読み取り専用 | :view fileで開く |
+ | 未保存の変更がある | 1文字でも打って保存しない |
x | 読み込みエラーがあった | 巨大なファイルの読み込みをCtrl-cで中断する |
R | ジョブが動いている端末バッファ | :terminal sleep 300 |
F | ジョブが終わった端末バッファ | :terminal true |
フラグは5つの桁に分かれている
ヘルプは読み方の鍵になる一文を先頭に置いています。
Indicators (chars in the same column are mutually exclusive):
記号は自由に並んでいるのではなく、バッファ番号の右に必ず5桁ぶんの幅があり、桁ごとに出る候補が決まっています。同じ桁の記号は排他的なので、その桁を見れば状態が1文字で分かります。
- 1桁目:リストに載っているか(外れていれば
u) - 2桁目:いまいる場所
%か、直前にいた場所#か - 3桁目:メモリに読み込まれているか(表示中
a、非表示h、未読み込みは空白) - 4桁目:書き換えられるか(
=と-、端末バッファならRやF) - 5桁目:変更の有無
+と、読み込みエラーx
この見方の利点は、記号の組み合わせを覚え直さずに済むことです。%a +は2桁目が%、3桁目がa、5桁目が+という3つの独立した事実であって、%a+という1つの記号ではありません。逆に+とxは同じ5桁目なので、同時に並ぶことはありません。
主要な記号を1画面で出す
桁の話は、全部の記号が出た一覧を1回見るほうが早く入ります。空のディレクトリにa.txtからf.txtまで6つ置き、次の順で操作すると主要な記号が一度に並びます。
:set hidden
:badd b.txt " 登録だけして開かない
:view c.txt " 読み取り専用で開く
:e d.txt
:setlocal nomodifiable
:e e.txt
:setlocal nobuflisted
:badd f.txt
:e b.txt
:e a.txt " 直前が b.txt になるので # がそちらへ移る
最後にoで1行足し、保存せずに残します。この状態でVim 9.1.1752の:ls!は次の6行を返しました。
1 %a + "a.txt" line 2
2 #h "b.txt" line 1
3 h= "c.txt" line 1
4 h- "d.txt" line 1
5u h "e.txt" line 1
6 "f.txt" line 0
3行目と4行目の違いが4桁目の=と-です。:viewで開いたc.txtは'readonly'が立っているだけなので編集自体はでき、:wがE45で止まっても:w!なら書けます。'modifiable'を落としたd.txtはそもそも文字が打てません。5行目のuは:setlocal nobuflistedで外したもので、:lsを!なしで打つとこの行だけ消えます。
xだけは意図して出すのが難しい記号でした。読み込みの途中で失敗したバッファに付くもので、260MBのテキストファイルを開いた直後にCtrl-cで中断し、別のバッファへ移ってから:ls!を打つと2 #h=x "huge.txt" line 1になりました。途中までしか読めていないので4桁目に=も同時に付きます。xが見えたら中身は信用せず、:e!で読み直します。
%(今いる場所)、#(さっきいた場所)、+(未保存の変更あり)の3つ。絞り込んで表示する
バッファが十数個を超えると、番号順の一覧を目で追うのは無理があります。:lsは引数に記号を渡すと、その状態のバッファだけに絞れます。
1 h "config.lua"
2 #h + "main.py"
3 %a "notes.txt"
:ls +2 #h + "main.py"
:ls + " 変更のあるバッファだけ
:ls a " 表示中のバッファだけ
:ls h " 隠れバッファだけ
:ls % " カレントバッファだけ
:ls # " 代替バッファだけ
:ls u " リストから外れたバッファ「だけ」
:ls t " 最後に使った順に並べ替える
ここで引っかかりやすいのが:ls uです。「リストされていないバッファも含めて全部出す」と読みたくなりますが、実際は逆で、リストから外れたバッファ「だけ」を出します。ヘルプにもuは!を上書きすると書かれており、前節の6バッファで:ls uが返したのは5u h "e.txt"の1行だけでした。全部を見たいときは:ls!のほうを使います。
記号を複数並べるとANDになります。:ls h+は「隠れていて、なおかつ変更がある」バッファだけで、ORはありません。!との関係も覚えておくと迷いません。:ls hはリストに載っている隠れバッファしか出さないので手元では3行、:ls! hにすると5u h "e.txt"が加わって4行になりました。
知らない文字を渡してもエラーは出ず、:ls zは絞り込みが効かず、:lsと同じ結果になります。ここにVimとNeovimの差が1つあります。?(ジョブの無い端末バッファ)はVimでは正式なフラグなので:ls ?は該当が無ければ1行も返しませんが、Neovim 0.12.4のヘルプでは?が記号の一覧には載っているのに[flags]の一覧には無く、同じ:ls ?が5バッファ全部を並べました。件数が減らないときは綴りを疑ってください。
:ls tは右端のline Nを「最後に使ってからの経過時間」に差し替え、新しい順に並べ替えます。Neovimで打つと1 second ago、5 seconds ago、8 seconds agoと続き、一度も開いていないバッファだけがline 0のまま最後に残りました。番号は作った順でしかないので、直前まで触っていたファイルを探すならこちらが速いです。
ファイル名で絞り込む
状態ではなくファイル名やパスで絞るときは:filterと組み合わせます。これは直後のコマンドの出力行をパターンで間引く汎用の修飾子で、:lsでは表示されるバッファ名との部分一致で判定されます。
:filter /\.vim/ ls
:filter! /config/ ls
:filter /config/ ls +
config.lua、config.vim、main.pyを開いた状態で試すと、1行目はconfig.vimだけ、!を付けた2行目は一致しなかったmain.pyだけを返しました。3行目のように状態のフラグと併用もでき、これは「名前にconfigを含み、かつ変更がある」バッファを出します。同じ書き方は:oldfilesの絞り込みでも効きます。
スクリプトから同じ情報を取る
自作のバッファ一覧を書くときは、:lsの出力を解析せずにgetbufinfo()を呼びます。引数の辞書で絞り込めて、getbufinfo({'bufmodified': 1})なら変更のあるバッファだけを返します。返り値は辞書のリストで、bufnr、lnum、loaded、changedといったキーが並びます。
:echo map(getbufinfo({'buflisted': 1}), {_, v -> [v.bufnr, v.lnum, v.loaded, v.changed]})
3つのファイルを開き、後ろ2つだけを編集した状態でこれを打つと[[1, 1, 1, 0], [2, 1, 1, 1], [3, 1, 1, 1]]が返りました。4つ目の要素が:lsの+にそのまま対応します。Neovimならvim.fn.getbufinfo({buflisted = 1})でLuaからも同じ値が取れます。
番号と名前で切り替える
:b 3のほか、ノーマルモードで3に続けてCtrl-^を押しても同じところへ飛べます。手元で3Ctrl-^を押すと、3番のバッファに%aが移りました。
:b 3
:buffer 3
:b conf<Tab> " 名前の一部から補完する
:bnext / :bprev " 番号順に送る
番号を覚えていないときは名前の一部を渡します。:b confのように書くと部分一致で切り替わりますが、候補が2つ以上あるとE93: More than one match for confで止まります。config.luaとconfig.vimを同時に開いているとこうなるので、Tabで候補を回して確定させます。
2つのファイルを往復するだけならCtrl-^が最短で、一覧の#が付いたバッファへ飛ぶキーです。3つ以上を回すなら:bnextと:bprevもありますが、番号順に送るだけなので目的のファイルまで何回押すか読めません。バッファとウィンドウ・タブが何を指すのかはバッファ・ウィンドウ・タブの違いで整理しています。
記号の意味を調べたのは、:wqしたつもりのファイルが翌日そのまま残っていたのがきっかけでした。原因は単純で、分割した窓の片方だけ保存して、隠れていたもう1つのバッファに+が付いたまま気づいていなかっただけです。それ以来、席を立つ前に:ls +を打つ癖がついています。1行も出なければ保存漏れは無い、という確認が1コマンドで済むのが気に入っています。
使わないバッファを消す
一覧が長くなったら使わないバッファを閉じます。閉じ方は3段階あり、どこまで捨てるかが違います。:bunloadは中身をメモリから降ろすだけ、:bdeleteはリストからも外す、:bwipeoutはバッファごと消す、の順です。
:bunload 2 " 中身を降ろす(一覧には残る)
:bdelete 3 " 一覧から外す(マークやジャンプ位置は残る)
:bwipeout 4 " 完全に消す
:ls! " 一覧から外れたバッファも表示する
4つ開いてこの3つを順に実行し、:ls!を打つと違いが出ます。:bunloadした2番は3桁目が空白になっただけでline 1を保ったまま残り、:bdeleteした3番は3uになって:lsから消え、:bwipeoutした4番は:ls!にも出ませんでした。「:bdeleteでは完全に消えない」と言われるのはこの違いのことです。
普段は:bdeleteで足ります。:bwipeoutはマークや取り消し履歴まで捨てるので、同じファイルを開き直しても以前のカーソル位置が復元されなくなります。逆に、作り直したファイルにVimが古い状態を引きずっているときは:bwipeoutしてから開き直すと直ります。
まとめて閉じる
1つずつ番号を指定するのは手間なので、範囲やリストでまとめて渡せます。
:2,5bdelete " 2番から5番まで
:bdelete 3 5 7 " 番号を並べる
:%bdelete|edit# " 全部閉じて今のファイルだけ開き直す
最後の書き方は作業の区切りに使えますが、未保存のバッファがあると途中で止まります。3つ開いたうち1つを編集して試すとE89: No write since last change for buffer 3が出て処理が終わりました。手前の2つは消えたままで、後ろのedit#も実行されません。先に:ls +で未保存を確かめてから打つのが確実です。複数のバッファに同じ処理をまとめて流すやり方はwindo・bufdo・tabdoの使い分けで扱っています。
まとめ
:lsの出力は、バッファ番号と5桁のフラグ欄とline Nという決まった形をしています。桁ごとに見れば、組み合わせを暗記しなくても読めます。一覧が長くなったら:ls +や:ls tで絞り、名前で探すなら:filterを前に付けます。スクリプトから同じ判定をするならgetbufinfo()で、記号と返り値のキーは1対1に対応しています。