Vim エディターのスクリプトの作成: 第 5 回 イベント駆動型のスクリプト作成と自動化

Vim のオートコマンドでワークフローを自動化する

同じ操作を何度も繰り返す必要はないことをご存知ですか?Vim では包括的なイベント・モデルを構成することによって、特定の編集イベント (ファイルのロードやエディター・モードの切り替えなど) が発生するたびに時間を節約するためのスクリプトを実行することができます。連載の 5 回目となるこの記事では、Vim ではイベントがどのように動作するかを説明し、特に役立つイベント・タイプをいくつか説明した後、特定のスクリプトを特定のイベントに結び付ける方法を手ほどきします。最終的な結果は、ユーザーそれぞれの必要に応じて構成された一層自動化が進んだワークフローです。

Vim のイベント・モデル

Vim の編集用の関数は、イベント駆動型であるかのように振る舞います。パフォーマンス上の理由から、実際の実装はイベント駆動型よりも複雑になっていて、イベント処理の大部分が最適化されたり、あるいはイベント・ループ自体より何層も深い下位層で処理されたりしますが、それでも Vim エディターは一連の編集イベントに応答する単純な while ループであると見なすことができます。

Vim セッションの開始、ファイルのオープン、バッファーの編集、編集モードの変更、ウィンドウの切り替え、または周りのファイルシステムとのやりとりなどを行うたびに、ユーザーは事実上、イベントをキューに入れています。Vim はこれらのイベントを瞬時に受け取り、処理しているというわけです。

例えば Vim を起動し、demo.txt という名前のファイルを編集してから挿入モードに切り替え、何らかのテキストを入力してファイルを保存してから、終了するとします。この場合、Vim セッションはリスト 1 に記載する一連のイベントを受け取ります。

