今さらvi?今からvi!ファイル編集のバッチ化

0
828

エディタは何をお使いでしょうか?
弊社内では「Visual Studio Code」が人気ですかね。
個人的にはviを使ってますが。

今時の高機能GUIエディタを使っている人から見ると化石のような存在かもしれません。
しかし、便利だと思いますがね、vi。
まず、MacおよびLinuxであれば標準搭載されています。外れなく使えます。
エディタの操作において基本的にはマウスを必要としません。右手がキーボードとマウスを行ったり来たりする必要がありません。
キーのみで各種操作(入力、移動、コピペ等)が行えてしまうため、次第に操作が脊椎反射化してきます。vi上の各操作において自分がどのキーを押しているか意識していません。タッチタイピングにおいて、例えば「a」を入力しようとした際にキーボードにおける「a」のキーの配置をいちいち意識していませんよね?何らかの入力・編集操作を行う際に、操作を意識せず内容に集中できると言うことはかなりのメリットだと思いますが。

エディタの選択においては、基本的にはあくまで各自使いやすい物を使えば良いと思います。
ただ、Linuxでのファイル編集を行う場合はある程度viが使える必要が出てくるでしょう。
加えて昨今viの便利さを再認識した事例がありましたので、それを紹介したいと思います。
それが「ファイル編集のバッチ化」です。

なお、「vi」と表記していますがMacやLinux(CentOS)に搭載されているviは正確には「vim」(VIMproved)です。
viの改良版と言ったところですが、世の実態としてvi=vimだと思いますので、本記事でも黙って「vi」と表記したら「vim」を意味するものとします。
Mac、CentOS7のいずれにおいても「vi」「vim」のいずれの名称でもviを実行可能で、実体としてはvimが実行されるようになっています。

viに関する基本中の基本

viの操作に関して説明し出したらとても一つの記事では終わりません。
ここではあくまで基本中の基本的内容に関してのみ軽く触れておきます。

まずviはモーダル(モードがある)インタフェースであると言う点が最大の特徴です。
具体的には以下のようなモード(状態)が存在します。

ノーマルモードカーソル移動やテキスト削除、コピペなどの操作を行うモード
挿入モードテキストの入力を行うモード
コマンドラインモード編集結果の保存やviの終了、検索・置換などを行うモード
ビジュアルモード特定のテキストを選択するモード

上記それぞれのモードにおいて、実施すべき操作は全てキーに対応していて、メニュー選択やクリック、ドラッグ&ドロップなどマウスを使用するような操作が一切関与しません。
ノーマルモードにおいて上下左右へのカーソル移動はそれぞれ「k」「j」「h」「l」で行います。
ノーマルモードで「i」を押せば挿入モードに切り替わり、カーソル位置からテキスト入力できるようになります。例えば「ii」と「i」を2回連続した場合、最初の「i」押下で挿入モードに切り替わり、2つ目の「i」押下は「i」と言うテキスト入力と見做され、カーソル位置に「i」が入力されます。このように同じキーを押してもモードによって振る舞いが変わると言うことがviの大きな特徴です。
ノーマルモードで「:」を押すとコマンドラインモードに切り替わり、「w」押下で編集結果を保存し、「q」押下でファイル編集を終了します。なお、あえて各キーの操作の意味を説明すれば前述のような言い方になりますが、実際には「ファイルを保存して終了」と言う一連の操作を「:wq」と言うコマンド実行として認識していると言った方が実態に近いかと思います。
ノーマルモードで「V」を押すとカーソルがある行の背景色が変わり(私の環境では背景がグレーになりますが、この辺の見え方は環境ごとに違うかもしれません)、ビジュアルモードになります。この時、背景色が変わっている部分は選択された箇所であることを示しています。また、ノーマルモードと同様に「k」「j」でカーソルを上下に移動でき、カーソルの移動に合わせて前述の選択範囲が変更できます。選択した範囲に対しての操作としては、「d」で該当範囲を削除できたり、「y」で該当範囲をコピーし、カーソル移動後に「p」でカーソルの下にコピーした内容をペーストしたりできます。

上記はあくまで一例です。ここではモードの違いと、そこでの操作が全てキー操作で実現されていることを認識してもらうことが重要です。
viを使い始める方に取って、上記モードの違いと独特なキー操作に慣れなければならない点が難しく感じる点かと思います。
ただ、最初から全ての操作を覚える必要はなく、必要な操作から覚えていって、少しずつテリトリーを広げていけば良いと思います。
加えて、最初の方で触れましたが、慣れてしまえば操作は脊椎反射化してしまいます。逆に目的の操作とキーの対応が意識できなくなる程です。viの操作を説明しようと思ってもその場では思い出せず、実際にviを操作して初めて自分が各局面でどのキーを押しているかが再認識できたと言うこともしばしばです。
viの操作は頭ではなく体で覚えるものかと思います。

