Vim エディターのスクリプトの作成: 第 2 回 ユーザー定義関数

オートメーションの基本となるビルディング・ブロックの作成

実際のプログラミング・タスクの複雑さを管理するために、アプリケーションを保守可能なコンポーネントへと適切に分解するには、ユーザー定義関数は欠かせないツールです。連載第 2 回目となるこの記事では、Vim スクリプト言語で新しい関数を作成してデプロイする方法を、なぜその関数が必要になるかを説明する実際的な例と併せて説明します。

Damian Conway, Dr., CEO and Chief Trainer, Thoughtstream

author photo - damian conwayDamian Conway は、オーストラリア Monash University でコンピューター・サイエンスの Adjunct Associate Professor を務める傍ら、国際的 IT トレーニング会社、Thoughtstream の CEO でもあります。彼が日常的に vi を使うようになって 4 半世紀が過ぎた今、彼がこの依存症を克服する見込みはなさそうです。



2009年 7月 07日

Vim スクリプトとこの連載について

Vim スクリプトは、Vim エディターを変更し、拡張することを可能にする強力なスクリプト言語です。この言語を使えば、新しいツールを作成したり、共通のタスクを単純化したり、さらにはエディターの既存の機能を作成し直すことさえできます。この連載では、読者がある程度 Vim エディターを使い慣れていることを前提とします。また、「Vim エディターのスクリプトの作成: 第 1 回」も読んでください。第 1 回の記事では、変数、値、および式について説明するとともに、関数を作成する際に必要となる情報も記載されています。

ユーザー定義関数

本格的なプログラミング言語で最も重要な機能は何かと尋ねれば、Haskell や Scheme のプログラマーは、それは関数だと答えるでしょう。C または Perl のプログラマーでも、まったく同じ答えを返してくるはずです。

関数は、本格的なプログラマーにとって不可欠な以下の 2 つのメリットを与えてくれます。

  1. 関数によって、複雑な計算タスクを一人の人間の頭脳で無理なく理解できるだけの小さな構成部分に分解することができます。
  2. これらの分解された部分に論理的な理解しやすい名前を付けて、一人の人間の頭脳でも完全に操作できるようにすることができます。

Vim スクリプトは本格的なプログラミング言語なので、当然、ユーザー定義関数の作成をサポートします。しかも、そのユーザー定義関数のサポートは Scheme、C、または Perl より優れていると言っても過言ではありません。この記事では、Vim スクリプトの関数が持つさまざまな機能を探り、これらの機能を利用して Vim の組み込み機能を、保守しやすい方法で強化、拡張する方法を説明します。

関数の宣言

Vim スクリプトで関数を定義するには、function キーワードに続けて関数の名前、次にパラメーターのリストを指定します (関数が引数を取らないとしても、パラメーターのリストは必須です)。関数の本体は次の行から始まり、function キーワードに対応する endfunction キーワードが出てくるまで続きます。例えば、以下のようになります。

リスト 1. 適切に構成された関数
functionExpurgateText (text)
    let expurgated_text = a:text

    for expletive in [ 'cagal', 'frak', 'gorram', 'mebs', 'zarking']
        let expurgated_text
        \   = substitute(expurgated_text, expletive, '[DELETED]', 'g')
    endfor

    return expurgated_text
endfunction

関数の戻り値は、return 文を使って指定します。return 文は必要な数だけ指定することができます。関数がプロシージャーとして使用されていて、有用な戻り値がない場合には、この文を含めないという選択肢もあります。ただし、Vim スクリプトの関数は必ず値を返すため、return を指定しないと関数は自動的にゼロを返すことになります。

Vim スクリプトでの関数名は、大文字で始めなければなりません。

リスト 2. 大文字で始まる関数名
function SaveBackup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

nmap <silent>  <C-B>  :call SaveBackup()<CR>