リスト 1. 単純な Vim 編集セッションでのイベント・シーケンス
> vim
    BufWinEnter     (create a default window)
    BufEnter        (create a default buffer)
    VimEnter        (start the Vim session):edit example.txt
    BufNew          (create a new buffer to contain demo.txt)
    BufAdd          (add that new buffer to the session’s buffer list)
    BufLeave        (exit the default buffer)
    BufWinLeave     (exit the default window)
    BufUnload       (remove the default buffer from the buffer list)
    BufDelete       (deallocate the default buffer)
    BufReadCmd      (read the contexts of demo.txt into the new buffer)
    BufEnter        (activate the new buffer)
    BufWinEnter     (activate the new buffer's window)i
    InsertEnter     (swap into Insert mode)
Hello
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)
    CursorMovedI    (insert a character)<ESC>
    InsertLeave     (swap back to Normal mode):wq
    BufWriteCmd     (save the buffer contents back to disk)
    BufWinLeave     (exit the buffer's window)
    BufUnload       (remove the buffer from the buffer list)
    VimLeavePre     (get ready to quit Vim)
    VimLeave        (quit Vim)

さらに興味深いことに、Vim が提供する「フック」を使用すれば、これらの編集イベントのいずれにしても、インターセプトすることができます。つまり、Vim の起動、ファイルのロード、挿入モードの終了、さらにはカーソルの移動といった指定のイベントが発生するたび、特定の Vim スクリプト・コマンドまたは関数が実行されるようにできるということです。そのため、エディターの至るところに、自動的な振る舞いを追加することができます。

Vim が通知する編集イベントは 78 種類あります。これらのイベントは大まかに、セッションの起動とクリーンアップ・イベント、ファイル読み取りイベント、ファイル書き込みイベント、バッファー変更イベント、オプション設定イベント、ウィンドウ関連のイベント、ユーザー操作イベント、非同期通知の 8 つに分類することができます。

Vim のすべての通知対象イベントを表示するには、Vim コマンドラインで help autocmd-events と入力します。各イベントの詳しい説明については、:help autocmd-events-abc と入力してください。

この記事では、イベントが Vim でどのように機能するかを説明した後、編集イベントおよび振る舞いを自動化するための一連のスクリプトを紹介します。


オートコマンドによるイベント処理

Vim がイベントをインターセプトするために提供しているメカニズムは、オートコマンド (autocommand) として知られています。それぞれのオートコマンドが、インターセプト対象のイベントのタイプ、そのタイプのイベントをインターセプトする編集ファイルの名前、イベントが検出されたときに実行するコマンドライン・モードのアクションを指定します。このすべての指定内容に対するキーワードは、autocmd です (au と省略されることもよくあります)。オートコマンドは通常、以下の構文となります。

autocmd  EventName  filename_pattern   :command

イベントの名前には、有効な 78 の Vim イベント名 (:help autocmd-events 表示されるイベント名) のうちの 1 つを指定します。ファイル名パターンの構文は、通常のシェル・パターンと同様です (ただし、まったく同じというわけではありません。詳細については :help autocmd-patterns を参照)。コマンドには、Vimスクリプト関数の呼び出しを含め、有効な Vim コマンドのいずれかを指定します。コマンドの先頭にあるコロンはオプションですが、コロンを含めることをお勧めします。こうすると、autocmd の (通常は複雑な) 引数リストのなかで該当するコマンドを見つけやすくなるためです。

一例として、恥ずかしげもなく以下のコードを .vimrc ファイルに追加して、FocusGained イベントを対象としたイベント・ハンドラーを指定するとします。

autocmd  FocusGained  *.txt   :echo 'Welcome back, ' . $USER . '! You look great!'

FocusGained イベントは、Vim ウィンドウにウィンドウ・システムの入力フォーカスが置かれるたびに、キューに入れられます。したがって、上記でファイル名のパターンとして指定された *.txt と一致するファイルの編集中に Vim セッションに戻るたびに、Vim はこの指定された echo コマンドを自動的に実行します。

1 つのイベントに対してセットアップできるハンドラーの数には制限がありません。複数のイベント・ハンドラーをセットアップすると、そのすべてが最初に指定された順に実行されます。例えば FocusGained イベントの場合、上記よりも遥かに有益な自動化として考えられるのは、編集セッションに戻るたびに、Vim に短時間、カーソル行を強調表示させることです (リスト 2 を参照)。

リスト 2. FocusGained イベントの有益な自動化
autocmd  FocusGained  *.txt   :set cursorline
autocmd  FocusGained  *.txt   :redraw
autocmd  FocusGained  *.txt   :sleep 1
autocmd  FocusGained  *.txt   :set nocursorline

上記の 4 つのオートコマンドによって、Vim は自動的にカーソルが置かれている行を強調表示し (set cursorline)、画面を再描画し (redraw)、1 秒間待機し (sleep 1)、最後に強調表示を無効にします (set nocursorline)。

このようにして、あらゆるコマンドの組み合わせを使用することができます。さらに、1 つの制御構造を複数のオートコマンドに分けることさえ可能です。例えば、以下のように「自動保存」メカニズムを制御するグローバル変数 (g:autosave_on_focus_change) をセットアップすることで、ユーザーが Vim から別のウィンドウに移ると (これにより、FocusLost イベントがキューに入れられます)、変更された .txt ファイルが自動的に保存されるようにすることができます。

リスト 3. 編集ウィンドウから離れると自動保存を行うためのオートコマンド
autocmd  FocusLost  *.txt   :    if &modified && g:autosave_on_focus_change
autocmd  FocusLost  *.txt   :    write
autocmd  FocusLost  *.txt   :    echo "Autosaved file while you were absent" 
autocmd  FocusLost  *.txt   :    endif

上記のような複数行にわたるオートコマンドでは、必須となるイベント・セレクターの指定 (つまり、FocusLost *.txt) を何度も繰り返されなければなりません。そのため、通常は保守するのが面倒になり、エラーの原因にもなりがちです。それよりも、制御構造やその他のコマンド・シーケンスは 1 つの独立した関数として分離し、1 つのオートコマンドでその関数を呼び出すほうが遥かに簡潔になり、エラーが起こる危険も少なくなります。以下はその一例です。

リスト 4. 複数行のオートコマンドを簡潔に処理する方法
function! Highlight_cursor ()
    set cursorline
    redraw
    sleep 1
    set nocursorline
endfunction
function! Autosave ()
   if &modified && g:autosave_on_focus_change
       write
       echo "Autosaved file while you were absent" 
   endif
endfunction

autocmd  FocusGained  *.txt   :call Highlight_cursor()
autocmd  FocusLost    *.txt   :call Autosave() 

全ファイルおよび単一ファイルのオートコマンド

これまでに記載したすべての例では、イベント処理の対象を *.txt というパターンと一致するファイルに限定していました。このことから、ファイルを取得するためのパターンを使えば、特定のオートコマンドを適用するファイルを自由自在に指定できることは明らかです。例えば、前に説明した、カーソルを強調表示する FocusGained オートコマンドをすべてのファイルに適用するには、ファイル名のフィルターとして、任意のファイルに一致する * というパターンを使用します。

" Cursor-highlight any file when context-switching ...
autocmd  FocusGained  *          :call Highlight_cursor()

あるいは以下のように、コマンドを 1 つのファイルに限定することもできます。

" Only cursor-highlight for my .vimrc ...
autocmd  FocusGained  ~/.vimrc   :call Highlight_cursor()

これはすなわち、編集されているファイルによって、同じイベントに対して異なる振る舞いを指定できることも示唆します。例えばユーザーがフォーカスを別の場所に移した時点で、テキスト・ファイルの場合には自動保存し、Perl または Python スクリプトの場合にはチェック・ポイント操作を行う一方、文書ファイルの場合には、現在のパラグラフのフォーマットを設定しなおすように指示するようにすることもできます (リスト 5 を参照)。

リスト 5. ユーザーのフォーカスが離れた時点での振る舞い
autocmd  FocusLost  *.txt   :call Autosave()
autocmd  FocusLost  *.p[ly] :call Checkpoint_sourcecode()
autocmd  FocusLost  *.doc  :call Reformat_current_para()

オートコマンド・グループ

オートコマンドには名前空間メカニズムが関連付けられています。この名前空間メカニズムを使用して、複数のオートコマンドをグループにまとめ、グループとして一括で操作することができます。

オートコマンド・グループを指定するには、augroup コマンドを使用します。このコマンドの一般的な構文は以下のとおりです。

augroup GROUPNAME
" autocommand specifications here ...
augroup END

グループの名前は、ホワイト・スペース以外の文字からなる任意の文字列にすることができます。ただし、「end」または「END」という文字列は、グループの終了を指定するために予約されているので使用することはできません。

オートコマンド・グループには、任意の数のオートコマンドを含めることができます。通常はイベントを基準にして、同じイベントに応答するコマンドを 1 つのグループにまとめることになります (リスト 6 を参照)。

リスト 6. FocusLost イベントに応答するオートコマンドのグループを定義する
augroup Defocus
    autocmd  FocusLost  *.txt   :call Autosave()
    autocmd  FocusLost  *.p[ly] :call Checkpoint_sourcecode()
    autocmd  FocusLost  *.doc   :call Reformat_current_para()
augroup END

またはファイルのタイプ別に、関連する一連のオートコマンドをグループにすることもあります。

リスト 7. テキスト・ファイルを処理するオートコマンドのグループを定義する
augroup TextEvents 
    autocmd  FocusGained  *.txt   :call Highlight_cursor()
    autocmd  FocusLost    *.txt   :call Autosave()
augroup END

オートコマンドを無効にする方法

特定のイベント・ハンドラーを削除するには、autocmd! コマンド (つまり、感嘆符を追加) を使用します。このコマンドの一般的な構文は以下のとおりです。

autocmd!  [group]  [EventName [filename_pattern]]

単一のイベント・ハンドラーを削除する場合には、3 つの引数をすべて指定します。例えば、.txt ファイルでの FocusLost イベントを対象としたイベント・ハンドラーを Unfocussed グループから削除するには、以下のコードを使用します。

autocmd!  Unfocussed  FocusLost  *.txt

特定のイベント名の代わりにアスタリスクを使用すると、指定したグループに該当し、しかも指定したパターンと一致するファイル名のファイルに含まれるあらゆる種類のイベントが削除されます。例えば Unfocussed グループから .txt ファイルのすべてのイベントを削除する場合は、以下のコードを使用します。

autocmd!  Unfocussed      *      *.txt

ファイル名パターンを省略すると、指定したイベント・タイプのすべてのイベント・ハンドラーが削除されます。したがって、Unfocussed グループからすべての FocusLost ハンドラーを削除する場合のコードは、以下のようになります。

autocmd!  Unfocussed  FocusLost

ファイル名パターンだけでなくイベント名も省略すると、指定したグループに含まれるすべてのイベント・ハンドラーが削除されます。したがって、以下のコードによって、Unfocussed グループに指定されたすべてのイベント処理が無効になります。

autocmd!  Unfocussed

最後に、グループ名も省略すると、オートコマンドの無効化は現在アクティブになっているグループに適用されることになります。通常このオプションを使用するのは、一連のオートコマンドをセットアップする前に、グループに設定されている内容をすべて「片付ける」場合です。例えば、Unfocussed グループは以下の方法で指定したほうが賢明です。

リスト 8. 新しいオートコマンドを追加する前に、確実にグループの中身を空にする
augroup Unfocussed
    autocmd!

    autocmd  FocusLost  *.txt   :call Autosave()
    autocmd  FocusLost  *.p[ly] :call Checkpoint_sourcecode()
    autocmd  FocusLost  *.doc   :call Reformat_current_para()
augroup END

autocmd! をすべてのグループの先頭に追加することが重要な理由は、オートコマンドはすべてのイベント・ハンドラーを静的に宣言するのではなく、動的にイベント・ハンドラーを作成するからです。同じ autocmd を 2 回実行すると、2 つのイベント・ハンドラーが作成され、それ以降は同じイベントとファイル名の組み合わせによって、その両方が別々に呼び出されます。各オートコマンド・グループを autocmd! で始めれば、グループ内にある既存のハンドラーが消去されるため、その後の autocmd 文は既存のハンドラーに追加するのではなく、置き換えることになります。したがって、イベント処理エンティティーを不必要に倍増させることなく、スクリプトを必要なだけ何度でも実行することができます (つまり、.vimrc ファイルに繰り返しデータを提供できるということです)。

実例の紹介

オートコマンドを適切に使用することで、編集作業は大幅に楽になります。ここからは、オートコマンドを使用して編集プロセスを合理化し、今までのフラストレーションを解消する方法をいくつか紹介します。

同時編集を管理する方法

Vim でとりわけ便利な機能の 1 つは、ユーザーが Vim の別のインスタンスで編集中のファイルを編集しようとすると、それを自動的に検出してくれる機能です。このような事態は、マルチウィンドウ環境の別の端末ですでにファイルの編集を進めている場合や、マルチユーザーに設定された共有ファイルで、別のユーザーが作業している場合に起こることがよくあります。現在編集中のファイルに対する編集操作を Vim が検出すると、ユーザーには以下のプロンプトが出されます。

Swap file ".filename.swp" already exists!
[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: _

どのような環境で作業しているかによっては、ユーザーはほとんど意識することなく、毎回オプションのいずれかに対応するキーを押しているはずです。例えば、共有ファイルで作業することがめったにないユーザーは、q を押してセッションを終了し、そのファイルの編集をすでに始めているターミナル・ウィンドウを探すことでしょう。一方、日常的に共有リソースを編集しているユーザーであれば、デフォルト・オプションを選択してファイルを読み取り専用で開くために、即座に <ENTER> を押すような習慣になっています。

けれどもオートコマンドを使用すれば、このメッセージをトリガーする SwapExists イベントへのレスポンスを自動化するだけで、メッセージを読み、理解し、応答する手間を完全に排除することができます。例えば、別の場所で編集中のファイルについては、まったく手を付けられないようにするには、以下のコードを .vimrc に追加します。

リスト 9. 同時編集を自動的に終了する
augroup NoSimultaneousEdits
    autocmd!
    autocmd  SwapExists  *  :let v:swapchoice = 'q'
augroup END

上記のコードはまず、オートコマンド・グループをセットアップし、既存のハンドラーをすべて削除します (autocmd! コマンドを使用)。次に、SwapExists イベントのハンドラーを任意のファイルに設定します (汎用ファイル・パターンの * を使用)。このハンドラーは単にレスポンス 'q' を特殊な v:swapchoice 変数に割り当てるだけにすぎません。Vim は「swapfile exists」というメッセージを表示する前に、この変数を参照します。変数が設定されている場合は、その値を自動レスポンスとして使用し、わざわざメッセージを表示することはしません。これで、ユーザーに swapfile メッセージが表示されることはなくなります。別の場所で編集中のファイルを編集しようとすると、Vim セッションが自動的に終了するだけの話です。

あるいは、すでに編集中のファイルを常に読み取り専用モードで開くようにしたいという場合でも、NoSimultaneousEdits グループを以下のように変更するだけで済みます。

リスト 10. 既存のファイルへの読み取り専用アクセスを自動化する
augroup NoSimultaneousEdits
    autocmd!
autocmd  SwapExists  *  :let v:swapchoice = 'o'
augroup END

さらに興味深いことに、対象ファイルのロケーションを基に、上記の 2 つの方法 (あるいは他の方法でも) のいずれかが選択されるようにすることもできます。例えば、ユーザー固有のサブディレクトリーにあるファイルは自動終了する一方、/dev/shared/ の下に置かれている共有ファイルは読み取り専用として開くようにするには、以下のコードを使用します。

リスト 11. コンテキストに応じたレスポンスを自動化する
augroup NoSimultaneousEdits
    autocmd!
    autocmd  SwapExists  ~/*            :let v:swapchoice = 'q'
    autocmd  SwapExists  /dev/shared/*  :let v:swapchoice = 'o'
augroup END

上記のコードの内容を説明すると、完全なファイル名がホーム・ディレクトリーで始まり、その後に任意のサブディレクトリーが続く (~/*) 場合には「終了」の振る舞いが事前選択され、完全なファイル名が共有ディレクトリー (/dev/shared/*) で始まる場合には「読み取り専用」の振る舞いが事前選択されるようになっています。

コードのフォーマットを一貫して自動で設定する方法

Vim には編集時の自動コード・レイアウトに対する優れたサポートが備わっています (:help indent.txt および :help filter を参照)。例えば、'autoindent' および 'smartindent' オプションを有効にすると、ユーザーがコード・ブロックを入力すると同時に、Vim が自動的にインデントを再設定します。また、'equalprg' オプションを設定して、言語固有のコード・リフォーマッターを標準の = コマンドに結び付けることもできます。

残念ながら、コードのフォーマットに関して最もよくありがちな状況の 1 つについては、Vim にはそれに対処するためのオプションも、コマンドもありません。その状況とは、他の誰かが作成した、とてつもなくひどいフォーマットのコードを読まざるを得ないというものです。具体的に言うと、Vim には、ユーザーが開く任意のコード・ファイルのフォーマットを自動的にサニタイジングするためのオプションが組み込まれていません。

それでも、問題はありません。なぜなら、その作業を代わりに処理するためのオートコマンドを、ごく簡単にセットアップできるからです。

例えば、以下のオートコマンド・グループを .vimrc に追加することで、C、Python、Perl、または XML ファイルを開くと、ファイルのタイプに応じた適切なコード・フォーマッターで自動的にそのファイルが処理されるようになります (リスト 12 を参照)。

リスト 12. オートコマンドでの美しいコード
augroup CodeFormatters
    autocmd!

    autocmd  BufReadPost,FileReadPost   *.py    :silent %!PythonTidy.py
    autocmd  BufReadPost,FileReadPost   *.p[lm] :silent %!perltidy -q
    autocmd  BufReadPost,FileReadPost   *.xml   :silent %!xmlpp –t –c –n
    autocmd  BufReadPost,FileReadPost   *.[ch]  :silent %!indent
augroup END

このグループに含まれるオートコマンドの構造はすべて同じであり、異なる点は、それぞれのオートコマンドが適用されるファイル名の拡張子と、各オートコマンドが呼び出すプリティー・プリンターだけです。

注意する点として、これらのオートコマンドは処理対象とする単一のイベントを指定していません。代わりに、それぞれのオートコマンドがイベントのリストを指定します。autocmd は、カンマ区切りリストを使ってイベント・タイプを指定することができます。リストと併せて指定した場合には、リストに含まれるイベントのいずれかに対してハンドラーが呼び出されることになります。

上記の場合、各ハンドラーに対してリストに記載されているイベントには、BufReadPostFileReadPost があります。前者は既存のファイルが新規バッファーにロードされるとキューに入れられ、後者は任意の :read コマンドが実行された直後にキューに入れられるイベントです。この 2 つのイベントがあれば、既存のファイルの内容をバッファーにロードする最も一般的な形をすべてカバーできるため、大抵この 2 つは一緒に指定されます。

イベント・リストに続いて各オートコマンドが指定しているのは、そのオートコマンドを適用するファイル接尾辞です (複数の接尾辞を指定しているオートコマンドもあります)。接尾辞には、Python の .pyPerl.pl および .pm、XML の .xml、C の .c および .h ファイルがあります。イベントの場合と同様に、これらのファイル名のパターンも単一のパターンではなく、はカンマ区切りリストとして指定できることに注意してください。例えば Perl ハンドラーは、以下のように作成することもできます。

autocmd  BufReadPost,FileReadPost   *.pl,*.pm   :silent %!perltidy -q

また、C ハンドラーを拡張して一般的な C++ ファイルのバリエーション (.C.cc.cxx など) も処理させるようにするには、以下のようにします。

autocmd  BufReadPost,FileReadPost   *.[chCH],*.cc,*.hh,*.[ch]xx  :silent %!indent

いつもの如く、各オートコマンドの最後のコンポーネントが、実行されるコマンドです。上記のオートコマンドでこれに該当するのは、いずれもグローバル・フィルター・コマンド (%!filter_program) です。このコマンドがファイルの内容全体を取り込み (%)、これを指定の外部プログラム (PythonTidy.pyperltidyxmlpp、または indent のいずれか) にパイプ (!) します。そして各プログラムの出力がバッファーに入れられて、元の内容を置き換えます。

通常、このようなフィルター・コマンドが使用されている場合には、Vim はコマンドが完了した後に以下のような通知を自動的に表示します。

42 lines filtered
Press ENTER or type command to continue_

この通知でユーザーを煩わすことのないように、各オートコマンドのアクションには :silent という接頭辞が付いています。この接頭辞は、ありきたりな情報メッセージを無効にする一方、エラー・メッセージについては表示を許可します。

コードを場合によって自動フォーマットする方法

Vim は、C コードを入力すると同時に自動的にフォーマットするための優れたサポートを備えていますが、C 以外の言語に対するサポートはそれほど充実していません。これは Vim だけの責任ではなく、一部の言語 (まさに Perl がその一例です) は、オンザフライで正しくフォーマットするのが至難の業だからです。

使用している言語のソース・コードの自動フォーマットに対して Vim が十分なサポートを提供しないとしても、その代わりとして簡単に、エディターに外部ユーティリティーを呼び出させて自動フォーマットすることができます。

その場合の最も簡単な方法は、InsertLeave イベントを使用することです。このイベントは、Insert モードを終了するたびに (通常は、<ESC> を押した直後に) キューに入れられます。そこで、以下のようにすれば、コードの追加が終了するたびにコードをフォーマットしなおすハンドラーを簡単にセットアップすることができます。

リスト 13. 編集が終了するたびに PerlTidy を呼び出す
function! TidyAndResetCursor ()
    let cursor_pos = getpos('.')
    %!perltidy -q
    call setpos('.', cursor_pos)
endfunction

augroup PerlTidy
    autocmd!
    autocmd InsertLeave *.p[lm]  :call TidyAndResetCursor()
augroup END

TidyAndResetCursor() 関数はまず、組み込み getpos() によって返されたカーソル情報を cursor_pos 変数に保管することによって、現在のカーソル位置のレコードを作成します。次にファイル全体で外部 perltidy ユーティリティーを実行し (%!perltidy -q)、保存されたカーソル情報を組み込み setpos() 関数に渡すことによって、最終的にカーソルを元の位置に戻します。

後は PerlTidy グループ内に、ユーザーが Perl ファイル内で Insert モードを終了するたびに TidyAndResetCursor() を呼び出す 1 つのオートコマンドをセットアップすればよいだけです。

これと同じコード・パターンを適用すれば、テキストを挿入するたびに、どんな適切なアクションでも実行することができます。例えば、信頼性にかなり欠けるシステムで作業していて、何かが上手くいかなくなったときにファイルを回復する能力を最大限発揮したいとたら (:help usr_11.txt を参照)、以下のようにして、Insert モードを終了するたびに Vim がスワップ・ファイルを更新するようにします。

augroup UpdateSwap
    autocmd!
autocmd  InsertLeave  *  :preserve
augroup END

ファイルにタイムスタンプを付ける方法

有用なイベントにはその他、BufWritePreFileWritePre、および FileAppendPre もあります。これらの「Pre」イベントは、(:write:update:saveas などのコマンドの実行結果として) Vim セッションがバッファーをディスクに書き込む直前にキューに入れられます。BufWritePre イベントはバッファー全体が書き込まれる直前に発生し、FileWritePre はバッファーの一部が書き込まれる直前に発生します (つまり、:1,10write といった具合に、書き込む行の範囲を指定した場合です)。FileAppendPre は、以下のように、置換ではなく追加のための :write コマンドが使用される直前に発生します。

:write >> logfile.log).

この 3 つのタイプのイベントすべてに対し、Vim は、'[ および '] という特殊な行番号別名を、書き込み対象の行範囲に設定します。これらの別名を以降のすべてのコマンドの範囲指定子で使用することで、オートコマンドのアクションを関連する行にだけ適用することができます。

一般に、この場合のイベント・ハンドラーとしてセットアップするのは、書き込み前の 3 つのタイプのイベントすべてをカバーする単一のハンドラーです。例えば、ファイルがディスクに書き込まれる (または追加される) たびに Vim に内部タイムスタンプを自動更新させるようにするには、リスト 14 のコードを作成します。

リスト 14. ファイルが保存されるたびに自動的に内部タイムスタンプを更新する
function! UpdateTimestamp ()
    '[,']s/^This file last updated: \zs.*/\= strftime("%c") /
endfunction

augroup TimeStamping
    autocmd!

    autocmd BufWritePre,FileWritePre,FileAppendPre  *  :call UpdateTimestamp()
augroup END

UpdateTimestamp() 関数は、置換範囲を具体的に '[ から '] の間に制限して、書き込み対象の各行で置換を実行します ('[,']s/.../.../)。置換対象として検索されるのは、「This file last updated:」で始まり、その後に任意のテキスト (.*) が続く行です。.* の前にある \zs により、置換操作はコロン以降だけが一致しているかのように振る舞うため、実際のタイムスタンプだけが置換されるというわけです。