なお、viの操作に関してもっと知りたい場合はGoogle先生に聞いてください。

viのバージョン

今回の動作確認はCentOS7上で行いました。
しかし、残念なことにCentOS7にデフォルトでインストールされているvimでは本記事で紹介するバッチ処理が期待通りに動作しませんでした。
vimのバージョンを確認すると以下のようになっています。

$ vi --version
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Dec 15 2020 16:43:23)
Included patches: 1-207, 209-629
Modified by <bugzilla@redhat.com>
Compiled by <bugzilla@redhat.com>
Small version without GUI.  Features included (+) or not (-):
+acl             -farsi           -mouse_sgr       -tag_old_static
(以下省略)

どうも「Small version」であることが影響しているようです。
vimは同じバージョン番号でもビルド時の構成要素に含める機能によって「tiny」「small」「normal」「big」「huge」の5タイプが存在するようで、「tiny」は文字通りファイルサイズ的には最小である一方で最も機能制限があるもので、後ろに行く程にファイルサイズが大きく多機能になるようです。

CentOS用には「vim-enhanced」と言うパッケージが提供されており、viの機能をフルに使いたいのであればこちらを使用すると良いようです。
と言うことで、早速インストールしてみます。

yum install -y vim-enhanced

インストール結果を確認してみます。

$ vi --version
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Dec 15 2020 16:44:08)
Included patches: 1-207, 209-629
Modified by <bugzilla@redhat.com>
Compiled by <bugzilla@redhat.com>
Huge version without GUI.  Features included (+) or not (-):
+acl             +farsi           +mouse_netterm   +syntax
(以下省略)

バージョン番号は以前と同じ「7.4」ですが、「Huge version」になっています。
こちらであれば後述する内容が正しく実行できました。

ちなみに、Mac(Catalina)のviは以下でした。

% vi --version
VIM - Vi IMproved 8.1 (2018 May 18, compiled Jun  5 2020 21:30:37)
macOS version
Included patches: 1-503, 505-680, 682-2292
Compiled by root@apple.com
Normal version without GUI.  Features included (+) or not (-):
+acl               -farsi             -mouse_sysmouse    -tag_any_white
(以下省略)

バージョンは「8.1」の「Normal Version」です。
これでも後述する内容が正しく実行できました。

上記のように一言viと言ってもバージョンの違いで振る舞いに違いが生じる可能性があります。
可能であれば「Huge version」をインストールしておくと無難です。

編集内容

本記事を書くきっかけとなった実際の事例を紹介したいと思います。
以下、Laravelに関連する内容になりますが、要はファイル内に記述された内容から特定の箇所を検出し、そこに特定の内容を反映し、保存すると言う操作を行うことが目的です。
単にある行内の特定の文字列を別の文字列に置き換えると言うような話であればsedやawkのようなストリームエディタを使う方法が一般的ですが、今回の例では目的とする箇所の特定に複数行が関連する辺りがストリームエディタでは実現が難しい(私の知識の範囲では実現できない)内容かと思います。

Laravelには標準的なログ出力機能があって、ログの採取方法も様々に指定できます。
具体的には「config/logging.php」内で指定するのですが、デフォルトでもある程度の設定がされています。

<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

return [
    
    'default' => env('LOG_CHANNEL', 'stack'),

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['single'],
            'ignore_exceptions' => false,
        ],

        'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
        ],

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
        ],

        (中略)

        'emergency' => [
            'path' => storage_path('logs/laravel.log'),
        ],
    ],
];

部分的に割愛していますが、全体構成は上記のようになっています。

上記のようなファイルに対して独自の定義を追加したいとします。
例えば以下のような内容。

'debug' => [
    'driver' => 'daily',
    'path' => storage_path('logs/debug.log'),
    'level' => 'debug',
    'days' => 31,
    'tap' => [Wetch\MyLib\Logging\LogFormatter::class],
],

「driver」として「daily」を採用し、日別にファイルを分てログを採取します。
「path」として「storage_path(‘logs/debug.log’)」を指定することで「storage/logs」配下に「debug-YYYY-mm-dd.log」と言う名称のファイルでログが残されます。
「days」では何日分のファイルを残すかを指定でき、上記例では31日(最低1ヶ月)分のファイルが残るようになります。
「tap」ではログ出力時のフォーマットをカスタマイズしたい場合に、その書式設定に関するクラスを指定することができます。
※Laravelのログ設定に関しては、詳しくはGoogle先生に聞いてください。

上記のような設定を「channels」要素内の末尾に追加する方法を考えたいと思います。
実のところ「channels」要素内の先頭に追加するのであればもっと簡単なのですが、今後様々な編集パターンを考えた場合に一度の検索で場所を特定できないようなケースへの対応も必要になるかと思いますので、その点を考慮して若干込み入ったケースを想定しておきます。