上記の例で定義している関数は、現行バッファーの b:backup_count 変数の値をインクリメントします (まだ変数が存在していない場合は、変数の値を 1 に初期化します)。次に、現行ファイルのすべての行を取得し (getline(1,'$'))、組み込み関数 writefile() を呼び出して、この取得した行をディスクに書き込みます。writefile() の 2 番目の引数は、書き込み先の新規ファイルの名前です。この例の場合、現行ファイルの名前 (bufname('%')) にカウンターの新しい値が付加された名前となります。返される値は、writefile() 呼び出しの成功/失敗を示す値です。最後に nmap がこの関数を呼び出すための CTRL-B をセットアップして、現行ファイルの番号付きバックアップを作成します。

先頭に大文字を使う代わりに、(第 1 回で、変数のスコープ指定に接頭辞を使ったように) 明示的なスコープ接頭辞を設定して Vim スクリプト関数を宣言することもできます。最もよく使われる接頭辞は s: です。この接頭辞は、関数を現行のスクリプト・ファイルのローカル関数として指定します。このようにして関数にスコープを設定する場合には、関数名を大文字で始めなくても、有効な関数として識別されます。ただし、関数に明示的にスコープを設定した場合、その関数は必ずそのスコープ接頭辞を使用して呼び出さなければなりません。以下は、その一例です。

リスト 3. スコープ接頭辞付きの関数を呼び出す
" Function scoped to current script file...
function s:save_backup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

nmap <silent>  <C-B>  :call s:save_backup()<CR>

再宣言可能な関数

Vim スクリプトでの関数宣言はランタイム文です。つまり、スクリプトが 2 回ロードされると、そのスクリプト内の関数宣言が 2 回実行されて、対応する関数が再び作成されることになります。

関数の再宣言は、致命的エラーとして扱われます (別個の 2 つのスクリプトが誤って同じ名前の関数を宣言するという衝突を避けるためです)。このことが、例えばカスタムの構文強調表示スクリプトなど、繰り返しロードされるように意図された関数をスクリプト内に作成することを難しくしています。

そこで Vim スクリプトが提供しているのが、キーワード修飾子 (function!) です。この修飾子によって、何度でも必要なだけ関数宣言をリロードしても安全であると示すことができます。

リスト 4. 関数宣言を安全にリロードできることを示す
function! s:save_backup ()
    let b:backup_count = exists('b:backup_count') ? b:backup_count+1 : 1
    return writefile(getline(1,'$'), bufname('%') . '_' . b:backup_count)
endfunction

このようにキーワード修飾子を使って定義された関数では、再宣言のチェックは行われません。そのため、キーワード修飾子は明示的にスコープが設定された関数で使用するのが最適です (スコープの設定によって、関数が別のスクリプトからの関数と衝突しないことが確実になっているからです)。

関数の呼び出し

関数を呼び出して、その戻り値を長い式の一部として使用するには、関数の名前を指定し、それに続けて括弧で囲んだ引数のリストを記述するだけのことです。

リスト 5. 関数の戻り値を使用する
"Clean up the current line...
let success = setline('.', ExpurgateText(getline('.')) )

ただし注意する点として、C や Perl とは異なり、Vim スクリプトでは関数の戻り値を使用することなく破棄することはできません。したがって、関数をプロシージャーやサブルーチンとして使用して、その戻り値を無視する場合には、call コマンドを先頭に付けて関数を呼び出す必要があります。

リスト 6. その戻り値を使用しないで関数を使用する
"Checkpoint the text...
call SaveBackup()

このようにしないと、Vim スクリプトは関数呼び出しが実際には組み込み Vim コマンドであるという前提の下に、そのようなコマンドは存在しないとしてエラーを出すことになります。関数とコマンドの違いについては、連載の今後の記事で取り上げます。

パラメーター・リスト

Vim スクリプトでは、明示的なパラメーターと可変個引数のパラメーター・リスト、さらにはこの 2 つを組み合わせて定義することもできます。

サブルーチンの名前の宣言の直後には、明示的に名前を付けた最大 20 個のパラメーターを指定することができます。関数の中では、指定したパラメーターの名前に接頭辞 a: を付加すれば、現在呼び出されている関数に渡された引数の値を使用することができます。

リスト 7. 関数の中で引数の値を使用する
function PrintDetails(name, title, email)
    echo 'Name:   '  a:title  a:name
    echo 'Contact:'  a:email