タイムスタンプを更新するために、置換操作は置換テキストで特殊な \= エスケープ・シーケンスを使用します。このエスケープ・シーケンスは Vim に対し、置換テキストを Vim スクリプトの式として扱い、これを評価して実際の置換ストリングを取得するように指示します。上記の例の場合、この表現は組み込み関数 strftime() の呼び出しです。この呼び出しにより、「Fri Oct 23 14:51:01 2009」という形式の標準タイムスタンプ・ストリングが返されます。返されたこのストリングが、置換コマンドによってタイムスタンプの行に書き込まれます。

残る作業は、任意のファイル (*) での 3 つすべてのイベント・タイプ (BufWritePre,FileWritePre,FileAppendPre) に対するイベント・ハンドラー (autocmd) をセットアップし、そのイベント・ハンドラーに、該当するタイムスタンプ関数を呼び出させるだけです (:call UpdateTimestamp())。この作業が完了すると、ファイルが書き込まれるたびに、保存される行のタイムスタンプが現在時刻に更新されるようになります。

Vim には上記の 3 つのイベントの他にも、書き込み操作の振る舞いを変更するために使える 2 つのイベント・セットがあることに注意してください。その 1 つは、書き込み後に行わなければならないアクションを自動化するために使用できる BufWritePostFileWritePostFileAppendPost のセットです。そしてもう 1 つのセット、BufWriteCmdFileWriteCmd、および FileAppendCmd は、標準の書き込み操作の振る舞いを独自のスクリプトと完全に置き換えるために使用することができます (ただし、重要な注意事項があるので、使用する前に :help Cmd-event を参照してください)。

