😎

ちょっと面倒だなぁ~ってテキスト編集をなんとかする使い捨て十徳ナイフの作り方

2024/12/18に公開

はじめに

どうも亀茶です。
vimを使っている時に、このテキスト編集…大規模編集ってわけでもなく地味に面倒だけど、
vimでの上手い手法がパッと思いつかねぇなぁ…
って時ありませんか?

私は最近ありました。

そんな時ふと良い手法を思いついて、案外汎用性ありそうだったので紹介いたします。

手頃な編集

ここでいきなり手法をベタ張りしても良いのですが、
既にあるナイフやフォークの使い方を復習しておくのも重要だと思うので、
紹介しておきます。

ドットリピート

:help single-repeatで知られる.によって、簡易的な繰り返しを行うものです。
編集界隈でのナイフとも言えるでしょう。
ここで繰り返されるのは、「直前の変更」って事でして、
それとない概要としては以下のようになると思います。

  • オペレータ+モーションで行った変更(:help operator, :help motion.txt)
    • d awでの単語削除
    • ddでの行削除
  • ノーマルモード → インサートモード → ノーマルモードで行った変更
    • c iw 置換したい文字列 <ESC>での単語置換
  • ノーマルモードで、移動キー(h,j,k,l等)で移動できるまでの変更
  • 多分他にも色々ある…

これを駆使して、細やかな置換操作を行う事が多いと思います。

  1. /で検索をする
  2. ciw等で置換を行う
    • cgn等のgnで置換をすると、次の検索結果に移動しつつ置換も行えて便利です:help gn
  3. nで次に同じ置換をしたい箇所へ移動
  4. .で同じ置換を繰り返し

マクロ

:help complex-repeatで知られるマクロ機能です。
ドットマクロがナイフ・フォークのナイフなら、マクロはペティナイフくらいのイメージあります。

マクロ記録中にタイプした文字列を、人がタイプしたのと同じように実行する事ができます。
イメージとしては、ドットマクロよりも編集の幅が広い箇所を繰り返す感じです。
モードに関係無くマクロ記録中に入力した文字なら何でも実行してくれるので、
なにかと融通が効きやすいです。

マクロの使い方としては以下のような感じです。

  1. qaでマクロの記録を開始
    • ここでのaはレジスタ名で、何でも良いです(要help)
  2. 適当に編集を行う
    • 編集中に動作するプラグインも機能するはずなので便利
  3. qでマクロの記録を終了
  4. @aでマクロを実行
    • 一度マクロを実行すると@@で直前のマクロを実行できるので、@を連打できて便利

これもドットリピートと同じく、
検索等でカーソルを移動させて、編集操作の塊ことマクロを次々と実行していく事が多いのかなと思います。

コマンド

アーミーナイフくらいの感じがしますね。

多重繰り返し

:help multi-repeatで知られる手法ですね。
選択範囲の中で特定箇所に対して、コマンドを繰り返す事ができます。
この特定箇所ってのがイチオシポイントでして、コマンドによる行変更によって変化しないんですよね。
なので、編集の前後で行数が変化するような操作でも繰り返しを行う事ができちゃいます。

自分の主な用途としては、簡単な範囲のデバッグ用文章を削除する時とかですね。

  1. Vのビジュアルモードでデバッグ用分が含まれる行を範囲選択
  2. :'<,'>global/print/deleteでデバッグ用文章を削除
    • :'<,'>g/print/dでも良い
    • :deleteコマンドは対象行を削除するコマンド
      それぞれの行が適切に、ずれる事なく削除されている事が分かると思います。

:deleteコマンド以外にも様々なコマンドがありますが、
ここでは特にnormalコマンド(:help :normal)を紹介しておきます。
先ほどのマクロもこのコマンド経由で実行する事もできちゃいます。

  1. マクロを記録
  2. :global/hoge/normal @aでマクロを実行
    • この場合aレジスタにマクロを記録している

外部コマンドとの連携

:help complex-changeで知られる手法ですね。
紹介しておいてなんですが、個人的にはあんまし使ってないですね…
こういう場所で良く紹介されるのはjqを使用するイメージがあるます。

{ "hogd": "piyo","hoge":["gd","d"] }

{
  "hogd": "piyo",
  "hoge": [
    "gd",
    "d"
  ]
}

上記のような変更をする場合、

  1. Vでビジュアルモードで行選択
  2. :'<,'>!jqで外部コマンドを実行
    • :'<,'>!でビジュアルモードで選択した範囲を外部コマンドに渡す事ができる
    • jqはJSONを整形するコマンド

使い捨て十徳ナイフ

簡単に言ってしまえば、:help using-scriptsで紹介されている手法の一例って感じです。
テキスト編集自体をvimscriptで記述してしまうといった脳筋戦法ですね。
これによって、先程までの手法と組み合わせたりできちゃいます!!
そう…全ては筋肉へと帰着するのです…

vimscriptの構文は:help usr_41.txtをチェック!!

作り方

ひとえにvimscriptを使うといっても色々な方法があると思われますが、
今回はちょっと面倒だけど、使い易いであろう方を紹介します。

  1. :newで新規バッファを開く
    • :setfiletype vimでvimscriptのシンタックスハイライトを有効にしても良い
  2. function! Hoge() abort ...といった感じで関数を記述
  3. :sourceで読み込む
  4. 対象のバッファへ移動して、:call Hoge()で実行