具体的に人間が直接作業を行う場合はどのようなことを行うのかと言う視点で考えると以下のようになります。

  1. 当該ファイルが返す連想配列内の「channels」要素を見つける。
  2. 同要素の末尾を見つける。
  3. その直前に所定の設定内容を追記。
  4. 保存して終了。

上記をviの操作まで落とし込むと以下になります。
なお、追記する内容に関しては別ファイル(vendor/wetch/mylib/bat/config_logging/parts/channels.txt)として予め作成しておく形にします。

  1. コマンドラインモードで「/’channels’」を実行(ファイル内の’channels’なる文字列を検索)
  2. ノーマルモードで「$」(行末へ移動)、「%」(カーソルが括弧上にある場合、その対になる括弧に移動)、「k」(1行上に移動)を実行
  3. コマンドラインモードで「:r vendor/wetch/mylib/bat/config_logging/parts/channels.txt」(指定したパスのファイルを読み込む)を実行
  4. コマンドラインモードで「:wq」(保存して終了)を実行

細かい点ではもう少し厳密に(期待しない操作が発生しないように)考慮すべき点はありますが、大筋で言えば上記のような内容が実行したい操作になります。
加えて、上記操作をvi上で直接行うことは簡単ですが、Laravel環境を作成するごとに同じような設定を行うのであればバッチ的に実行できるようにしたくなる訳で、それが今回のチャレンジへとつながります。

操作手順定義と実行

viには事前にファイル化しておいた操作手順に準じて処理を実行する機能があります(今回初めて知りましたが)。
具体的には以下のような書式での実行になります。

vi -S <操作手順ファイル名> <編集対象ファイル名>

先に示した操作を行う場合、操作手順ファイルの内容は以下のようになります。
ほとんどviの画面上での操作をそのままファイルに記述するのみです。

/'channels'
:norm $%k
:r vendor/wetch/mylib/bat/config_logging/parts/channels.txt
:wq

操作手順はコマンドラインモード前提で記述する模様です。
よって、目的の手順で問題となるのはノーマルモードでの操作(カーソル移動)をどのように記述するかですが、上記にあるように「:norm」(あるいは「:normal」)指定でコマンドラインモードでノーマルモードの操作内容を記述できます。
※実は「Small version」では上記コマンドラインモードからノーマルモードの操作を行うことができませんでした。

操作手順ファイルに上記を、編集対象ファイルに「config/logging.php」を指定して先の書式でviを実行すると、期待した編集結果が「config/logging.php」に反映されることが確認できると思います。

これは使えるんじゃない?

上記のように、vi上で直接操作できることは概ねバッチ化できると言っても過言ではないでしょう(例外はあるかと思いますが)。
操作手順の定義方法も実際にvi上で操作してみた内容をほぼそのままファイルに記述するだけなので極めて簡単です。
実はviには「-W <ファイル名>」オプションで操作結果を指定したファイルに残す機能も存在します。
先ほど示した手順をvi上で実際に操作してみた結果として残された操作記録は以下になります。

/'channels'^M$%k:r vendor/wetch/mylib/bat/config_logging/parts/channels.txt^M:wq^M

改行が「^M」として記録されていますのでこれを本当の改行に変換し、ノーマルモードでの操作部分を少し加工すれば目的とする操作手順が作成できそうです。便利ですね。

なお、今回「操作手順」として記述した内容は「Vim script」と言う物の最も原始的な書き方と言えるのかもしれません。
「Vim script」は文字通りvimに対して様々な制御情報を与えるスクリプトで、変数や条件分岐、ループと言ったプログラム的要素も使用可能です。
何らかの操作をファイルに記述しておき、viの中で「:source <ファイル名>」と入力することで該当する操作が実行できます。
蛇足ながら、先に「-S <ファイル名>」オプションで操作手順ファイルの内容を実行できることには触れましたが、これは「-c “source <ファイル名>”」と同じ意味らしく、この「-c <コマンド>」オプションはvi起動直後に指定されたコマンドを実行すると言うものです。つまり、「-S <ファイル名>」と指定することはvi起動直後に「:source <ファイル名>」と実行するのと同じと言うことになります。

よって、「Vim script」を駆使すればもっと複雑な編集をviでバッチ的に行えるようにすることも可能かと思います。
ただ、あまり複雑な操作を考えるのであれば、専用プログラムを作成した場合とあまり違いがなくなってしまうような気がします。
前述したように、vi上で直接操作するような内容を簡単にバッチ化できると言うフットワークの軽さが本方式の最大のメリットであると思いますので、当面はこのやり方を基本とし、余程困ったことがあれば制御文に手を出すくらいが吉かと思っています(今のところ)。

いずれにしても定型的ファイル編集が、それがある程度複雑な手順であってもエディタ上で実施できれるものであれば手軽にバッチ化できてしまうと言うことは大きなメリットです。
今までバッチ化が面倒で似たような操作を毎回手作業で実施していた部分をどんどん改善し、作業効率を上げて行きたいと思います。