表によるタイムスタンプ

当然のことながら、さまざまなタイムスタンプの慣例が使用されているファイルを処理するために、さらに手の込んだメカニズムを作成することも可能です。例えば、各種のタイムスタンプ・シグニチャーとそれぞれに対応する代替シグニチャーを Vim の辞書 (この連載の前の記事を参照) に指定した上で、各ペアをループ処理してタイムスタンプをどのように更新するかを判断することができます。その方法を、リスト 15 に記載します。

リスト 15. 表による自動タイムスタンプ
let s:timestamps = {
\  'This file last updated: \zs.*'             :  'strftime("%c")',
\  'Last modification: \zs.*'                  :  'strftime("%Y%m%d.%H%M%S")',
\  'Copyright (c) .\{-}, \d\d\d\d-\zs\d\d\d\d' :  'strftime("%Y")',
\}

function! UpdateTimestamp ()
    for [signature, replacement] in items(s:timestamps)
        silent! execute "'[,']s/" . signature . '/\= ' . replacement . '/'
    endfor
endfunction

上記の for ループは、s:timestamps 辞書に指定されたタイムスタンプごとのシグニチャー/代替シグニチャーのペアを繰り返し処理します。以下が、これに該当する部分です。

for [signature, replacement] in items(s:timestamps)