endfunction

関数に指定される引数の個数がわからない場合には、名前付きパラメーターの代わりに省略記号 (...) を使用することで、可変個引数のパラメーター・リストを指定することができます。この場合、任意の数の引数を指定して関数を呼び出せるようになり、引数の値はまとめて 1 つの変数に集約されます。つまり、a:000 という名前の配列です。さらに個々の引数にも、a:1a:2a:3、等々の位置パラメーター名が指定されます。引数の数は、a:0 として表すことができます。以下の例を見てください。

リスト 8. 可変個引数パラメーター・リストを指定して使用する
function Average(...)
    let sum = 0.0

    for nextval in a:000"a:000 is the list of arguments
        let sum += nextval
    endfor


    return sum / a:0"a:0 is the number of arguments
endfunction

上記の例では、sum を明示的な浮動小数点の値に初期化しなければならないことに注意してください。そうしないと、この後のすべての計算で整数演算が使用されることになります。

名前付きパラメーターと可変個引数パラメーターの組み合わせ

名前付きパラメーターと可変個引数のパラメーターを同じ関数で使用するには、名前付きパラメーターのリストの後に、可変個引数の省略記号を続けます。

例えば、ストリングが渡されて、各種のプロブラミング言語に対応した適切なコメント・ブロックにフォーマット設定される CommentBlock() 関数を作成するとします。このような関数では必ず、呼び出し側がフォーマット設定対象のストリングを提供する必要があるため、パラメーターには明示的に名前を付けなければなりません。しかし、コメント・イントロデューサー (コメント行を表す行頭の文字)、「ボックス作成」文字 (ボックスのコメントを作成する際の開始行と終了行を構成する文字)、そしてコメントの幅はすべてオプションにしたいとします (省略された場合の妥当なデフォルト値も設定します)。この場合、以下のように関数を呼び出すことになります。

リスト 9. 単純な CommentBlock 関数呼び出し
call CommentBlock("This is a comment")

すると関数は、以下のような複数の行からなるストリングを返します。

リスト 10. CommentBlock が返すストリング
//*******************
// This is a comment
//*******************

一方、引数を追加するとしたら、これらの引数でコメント・イントロデューサー、「ボックス作成」文字、コメントの幅にデフォルト以外の値が指定されることになります。その場合の呼び出しは以下のようになります。

リスト 11. 入り組んだ CommentBlock 関数の呼び出し
call CommentBlock("This is a comment", '#', '=', 40)

上記の呼び出しによって、以下のストリングが返されます。

リスト 12. CommentBlock が返すストリング
#========================================
# This is a comment
#========================================

このような関数の実装としては、以下のような例が考えられます。

リスト 13. CommentBlock の実装
function CommentBlock(comment, ...)
    "If 1 or more optional args, first optional arg is introducer...
    let introducer =  a:0 >= 1  ?  a:1  :  "//"

    "If 2 or more optional args, second optional arg is boxing character...
    let box_char   =  a:0 >= 2  ?  a:2  :  "*"

    "If 3 or more optional args, third optional arg is comment width...
    let width      =  a:0 >= 3  ?  a:3  :  strlen(a:comment) + 2

    " Build the comment box and put the comment inside it...
    return introducer . repeat(box_char,width) . "\<CR>"
    \    . introducer . " " . a:comment        . "\<CR>"
    \    . introducer . repeat(box_char,width) . "\<CR>"
endfunction

1 つ以上のオプション引数 (a:0 >= 1) がある場合、最初のオプション (つまり、a:1) がコメント・イントロデューサーのパラメーターに割り当てられます。オプション引数がなければ、コメント・イントロデューサーとしてはデフォルト値の「//」が割り当てられます。同様に、2 つ以上のオプション引数 (a:0 >= 2) が指定されている場合には、2 番目のオプション (a:2) が box_char 変数に割り当てられ、そうでない場合にはデフォルト値の「*」が box_char 変数に割り当てられます。3 つ以上のオプション引数が指定されている場合は、3 番目のオプションが width 変数に割り当てられます。幅の引数が指定されない場合には、コメント引数自体から適切な幅が自動的に計算されます (strlen(a:comment)+2)。

