📜

Vimの:{range}!を通して任意の言語でテキストを処理する

2024/12/14に公開

この記事はVim Advent Calendar 2024の14日目の記事です。

Vimには:{range}!といって、
テキストを外部コマンドで処理する機能があります。
この機能を使うと、sed、awk、grepといった外部コマンドでテキストを編集することができ、
Vim単体ではやりづらい編集を、外部コマンドの力を借りて簡単にこなすことができます。

この図では外部コマンドにgrepを利用していますが、
この外部コマンドには、任意の言語で自作したコマンドを利用することもできます。

外部コマンドでの編集は、標準入出力をインターフェースに行われるため、
外部コマンドは自作がとてもしやすいものとなっています。
この記事では、そんなテキスト処理用のコマンドの作り方を紹介します。

要求仕様

まずVimから利用できる外部コマンドの要求仕様を抑えておきましょう。
といっても要求仕様は次の3点だけです。

  1. 標準入力から入力テキストを読み込む
  2. 標準出力に出力テキストを書き出す
  3. コマンドとして実行できる

次のRubyスクリプトを例に見ていきましょう。
このRubyスクリプトは次のように各行に行番号を付与するスクリプトになっています。[1]

numbering
#!/usr/bin/ruby

n = 1
$<.each_line do |l|
  puts "#{n}: #{l}"
  n = n + 1
end
入力テキスト
Alice
Bob
Carol
出力テキスト
1: Alice
2: Bob
3: Carol

標準入力から入力テキストを読み込む

Vimで指定した範囲のテキストは標準入力から送られてきます。
そのため、各言語で用意されている標準入力を読む関数やメソッドを利用して、入力テキストを受け取ります。

1行だけ読み込むのであれば、Rubyであればgets、Pythonであればinput()など、
1行ずつ読み込むのであれば、Rubyであれば$<.each_line、fileinput.input()などがあります。
処理する上で使いやすいものを使えば大丈夫です。
先ほどのRubyスクリプトでは次の箇所が該当します。ここで1行ずつ読み取っています。

numbering
$<.each_line do |l|

標準出力に出力テキストを書き出す

Vimに返すテキストは標準出力から返します。
そのため、各言語で用意されている標準出力に書き込む関数やメソッドを利用して、出力テキストを返します。

1行だけ書き込むのであれば、Rubyであればputs、Pythonであればprint()などがあります。
こちらも処理する上で使いやすいものを使えば大丈夫です。
先ほどのRubyスクリプトでは次の箇所が該当します。ここで1行ずつ書き込んでいます。

numbering
  puts "#{n}: #{l}"

コマンドとして実行できる

ここが少しハードルがあるところかもしれません。
Vimから実行する外部コマンドは、コマンドとして実行できる必要があります。
そのため、次の3点を満たしコマンドとして実行できるものにする必要があります。[2]

  1. 実行形式になっているかShebangの書かれたスクリプトになっている
  2. 実行権限が付与されている
  3. 環境変数PATHの通った場所にコマンドが配置されている

1はGoやRustのようにコンパイルをする言語であれば、コンパイルをすると実行形式のファイルができ、自動的に条件が満たされます。
RubyやPythonのようにコンパイルをしない言語であれば、スクリプトの先頭にShebangというものを付ける必要があります。
Shebangはスクリプトを何で実行するかを指定するもので、次のように#!の後にスクリプトを実行するコマンドのファイルのパスを書きます。

Shebang
#!スクリプトを実行するコマンドのパス

例) Rubyの場合
#!/usr/bin/ruby

例) Pythonの場合
#!/usr/bin/python3

2はコマンドのファイルにchmod等で実行権限を付与すると満たされます。

実行権限を付与するコマンド
chmod +x ここにコマンドのファイルのパス

例) 実行権限を付与
chmod +x numbering

3は環境変数PATHにあるディレクトリにコマンドのファイルを置くと満たされます。
環境変数PATHの値は次のコマンドで確認できるように、
次のようなコロン区切りでコマンドを置くディレクトリを区切ったものになっているのですが、
これらのディレクトリのいずれかにコマンドのファイルを置く必要があります。[3]

環境変数PATHの値を確認するコマンド
echo $PATH
出力結果
パス1:パス2:パス3

例) /bin、/sbin、/usr/bin、/usr/sbin、/usr/local/binのいずれかにコマンドのファイルを置く
/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin

これら3点を満たすと外部コマンドとして利用できるようになります。
ちょっと説明が多めで分かりづらかったかもしれません。
次に例を元に見ていきましょう。

作成例1

例を踏まえて見ていきましょう。
試しに、次のようなテキストを次のように左揃えで整列する外部コマンドを作ってみます。

入力テキスト
Name Power Toughness
Ghitu_Lavarunner  1 2
Viashino_Pyromancer 2 1
Goblin_Chainwhirler 3 3
出力テキスト
Name                Power Toughness
Ghitu_Lavarunner    1     2        
Viashino_Pyromancer 2     1        
Goblin_Chainwhirler 3     3        

言語はひとまずRubyにするとして、
コマンドのファイルの置き場所を決めておきましょう。
最終的な置き場所に置くのは最後でも大丈夫なのですが、最初は厄介なポイントなので先に置くことにします。

次のコマンドを実行してみてください。