次に、対応する置換コマンドが含まれるストリングを生成します。以下の置換コマンドの構造は前の例での構造と同じですが、今回はシグニチャー/代替シグニチャーのペアをストリングの間に挿入して作成されています。

"'[,']s/" . signature . '/\= ' . replacement . '/'

そして最後に、生成したコマンドを silent! を使用して実行します。

silent! execute "'[,']s/" . signature . '/\= ' . replacement . '/'

重要な点は、置換が一致しなかった場合に「Pattern not found」という煩わしいエラー・メッセージが表示されないようにするため、silent! を使用していることです。

s:timestamps の最後のエントリーは、特に実用的な例なので注目してください。このエントリーは、著作権表示が含まれるファイルへの書き込みが行われると常に、そこに組み込まれた著作権表示の年の範囲を自動的に更新します。

ファイル名によるタイムスタンプ

考えられるすべてのタイムスタンプのフォーマットを 1 つの表にリストアップする代わりに、UpdateTimestamp() 関数をパラメーター化し、各種のファイル・タイプに対応した一連の autocmd を作成するという方法もあります。それが、リスト 16 のコードです。

リスト 16. コンテキストに応じたファイル・タイプごとのタイムスタンプ

リスティングを見るにはここをクリック

リスト 16. コンテキストに応じたファイル・タイプごとのタイムスタンプ