最終的にすべてのパラメーター値が解決されると、行頭のコメント・イントロデューサーと、それに続く適切な回数の「ボックス作成」文字 (repeat(box_char,width)) の繰り返しによって、コメント・ボックスの開始行と終了行が構成されます。コメント・テキスト自体は、その 2 行の間に挟まれる形となります。

当然、この関数を使用するには何らかの方法で関数を呼び出さなければなりません。それには、以下の挿入マップが理想的な方法となるはずです。

リスト 14. 挿入マップを使用して関数を呼び出す
"C++/Java/PHP comment...
imap <silent>  ///  <C-R>=CommentBlock(input("Enter comment: "))<CR>

"Ada/Applescript/Eiffel comment...
imap <silent>  ---  <C-R>=CommentBlock(input("Enter comment: "),'--')<CR>

"Perl/Python/Shell comment...
imap <silent>  ###  <C-R>=CommentBlock(input("Enter comment: "),'#','#')<CR>

マップのそれぞれでは、まず初めにユーザーにコメントのテキストを入力するように要求する組み込み関数 input() が呼び出されます。続いて CommentBlock() 関数が呼び出され、入力されたテキストがコメント・ブロックに変換されます。そして最後に先行 <C-R>= によって最終的なストリングが挿入されるというわけです。

最初のマップでは、1 つしか引数を渡していないことに注目してください。そのため、コメント・マーカーにはデフォルトの // が使用されます。2 番目と 3 番目のマップでは 2 つ目の引数を渡し、それぞれのコメント・イントロデューサーとして # または -- を指定しています。3 番目のマップではさらに 3 つ目の引数を渡すことによって、「ボックス作成」文字をそのコメント・イントロデューサーと同じ文字になるようにしています。


関数および行範囲

call を含め、標準的な Vim コマンドはいずれも、行範囲を事前に指定して呼び出すことができます。このようにして呼び出すと、コマンドはその範囲に含まれる各行に対し、1 回実行されることになります。

「現在の行 (.) からファイルの終わり ($) までのすべての行を削除する…
:.,$delete

「行 1 から 10 までに含まれるすべての「foo」を「bar」に置換する
:1,10s/foo/bar/

「現在の行の 5 行上から 5 行下までのすべての行をセンタリングする…
:-5,+5center

この機能について詳しく調べるには、任意の Vim セッションで :help cmdline-ranges と入力してください。

call コマンドの場合には、範囲を指定すると、要求された関数が範囲内の各行に対して 1 度ずつ、繰り返し呼び出されることになります。この機能が役立つ理由を理解するには、現在の行にある「未加工」のアンパサンドを XML の適切な実体 &amp; に変換するだけでなく、すでに他の実体の一部となっているアンパサンドは無視できるだけの賢い関数を作成する方法を考えてみてください。この関数の実装としては、以下のような例が考えられます。

リスト 15. アンパサンドを変換する関数
function DeAmperfy()
    "Get current line...
    let curr_line   = getline('.')

    "Replace raw ampersands...
    let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&amp;','g')

    "Update current line...
    call setline('.', replacement)
endfunction

DeAmperfy() の最初の行は、エディターのバッファーから現在の行を取得します (getline('.'))。2 番目の行は、否定先読みパターン '&\(\w\+;\)\@!' を使用して (詳細は :help \@! を実行して調べてください)、取得した行内で識別子およびコロンが続いていないすべての & を検索します。続いて substitute() を呼び出すことで、該当するすべての「未加工」のアンパサンドが XML の実体 &amp; に置換されます。そして最後に、DeAmperfy() の 3 番目の行が現在の行を変更後のテキストで更新します。

この関数をコマンドラインから呼び出すには、以下のコマンドを使用します。

:call DeAmperfy()

このコマンドは、現在の行でしか置換を行いません。一方、以下のように範囲を指定してから関数を呼び出すとします。

:1,$call DeAmperfy()