環境変数PATHの値を確認するコマンド
echo $PATH

ディレクトリが次の形に似た形でコロン区切りで表示されていると思うのですが、
/home/ユーザー名/.local/binなど、/home/ユーザー名で始まっているディレクトリがあるでしょうか?あれば、そこに置いてしまうのがいいです。
無ければ/usr/local/bin、それも無ければ、あまり良くはありませんが/usr/bin/binに、コマンドのファイルを置きましょう。
コマンドのファイルの中身は現時点では空で大丈夫です。管理者権限を要求される場合はsudoを利用してファイルを作成します。
コマンドのファイル名は今回はalignとします。

出力結果例
/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/home/nil2/.local/bin

次に、コマンドのファイルの中身を書きます。
コマンドのファイルの中身が次のようなRubyスクリプトになるようにします。

align
#!/usr/bin/env ruby

table = []
$<.each_line do |l|
  table << l.split
end

widths = Hash.new(0)
table.each do |r|
  r.each_with_index do |c, i|
    widths[i] = c.size if c.size > widths[i]
  end
end

table.each do |r|
  puts r.map.with_index{|c, i| c.ljust(widths[i])}.join(" ")
end

最後にchmodを使って実行権限を付与します。
chmodの引数には実行権限付与の+x、そして対象のファイルとなるコマンドのファイルを指定します。

実行権限を付与するコマンド
chmod +x コマンドのファイルのパス

例) /usr/local/bin/align に置いた場合
chmod +x /usr/local/bin/align

これで準備完了です。
Vimを起動し、先ほどの元テキストを入力してみましょう。

入力テキスト
Name Power Toughness
Ghitu_Lavarunner  1 2
Viashino_Pyromancer 2 1
Goblin_Chainwhirler 3 3

Vで行単位のビジュアルモードに入って全行を選択し、
!alginで選択行を今回作ったコマンドで処理します。
そうすると、テキストが次のように整列されると思います。

出力テキスト
Name                Power Toughness
Ghitu_Lavarunner    1     2        
Viashino_Pyromancer 2     1        
Goblin_Chainwhirler 3     3        

このような流れでコマンドを作り、
Vimから利用できる外部コマンドを作ることができます。

作成例2

先ほどの例はRubyでコマンドを作成しましたが、
Shebangを変えれば、あるいは、実行形式のファイルを作成できる言語のコンパイラでコマンドを作成すれば、
その他の言語でもコマンドを作成することができます。

今度はシェルスクリプト(Bash)を利用して、
表形式のテキストをMarkdownのテーブルにするコマンドを作ってみましょう。

まず、依存が増えてしまいますが、
シェルスクリプト内で使うコマンドとしてDuckDBをインストールします。
DuckDBはCSVやJSONのファイルに対してSQLで集計をできるようにしてくれるツールですが、Markdownに変換するのにも使えます。

https://duckdb.org/

そして、mdtableという名前で、
次の内容のシェルスクリプトが書かれた、コマンドのファイルを作成します。
このシェルスクリプトでは、Rubyでスペース区切りのテキストをCSV形式にしたのち、DuckDBでCSV形式からMarkdown形式に変換しています。

mdtable
#!/bin/bash
set -eu

ruby -rcsv -e '$<.each_line{puts _1.split.to_csv}' | duckdb -markdown -s "select * from read_csv('/dev/stdin')"

こちらも同じ手順でコマンドを実行可能にし、
Vimを再度起動し、先ほどの元テキストを再び入力してみましょう。

入力テキスト
Name Power Toughness
Ghitu_Lavarunner  1 2
Viashino_Pyromancer 2 1
Goblin_Chainwhirler 3 3

Vで行単位のビジュアルモードに入って全行を選択し、
!mdtableで選択行を今回作ったコマンドで処理します。
そうすると、テキストが次のようにMarkdownの表形式になると思います。

出力テキスト
|        Name         | Power | Toughness |
|---------------------|------:|----------:|
| Ghitu_Lavarunner    | 1     | 2         |
| Viashino_Pyromancer | 2     | 1         |
| Goblin_Chainwhirler | 3     | 3         |

このように、コマンドは任意の言語で作ることができます。

まとめ

外部コマンドのインターフェースは標準入出力なので、
外部コマンドは基本的にどの言語でも作れるものになっています。

そのため、作るものに迷ったとき、
Vimで使う外部コマンドを作ってみるのも一つかもしれません。
作った外部コマンドはきっと編集に役立つので、
是非お試しください!

脚注
  1. with_indexを使うともう少しきれいに書けます。 ↩︎

  2. WindowsだとShebangを利用できないためスクリプトをバッチファイル等でラップする必要がある、PATHの値がセミコロン区切りである、など、ここで説明している内容について環境差異があります。ケアのしようは色々あるのですが、うまくまとめられる自信が無く割愛します。コマンドはGoなどのコンパイルする言語で作れば、バッチファイルでラップする必要が無く、PATHの通った場所にコマンドが配置されていれば実行できます。もしかするとそのほうが簡単かもしれません。 ↩︎

  3. 正確には置かなくとも、絶対パスや相対パスでコマンドのパスを指定して実行することで、外部コマンドとして利用することはできるのですが、置いておくとコマンド名だけを指定して実行できるようになります。 ↩︎

Discussion