function! UpdateTimestamp (signature, replacement)silent! execute "'[,']s/" . a:signature . '/\= ' . a:replacement . '/'
endfunction

augroup Timestamping
    autocmd!

" C header files use one timestamp format ... autocmd BufWritePre,FileWritePre,FileAppendPre  *.h
        \ :call UpdateTimestamp('This file last updated: \zs.*', 'strftime("%c")')" C code files use another ...autocmd BufWritePre,FileWritePre,FileAppendPre  *.c
        \ :call UpdateTimestamp('Last update: \zs.*', 'strftime("%Y%m%d.%H%M%S")')
augroup END

このバージョンでは、シグニチャーと代替コンポーネントが明示的に UpdateTimestamp() 関数に渡されます。すると、この関数はそれに対応する単一の置換コマンドを含めたストリングを生成して、実行します。Timestamping グループ内には、必要なファイル・タイプごとに個別のオートコマンドをセットアップし、それぞれに該当するタイムスタンプ・シグニチャーと代替テキストを渡します。

魔法のようにディレクトリーを作成する方法

オートコマンドは、編集を始める前からでも役に立ちます。例えば、新規ファイルの編集を開始すると、以下のようなメッセージが表示されることがあります。

"dir/subdir/filename" [New DIRECTORY]

このメッセージは、指定したファイル (上記の場合は filename) が存在しないこと、そして指定したファイルが配置されているはずのディレクトリー (上記では dir/subdir) も存在しないことを意味します。