すると、この範囲内にある各行に対して関数が呼び出されることになります (上記の例では、ファイルに含まれるすべての行ごとに呼び出されます)。

関数の行範囲の内部化

このように、「各行に対して繰り返し関数を呼び出す」という振る舞いは便利なデフォルトです。その一方、範囲を指定しながらも関数を一度だけ呼び出し、その上で、関数の内部で範囲のセマンティクスを処理したいという場合もあります。このような場合にも、Vim スクリプトでは簡単に対処することができます。ただ単に、特殊な修飾子 (range) を関数宣言に追加すればよいだけのことです。

リスト 16. 関数内部での範囲のセマンティクス
function DeAmperfyAll() range"Step through each line in the range...
    for linenum in range(a:firstline, a:lastline)
        "Replace loose ampersands (as in DeAmperfy())...
        let curr_line   = getline(linenum)
        let replacement = substitute(curr_line,'&\(\w\+;\)\@!','&amp;','g')
        call setline(linenum, replacement)
    endfor

    "Report what was done...
    if a:lastline > a:firstline
        echo "DeAmperfied" (a:lastline - a:firstline + 1) "lines"
    endif
endfunction

リスト 16 のようにパラメーター・リストの後に range 修飾子を指定した場合、DeAmperfyAll() を呼び出すときには常に以下のように範囲を指定します。

:1,$call DeAmperfyAll()

こうすれば、関数は 1 度だけ呼び出されて、a:firstlinea:lastline の 2 つの特殊な引数がそれぞれ範囲の開始行番号と終了行番号に設定されます。範囲が指定されていない場合には、a:firstlinea:lastline はどちらも現在の行番号に設定されます。

この関数はまず、該当するすべての行の番号のリストを作成します (range(a:firstline, a:lastline))。この場合の組み込み関数 range() の呼び出しは、関数宣言の一部として range 修飾子を使用することとはまったく関係ないことに注意してください。この場合の range() 関数は、Python での range() 関数や、Haskell または Perl での .. 演算子とまったく同じように、単にリストを作成するだけのものです。

処理対象の行番号のリストが決定されると、関数は for ループを使用して各行を順に処理します。

for linenum in range(a:firstline, a:lastline)

そして各行を (元の DeAmperfy() と同じく) 適宜、更新します。

最後に、範囲に複数の行が含まれる場合 (つまり、a:lastline > a:firstline の場合)、この関数は更新された行数をレポートします。

ビジュアル範囲

ある行の範囲で機能することが可能な関数呼び出しを作成した場合にとりわけ役に立つのが、ビジュアル・モードでその関数を呼び出すという手法です (詳細は :help Visual-mode を実行して調べてください)。

例えば、カーソルがテキスト・ブロック内にある場合、以下のコマンドを使用すれば、カーソル周辺のパラグラフに含まれるすべてのアンパサンドをエンコードすることができます。

Vip:call DeAmperfyAll()

通常モードで V と入力すると、ビジュアル・モードに切り替わります。続く ip がビジュアル・モードに現行のパラグラフ全体を強調表示するように指示し、それに続く : によってコマンド・モードに切り替わります。コマンドの範囲は、ビジュアル・モードで選択した行の範囲に自動的に設定されます。この時点で DeAmperfyAll() を呼び出すと、範囲に含まれるすべてのアンパサンドが置換されます。

この例の場合、以下のコマンドを使っても同じ効果が得られます。

Vip:call DeAmperfy()

唯一の違いは、上記のコマンドでは、ビジュアル・モードで Vip によって強調表示された各行に対して 1 度ずつ DeAmperfy() 関数が繰り返し呼び出されることです。


コードの作成に役立つ関数

Vim スクリプトの大部分のユーザー定義関数には、少数のパラメーターしか必要ありません。場合によっては、パラメーターがまったく必要ないこともあります。これは、ユーザー定義関数は通常、現行のエディター・バッファーとコンテキストに依存した情報 (現在のカーソル位置、現在のパラグラフ・サイズ、現在のウィンドウ・サイズ、現在の行のコンテンツなど) からデータを直接取得するためです。