といった感じです。
ここでポイントなのは、:source等でvimscriptを読み込む際に、スクリプトをファイル化しなくても良いという点です。
そう、まさしく†使い捨て†ですね。

使用例

例えば

ABCDEFGHIJKLMNOPQRSTUVWXYZ

から

- [ ] A
- [ ] B
...
- [ ] Z

のように変更したいとします。(こんな変換する事ないだろw的なのは置いておきます)

  1. :newで新規バッファを開く
    • :setfiletype vimでvimscriptのシンタックスハイライトを有効にしても良い
  2. function! Hoge() abort ...といった感じで関数を記述
    " !をつけて上書き定義
    "	複数回`:source`しても大丈夫になる
    "関数名の始めを大文字にしてグローバル関数にする
    "abortをつけてエラー時に即時中断
    "詳細は`:help define-function`を参照
    function! Hoge() abort
    	" 関数呼び出し時のカーソル行を取得
    	let lnum = line('.')
    	" カーソル行の文字列を分割してループ
    	for char in split(getline('.'), '\zs')
    		" append()で行を追加
    		call append(lnum, "- [ ] " . char)
    		let lnum += 1
    	endfor
    endfunction
    
  3. :source or :'<,'>sourceで読み込む
  4. 対象のバッファへ移動して、:call Hoge()で実行

何が起ってるのか

自分はプラグインを作ったりするまではあまり意識していなかったのですが、vimはスクリプト実行環境でもあるみたいで、いつでもコマンドを実行できるみたいなのですよね。
:set numberみたいなやつもコマンドでvim起動中に実行できますよね、アレです。
そして、このコマンドってのには関数定義等のコマンドも含まれているので、:source経由で独自の関数をvimが起動中に定義しちゃえというのが今回の手法です。

こういったvimのコマンドを使ったスクリプトを発展させると、普段使用しているプラグインになる感じですね。

便利な組み込み関数 :help function-list

スクリプトを書くにしても、式を書くにしても、
vimの組み込み関数をある程度知っておかないと損なので、
使用頻度が高そうなのをいくつか紹介しておきます。

カーソル系 :help cursor-functions

関数 機能 ヘルプ
line() 引数に応じた行番号を取得 :help line()
col() 引数に応じた列番号を取得 :help col()

バッファ系 :help text-functions

関数 機能 ヘルプ
setline() カレントバッファの指定行を置換 :help setline()
append() カレントバッファの指定行以降に行を追加 :help append()
getline() カレントバッファの指定行を取得 :help getline()

文字列操作系 :help string-functions

関数 機能 ヘルプ
split() 文字列を分割してリストに変換する :help split()
join() リストを連結して文字列変換する :help join()
match() 対象文字列をパターンマッチ :help match()
substitute() 対象文字列を置換
普段使用する:sコマンドと同じ様な感じ
:help substitute()
trim() 先頭・末尾の空白を削除したりする :help trim()
tolower() 小文字に変換 :help tolower()
toupper() 大文字に変換 :help toupper()

メソッド記法(:help method)も便利なので軽く紹介します。

split(getline('.'), '\zs')

みたいな関数呼び出しを以下のように関数の適用順を左から順に記述する事ができるって感じです。
関数の第一引数を->の前の式の評価結果として受け取れます。

getline('.')->split('\zs')

一行で式を書きたい時とか簡潔に書きたい時に便利そうですね。
ちなみにこの記法は自作関数にも適用されるので、重ねがけしたい関数を以下のように作成すると便利かもです。

" #tag ←を†で囲む
" 他にも良さそうな正規表現の置換方法ありそう
" ここでは#と空白文字以外の文字列を一塊として認識してる体
function! Hoge(str, num) abort
	return a:str
		\->substitute("\\ze#\[^#[:blank:]\]\\+", repeat("†", a:num), "")
		\->substitute("#\[^#[:blank:]\]\\+\\zs", repeat("†", a:num), "")
endfunction

" piyo†††#hoge†††
echo Hoge(Hoge("piyo#hoge", 2), 1)

" piyo†††#hoge†††
echo "piyo#hoge"->Hoge(2)->Hoge(1)

第一引数と返り値の型を同じにするのがミソです。

あとがき

気付けば紹介した手法のほとんどが:help repeatingに記載されているというね…
編集操作の塊を様々な粒度で保持しておき、適切な箇所で実行を繰り返すというのはvim操作における基本なのかもしれませんね。
dmacroってのもあるみたいだし、近々触ってみたいかも
https://zenn.dev/dog/articles/dmacrod_2024
たかが繰り返し、されど繰り返し、繰り返しひとつとっても奥が深いなぁ



P.S.
当初はExpressionレジスタ周りの知識を少し勘違いしていて、vim-jpで質問をして理解仕直しました。
ありがとうございました。かんしゃ感謝です。
そんなありがたいコミュニティには↓のリンクから詳しく知る事ができるみたいです。
https://vim-jp.org/docs/chat.html

GitHubで編集を提案
traP

Discussion