Vim は、この警告を無視し (多くのユーザーは、これが警告であることにさえ気付いていません)、ファイルの編集を続けることを快く許可しますが、ファイルを保存しようとすると以下の不親切なエラー・メッセージが突き付けられます。

"dir/subdir/filename" E212: Can't open file for writing.

この場合、作業内容を保存するには、その存在しないディレクトリーを明示的に作成し、そこにファイルを書き込む必要があります。それには、Vim から以下のコマンドを実行します。

:write
"dir/subdir/filename" E212: Can't open file for writing.
:call mkdir(expand("%:h"),"p")
:write

上記の組み込み expand() 関数の呼び出しは "%:h" に適用されます。ここで、% は現行のファイル・パス (上記の場合は dir/subdir/filename) で、:h はそのパスの「先頭」だけを取ります。つまり、ファイル名を削除して、目的のディレクトリーのパス (dir/subdir) だけを残すということです。次に Vim の組み込み mkdir() の呼び出しはこのパスを取り、(2 番目の引数 "p" で指定されているように) パスに至るまでのすべての中間ディレクトリーを作成します。

ただし現実的には、ほとんどの Vim ユーザーは単純にシェルにエスケープして必要なディレクトリーを作成するほうを好むものです。以下はその一例です。

:write
"dir/subdir/filename" E212: Can't open file for writing.
:! mkdir -p dir/subdir/
:write

いずれの方法にしても面倒なことには変わりありません。存在しないディレクトリーを結局は作成しなければならないのだとしたら、Vim にディレクトリーが存在しないことを通知させる代わりに、初めからディレクトリーを作成させればよいだけの話です。こうすれば、あの曖昧な [New DIRECTORY] のヒントが表示されることも、同じくミステリアスな E212 エラーによってワークフローが途中で中断されることもなくなります。

存在しないディレクトリーを Vim に事前に作成させるには、存在しないファイルの編集を開始すると常にキューに入れられる BufNewFile イベントにハンドラーを接続するという方法を使えます。この方法で存在しないディレクトリーを事前に作成するには、リスト 17 のコードを .vimrc ファイルに追加することになります。

リスト 17. 存在しないディレクトリーを無条件で自動作成する
augroup AutoMkdir
    autocmd!
    autocmd  BufNewFile  *  :call EnsureDirExists()
augroup END
function! EnsureDirExists ()
    let required_dir = expand("%:h")
    if !isdirectory(required_dir)
        call mkdir(required_dir, 'p')
    endif
endfunction

AutoMkdir グループは、あらゆる種類のファイルでの BufNewFile イベントを対象に 1 つのオートコマンドをセットアップし、新規ファイルの編集時には常に EnsureDirExists() 関数を呼び出します。EnsureDirExists() はまず、expand("%:h") によって現行ファイル・パスの「先頭」を展開し、要求されているディレクトリーを判断します。次に、組み込み関数 isdirectory() を使用してそのディレクトリーが存在するかどうかをチェックし、ディレクトリーが存在しなければ、Vim の組み込み関数 mkdir() によって、要求されているディレクトリーを作成します。

mkdir() の呼び出しが何らかの理由で要求されているディレクトリーを作成できない場合には、多少具体的な情報を提供するエラー・メッセージが生成されることに注意してください。

E739: Cannot create directory: dir/subdir

慎重にディレクトリーを作成する方法

上記のソリューションの唯一の問題は、存在しないサブディレクトリーを自動作成すること自体がまさにエラーの原因になる場合があることです。例えば、以下のリクエストを行ったとします。

> vim /share/sites/corporate/root/.htaccess

このリクエストの意図は、/share/corporate/website/root/ という既存のサブディレクトリーに新しいアクセス制御ファイルを作成することでしたが、残念ながら入力したパスが誤っています。そのため、このリクエストによって実際に何が行われるかというと、今までは存在していなかったサブディレクトリー /share/website/corporate/root/ が作成され、そこに新規アクセス制御ファイルが作成されます。これは、まったく警告なしに自動的に行われるため、ユーザーはパスを誤って入力したことに気付かない場合さえあります。少なくとも、誤って適用されたアクセス制御がオンラインに大災害をもたらすまではの話ですが。

このようなエラーを防ぐ対策としては、存在しないディレクトリーの自動作成についての Vim による支援を減らすことが考えられます。リスト 18 に記載する、さらに手の込んだ EnsureDirExists() のバージョンは、ディレクトリーが存在しないことを検出することはしても、ユーザーにそのディレクトリーの処理方法を尋ねるようになっています。ここでのオートコマンドのセットアップはリスト 17 とまったく同じで、EnsureDirExists() 関数が変更されているだけです。

リスト 18. 存在しないディレクトリーを条件付きで自動作成する
augroup AutoMkdir
    autocmd!
    autocmd  BufNewFile  *  :call EnsureDirExists()
augroup END
function! EnsureDirExists ()
    let required_dir = expand("%:h")
    if !isdirectory(required_dir)
call AskQuit("Directory '" . required_dir . "' doesn't exist.", "&Create it?")

        try
            call mkdir( required_dir, 'p' )
        catch