その上、引数リストからデータを取得する関数に比べ、コンテキストからデータを取得する関数は、大抵の場合、はるかに有益で便利です。例えばソース・コードを保守する上でありがちな問題は、代入演算子が累積するにつれて位置合わせが崩れ、コードが読みにくくなってしまうことです。

リスト 16. 整列していない代入演算子
let applicants_name = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative = 'sister'
let fathers_occupation = 'Sith'

新しい文を追加するたびに、代入演算子の位置を手動で調整するのは面倒です。

リスト 17. 手動での代入演算子の位置調整
let applicants_name     = 'Luke'
let mothers_maiden_name = 'Amidala'
let closest_relative    = 'sister'
let fathers_occupation  = 'Sith'

日常のコーディング作業でこのような面倒を省くには、現行のコード・ブロックを選択して代入演算子が含まれる行を見つけ、自動的に代入演算子の位置を揃えるキー・マッピング (;=など) を作成するという方法があります。以下はその一例です。

リスト 18. 代入演算子の位置合わせをする関数
function AlignAssignments ()
    "Patterns needed to locate assignment operators...
    let ASSIGN_OP   = '[-+*/%|&]\?=\@<!=[=~]\@!'
    let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'

    "Locate block of code to be considered (same indentation, no blanks)
    let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
    let firstline  = search('^\%('. indent_pat . '\)\@!','bnW') + 1
    let lastline   = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
    if lastline < 0
        let lastline = line('$')
    endif

    "Find the column at which the operators should be aligned...
    let max_align_col = 0
    let max_op_width  = 0
    for linetext in getline(firstline, lastline)
        "Does this line have an assignment in it?
        let left_width = match(linetext, '\s*' . ASSIGN_OP)

        "If so, track the maximal assignment column and operator width...
        if left_width >= 0
            let max_align_col = max([max_align_col, left_width])

            let op_width      = strlen(matchstr(linetext, ASSIGN_OP))
            let max_op_width  = max([max_op_width, op_width+1])
         endif
    endfor

    "Code needed to reformat lines so as to align operators...
    let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
    \                                    max_op_width,  submatch(2))'

    " Reformat lines with operators aligned in the appropriate column...
    for linenum in range(firstline, lastline)
        let oldline = getline(linenum)
        let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
        call setline(linenum, newline)
    endfor
endfunction

nmap <silent>  ;=  :call AlignAssignments()<CR>

この AlignAssignments() 関数が最初に実行することは、2 つの正規表現のセットアップです (Vim の正規表現構文についての詳細は、:help pattern を実行して調べてください)。

let ASSIGN_OP   = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)'

ASSIGN_OP のパターンは、標準的な代入演算子 (=+=-=*= など) のどれとでも一致しますが、= が含まれるその他の演算子 (===~ など) との一致は慎重に回避しています。使用する言語にその他の代入演算子 (.=||=^= など) がある場合には、それに該当するものを認識するように ASSIGN_OP 正規表現を拡張してください。あるいは ASSIGN_OP を再定義して、コメント・イントロデューサーや列マーカーなどのその他のタイプの「位置合わせ可能な要素」を認識して、これらの要素を代わりに位置合わせするようにしても構いません。

ASSIGN_LINE のパターンが対象とするのは行の先頭 (^) だけで、最小数の文字 (.\{-})、次に空白文字 (\s*)、そして代入演算子を突き合わせます。

最初の「最小数の文字」サブパターンと演算子サブパターンはどちらも \(...\) の括弧のなかに取り込まれた上で指定されていることに注意してください。正規表現のこの 2 つのコンポーネントに取り込まれたサブストリングは、後で組み込み関数 submatch() を呼び出すことによって抽出されます。具体的には、submatch(1) を呼び出して演算子の左側にあるコードを抽出し、それから submatch(2) を呼び出して演算子自体を抽出します。

AlignAssignments() は次に、操作対象の行の範囲を見つけます。

let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline  = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline   = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
    let lastline = line('$')
endif