call AskQuit("Can't create '" . required_dir . "'", "&Continue anyway?")
        endtry
    endif
endfunction

function! AskQuit (msg, proposed_action)
    if confirm(a:msg, "&Quit?\n" . a:proposed_action) == 1
        exit
    endif
endfunction

このバージョンの EnsureDirExists() 関数は、必要なディレクトリーを探し、それが存在するかどうかを検出します。ここまでは前のバージョンとまったく同じですが、ディレクトリーが存在しない場合、今度は EnsureDirExists() がヘルパー関数 AskQuit() を呼び出します。このヘルパー関数は組み込み confirm() 関数を使用して、ユーザーにセッションを終了するか、それともディレクトリーを自動作成するかを尋ねます。最初の選択肢として示される「Quit?」は、ただ単に <ENTER> を押した場合のデフォルトでもあります。

「Quit?」を選択すると、ヘルパー関数が直ちに Vim セッションを終了します。そうでない場合は、ヘルパー関数は単純にリターンするだけです。その場合、EnsureDirExists() は実行を続行して mkdir() の呼び出しを試行します。

ただし、ここで注意しなければならないのは、mkdir() の呼び出しは、このバージョンでは try...endtry 構成体のなかに含まれていることです。お察しのとおり、これは例外ハンドラーであり、mkdir() が要求されたディレクトリーを作成できないとスローされる E739 エラーをキャッチします。

エラーがスローされると、catch ブロックがエラーをインターセプトして AskQuit() をもう一度呼び出し、ユーザーにディレクトリーを作成できなかったことを通知して、続行するかどうかを尋ねます。Vim の多数の例外処理メカニズムについての詳細は、:help exception-handling を参照してください。

この 2 番目の EnsureDirExists() バージョンの総体的な効果は、存在しないディレクトリーを明らかにする一方、そのディレクトリーを作成するには、ユーザーが明示的に要求 (プロンプトが出されたときに「c」の 1 文字を入力) する必要があるということです。ディレクトリーを作成できない場合には、ユーザーには再び警告メッセージが表示され、それでもセッションを続行することを選択できるようになっています (この場合も、プロンプトに対して「c」の 1 文字を入力)。この仕組みは、誤って行った編集からも簡単にエスケープできるようにします (いずれのプロンプトでも、<ENTER> を押してデフォルトの「Quit?」オプションを選択すればよいだけです)。

もちろん、続行することをデフォルトにすることもできます。その場合には、AskQuit() の最初の行を以下のように変更すればよいだけの話です。

if confirm(a:msg, a:proposed_action . "\n&Quit?") == 2

上記のように変更すると、提案されたアクションが最初の選択肢となり、したがってこれがデフォルトの振る舞いとなります。「Quit?」は 2 番目の選択肢となっているため、レスポンスは値 2 と比較されることに注意してください。

これからの展望

オートコマンドは、ユーザー自身が繰り返し行う操作を自動化することによって、ユーザーの労力を大幅に軽減します。オートコマンドを使い始めるのに有効な方法として、編集するときを思い返して、Vim のイベント処理メカニズムを使って適切に自動化できそうな、繰り返し使用している操作のパターンを考えてください。これらのパターンをオートコマンドのスクリプトにするには、前もって追加の作業を行わなければなりませんが、その作業は、自動化されたアクションによって日々報われるはずです。日常的に行う操作を自動化すると、時間と労力の節約、エラーの回避、ワークフローの円滑化、ささいなストレス要因の解消になり、それによって生産性が向上することになります。

オートコマンドは、初めは単純なたった 1 行の自動化でしかないかもしれませんが、そのうちすぐに、Vim により多くの単調な作業を任せるためのより良い方法を思い付き、オートコマンドを再設計して工夫を凝らすようになるはずです。このようにして、イベント・ハンドラーは次第に賢くなり、安全性が向上し、ユーザーそれぞれが好むやり方にぴったり順応していくことになります。

一方、このような Vim スクリプトが複雑さを増していくにつれ、スクリプトを管理するための優れたツールも必要になってきます。毎回、賢い新たなキー・マッピングやオートコマンドを考え出すたびに、.vimrc に 10 行や 20 行を追加していくと、最終的には数千行にも膨らんだ構成ファイルとなり、まったく保守しきれなくなるからです。

そこで、この連載の次回の記事で取り上げるのが、.vimrc からその一部を取り除いて個別のモジュールに分離できるようにする、Vim の単純なプラグイン・アーキテクチャーです。このプラグイン・システムがどのように機能するかを調べるために、XML を扱う際の恐ろしい作業を改善するスタンドアロンのモジュールを開発します。

参考文献

学ぶために

製品や技術を入手するために

  • Vim ディストリビューションのダウンロード・ページから、それぞれのプラットフォームに対応した Vim の最新バージョンにアップグレードしてください。
  • ご自分に最適な方法で IBM 製品を評価してください。評価の方法としては、製品の試用版をダウンロードすることも、オンラインで製品を試してみることも、クラウド環境で製品を使用することもできます。また、SOA Sandbox では、数時間でサービス指向アーキテクチャーの実装方法を効率的に学ぶことができます。

議論するために

  • My developerWorks コミュニティーに加わってください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者が主導するブログ、フォーラム、グループ、ウィキを調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=477870
ArticleTitle=Vim エディターのスクリプトの作成: 第 5 回 イベント駆動型のスクリプト作成と自動化
publish-date=03032010