これまでの例では、関数は明示的なコマンド範囲またはビジュアル・モードでの選択に依存して操作対象の行を決定していましたが、この AlignAssignments() 関数はその固有の範囲を直接計算します。具体的に言うと、最初に組み込み関数 matchstr() を呼び出して、現在の行 (getline('.')) の先頭がどのような空白文字の並びになっているか ('^\s*') を判別します。次に、これとまったく同じ並びの空白文字を、空ではない行 (従って、最後に '\S' が付けられています) の先頭で突き合わせる新しい正規表現を indent_pat として作成します。

AlignAssignments() は続いて、組み込み関数 search() を呼び出して上方検索を行い (フラグ 'bnW' を使用)、カーソルの上方にあり、完全に同じインデントが使用されていない最初の行を見つけます。この行番号に 1 を加算して、対象範囲の開始、つまり現在の行と同じインデントが設定された連続する行の最初の行を指定します。

今度は 2 番目の search() 呼び出しで下方検索 ('nW') を行って lastline を決定します。これは、同じインデントが設定された連続する行の最後の行の番号です。この場合の呼び出しでは、インデントが異なっている行を検出することなく、検索がファイルの終わりに達する可能性がありますが、その場合には search()-1 を返します。このケースを適切に処理するため、以下の if 文が明示的に lastline をファイルの終わりの行番号 (つまり、line('$') によって返される行番号) に設定します。

この 2 つの検索による結果、AlignAssignments() は現在の行とまったく同じインデントが設定された連続する行の範囲全体 (現在の行より上あるいは下にまで連続する行) を認識することになります。そしてこの情報を使用して、同じコード・ブロック内の同じスコープ・レベルにある代入文だけを位置合わせするようにします。ただし、コードのインデントがこのスコープを正しく反映していなければ、当然、悲惨なフォーマット設定という事態を招くことになります。

AlignAssignments() の最初の for ループは、位置合わせする代入演算子が含まれる列を決定します。その方法として、選択された範囲内の行のリスト (getline(firstline, lastline) によって取得された行) をウォークスルーし、それぞれの行に (場合によっては空白文字で始まる) 代入演算子が含まれているかどうかをチェックします。

let left_width = match(linetext, '\s*' . ASSIGN_OP)

行に演算子が含まれていない場合、組み込み関数 match() は一致を検出できないことから -1 を返します。その場合には、ループは単純に次の行にスキップします。演算子が含まれている場合には、match() はその演算子が現れる位置を示す (正の) インデックスを返します。すると if 文は組み込み関数 max() を使用して、この最新の列位置が、それよりも前に配置されている演算子よりも右の位置にあるかどうかを判断することによって、範囲内のすべての代入演算子を位置合わせするために必要な最大列位置を調べます。

let max_align_col = max([max_align_col, left_width])

if 文の残りの 2 行では、組み込み関数 matchstr() によって実際の演算子を取得し、続いて組み込み関数 strlen() を使用してその演算子の長さを判断します (「=」の場合は 1、「+=」、「-=」などの場合には 2 となります)。次に max_op_width 変数を使用して、範囲内に含まれるさまざまな演算子を位置合わせするために必要な最大幅が調べられます。

let op_width     = strlen(matchstr(linetext, ASSIGN_OP))
let max_op_width = max([max_op_width, op_width+1])

位置合わせ対象のゾーンの位置と幅が決まれば、後は範囲内の行を繰り返し処理して、それぞれに応じてフォーマットを再設定すればよいだけの話です。この再フォーマットを行うため、関数は組み込み関数 printf() を使用します。この関数は大いに役に立ちますが、その名前はかなり不適切です。この関数は、C や Perl、あるいは PHP の printf 関数と同じではありません。実際には、これらの言語の sprintf 関数に相当します。つまり Vim スクリプトでは、printf はフォーマット設定されたデータ引数リストを出力する関数ではなく、フォーマット設定されたデータ引数リストを含むストリングを返す関数であるということです。

各行をフォーマット設定しなおすには、AlignAssignments() が組み込み関数 substitute() を使用して、演算子までのすべてのテキストを printf で再編成したテキストに置き換えるのが理想的ですが、残念ながら、substitute() が置き換える値として期待するのは関数呼び出しではありません。この関数が期待するのは固定ストリングです。

したがって printf() を使用して各置換テキストをフォーマット設定しなおすには、特殊な組み込み置換フォーム、"\=expr" を使用する必要があります。置換ストリングの先行 \= は、これに続く式を評価し、その結果を置換テキストとして使用するように substitute() に指示します。これは挿入モードの <C-R>= メカニズムと似ていますが、この魔法の振る舞いは組み込み関数 substitute() の置換ストリング (または標準的な :s/.../.../ Vim コマンド) に対してしか機能しないので注意してください。

この例では、特殊な置換フォームはすべての行に共通して printf となるため、2 番目の for ループが開始される前に、以下のようにしてこのフォームをあらかじめ FORMATTER 変数に格納します。

let FORMATTER = '\=printf("%-*s%*s", max_align_col, submatch(1),
\                                    max_op_width,  submatch(2))'

このようにして組み込まれた printf() が最終的に substitute() によって呼び出されると、演算子の左側にあるコード (submatch(1)) を (%-*s プレース・ホルダーを使用して) 左寄せし、max_align_col 文字幅のフィールドに配置します。次に、演算子自体 (submatch(2)) を max_op_width 文字幅の 2 番目のフィールドに (%*s を使用して) 右寄せして配置します。- および * オプションが、ここで使用されている 2 つの %s フォーマット指定子を変更する仕組みについての詳細は、:help printf() を実行して調べてください。

このフォーマッターが使用できるようになったことから、2 番目の for ループは行番号の範囲全体を繰り返し処理して、対応するテキスト・バッファーの内容を一度に 1 行ずつ取得することができます。

for linenum in range(firstline, lastline)
    let oldline = getline(linenum)

ループは次に、取得したテキスト・バッファーの各行の内容を変換するために substitute() を使用して代入演算子までのコード (代入演算子を含む) を突き合わせ (ASSIGN_LINEのパターンを使用)、そのテキストを (FORMATTER の指定に従って) printf() 呼び出しの結果に置換します。

    let newline = substitute(oldline, ASSIGN_LINE, FORMATTER, "")
    call setline(linenum, newline)
endfor

for ループがすべての行を処理し終わると、行に含まれる代入演算子が正しく位置合わせされた状態になります。後は、以下のように AlignAssignments() 呼び出すキー・マッピングを作成するだけです。

nmap <silent>  ;=  :call AlignAssignments()<CR>

これらからの展望

関数は、実際の Vim プログラミング・タスクの複雑さを管理するために、アプリケーションを保守可能なコンポーネントへと適切に分解するには欠かせないツールです。

Vim スクリプトでは、一定または可変個引数のパラメーター・リストを使って関数を定義することができます。また、定義した関数を使ってエディターのテキスト・バッファーにある行の範囲を、自動的にあるいはユーザーが制御する方法で、操作することも可能です。関数は Vim の組み込み機能 (例えば、テキストに対する search()substitute() など) を呼び出して、エディターの状態情報に直接アクセスすることも (line('.') によって、カーソルが置かれている現在の行を判断するなど)、現在編集中のテキスト・バッファーを (getline() および setline() を使用して) 操作することもできます。

こうした機能は明らかに強力ですが、プログラマーがエディターの状態やテキスト・バッファーの内容をプログラムによって操作できるかどうかは、常にコードの操作対象のデータをいかに簡潔かつ正確に表現できるかによって左右されます。この連載ではこれまで単一のスカラー値 (数値、ストリング、ブール値) に限って使用してきました。次の 2 回の記事では、これより遙かに強力で便利なデータ構造として、番号付きリストとランダム・アクセス・ディクショナリーの使用について探ります。

参考文献

学ぶために

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

議論するために

  • My developerWorks コミュニティーに加わってください。自分個人のプロファイルとカスタム・ホーム・ページを作成して、自分の興味に合わせて 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=418065
ArticleTitle=Vim エディターのスクリプトの作成: 第 2 回 ユーザー定義関数
publish-date=07072009