post

Vim ja:スクリプト

Contents

はじめに

どんなソフトウェアでも、色々な設定を必要や好みに応じて変更することで、カスタマイズをするでしょう。それ以上のことをしたいときはどうすればよいでしょうか。たとえば、「GUI 版ならこのカラースキームを、そうでなければこのカラースキームを使う」というような。「スクリプト」が必要となるのは、まさにそこです。スクリプトとは、基本的には、条件や操作をまとめて記述するための言語を言います。

Vim でスクリプトを組むには、2つのアプローチがあります。Vim 組み込みのスクリプト言語を使うか、Python や Perl などのほかの立派なプログラム言語を使うかです。後者の場合、Vim へアクセスするためのモジュールが提供されている必要があります(また、コンパイルの際に、そのためのオプションを有効にしていなければなりません)。

この章を読むには、プログラミングの知識が少し必要です。プログラミング経験のない方でも、理解はできますが、記述が簡潔すぎると感じられるかもしれません。プログラミングについて学習したい方は、私のほかのフリーブック A Byte of Python (英語) を御覧ください。

再利用できる機能を作るためには、2つの方法があります。マクロと、スクリプトです。

マクロ

マクロを用いて、コマンド操作を記録して、ほかの場面で再利用することができます。

たとえば、次のようなテキストがあるとしましょう。

   tansen is the singer
   daswant is the painter
   todarmal is the financial wizard
   abul fazl is the historian
   birbal is the wazir

いくつか、修正すべき点があります。

  1. 各文の1文字目を大文字にする。
  2. ‘is’ を ‘was’ に変える。
  3. ‘the’ を ‘a’ に変える。
  4. 文末に “in Akbar’s court.” と書く。

:s/^\w/\u/ のような置換コマンドをいくつか書くのもひとつの方法ですが、それには4つの置換コマンドを実行する必要があり、しかも必要のない部分まで置換してしまう可能性があるので、あまり単純ではありません。

マクロを使うと、もっと効果的にできます。

  1. カーソルを1行目 tansen is the singer の1文字目に合わせます。
  2. ノーマルモードで qa と入力することで、a という名前のマクロの記録を始めます。
  3. gUl と入力し、1文字目を大文字にします。
  4. w と入力し、次の単語に移動します。
  5. cw と入力し、単語を変更します。
  6. was と入力します。
  7. <Esc> を入力します。
  8. w を入力して次の単語に移動します。
  9. cw と入力し、単語を変更します。
  10. a と入力します。
  11. <Esc> を入力します。
  12. A と入力することで、行末へのテキストの挿入を始めます。
  13. in Akbar's court. と入力します。
  14. <Esc> を入力します。
  15. q と入力し、マクロの記録を終わります。

長い手順のように見えますが、複雑な置換コマンドをいくつか入力するよりは易しい場合もあるのです。

この手順が終わると、1行目は次のようになっているはずです。

   Tansen was a singer in Akbar's court.

OK ですね。では、他の行にもこれを適用しましょう。2行目の1文字目に移動して、@a と入力しましょう。するとどうでしょう!次のように行が編集されました。

   Daswant was a painter in Akbar's court.

このように、マクロは複雑な操作を記録し、すぐに再生することができます。マクロを使えば、複雑な編集を複数の箇所で実行できるのです。これが、テキストへの変更を再利用する、1つめの方法です。次は、もっと本格的な編集方法を見ていきます。

注意
操作のまとまりではなく、単に直近の操作を繰り返すだけならば、マクロを使う必要はありません。. (ドット)を入力するだけです。

スクリプトの基本

Vim にはスクリプト言語が組み込まれており、それを用いて、条件分岐の入ったスクリプトや、操作の「実行」、テキストの編集などができます。

アクション

テーマ、つまり Vim で使われる色を変えるには、次を実行します。

:colorscheme desert

ここでカラースキーム ‘desert’ を使ったのは、私のお気に入りだからです。:colorscheme とタイプした後 <tab> キーを押していくと、利用可能なスキームを見ることができます。

現在の行の文字数を知りたいときは?

:echo strlen(getline("."))

‘strlen’ と ‘getline’ という名前に注目しましょう。これらは「関数」です。関数とは、既に書かれたスクリプトの塊で、何度も使えるように名前がついているものです。たとえば、getline 関数は行を取得するもので、ここでは現在行を意味する .(ドット)を指定しています。getline 関数に返された結果は、テキストの文字数を数える関数 strlen に渡され、さらにその結果はテキストを表示するコマンド :echo に渡されます。情報の流れを把握しましょう。

strlen(getline(".")) は表現と呼ばれます。このような表現は変数に保管することができます。「変数」はその名の通りのことをします — 値を指し示すもので、その値は何でもあり得る、つまり変わるのです。たとえば、次のようにして行の長さを保管できます。

:let len = strlen(getline("."))
:echo "We have" len "characters in this line."

これを、上のテキストの2行目で実行すると、次の出力を得ます。

   We have 46 characters in this line.

ほかの「表現」について、変数をどう使えるか考えてみましょう。変数、表現そしてコマンドを使ってできることは無限です。

Vim には様々な種類の変数があり、それは語頭の文字で区別されます。たとえば $ は環境変数に、& はオプションに、@ はレジスタを意味します。

:echo $HOME
:echo &filetype
:echo @a

:help function-list に、利用可能な関数の膨大なリストがあります。

自作の関数を作ることもできます。

:function CurrentLineLength()
:    let len = strlen(getline("."))
:    return len
:endfunction

カーソルを好きな行に移動し、次のコマンドを実行しましょう。

:echo CurrentLineLength()

数が表示されるはずです。

関数名は大文字で始めなければいけません。これは、組み込みの関数は小文字で始まり、ユーザ定義関数は大文字で始まるという区別をするためです。

内容を表示するのではなく、単に「呼び」たいときは、:call CurrentLineLength() を使います。

条件分岐

Vim がターミナルで起動しているか GUI かに応じて、違うカラースキームを利用したいとしましょう。つまり、スクリプトに条件分岐させる必要があるのです。このようなときは、次のようにします。

:if has("gui_running")
:    colorscheme desert
:else
:    colorscheme darkblue
:endif

次のように働きます。

  • has() は、指定された機能を、いまコンピュータにインストールされている Vim がサポートしているかどうかを決定する関数です。どのような機能を指定できるかは、:help feature-list をご覧ください。
  • if 文は、与えれた条件をチェックします。条件が満たされれば指定された動作をします。そうでなければ (else)、もう1つの動作をします。
  • if 文には、対応する endif がなければなりません。
  • 複数の条件と動作を連ねたいときは、elseif を使うこともできます。

ループ文 ‘for’ と ‘while’ も用意されています。

:let i = 0
:while i < 5
:    echo i
:    let i += 1
:endwhile

出力:

   0
   1
   2
   3
   4

Vim の組み込み関数を使えば、同じことが次のようにしてできます。

:for i in range(5)
:    echo i
:endfor
  • range() は、数の範囲で定まるリストを作る組み込み関数です。詳しくは :help range() をご覧ください。
  • continue, break 文も使用できます。

データ構造

Vim スクリプトは、リストと辞書もサポートしています。これを用いて、複雑なデータ構造やプログラムも組み上げることができます。

:let fruits = ['apple', 'mango', 'coconut']

:echo fruits[0]
" apple

:echo len(fruits)
" 3

:call remove(fruits, 0)
:echo fruits
" ['mango', 'coconut']

:call sort(fruits)
:echo fruits
" ['coconut', 'mango']

:for fruit in fruits
:   echo "I like" fruit
:endfor
" I like coconut
" I like mango

利用できる関数はたくさんあります。:help function-list の ‘List manipulation’ や ‘Dictionary manipulation’ セクションをご覧ください。

Vim スクリプトを書く

では、Vim に読み込ませるための Vim スクリプトを書き始めましょう。これで、必要なときに機能を呼び出せます。今までやってきたような、インラインでスクリプトを書いて直接動かすのとは、その点が異なります。

単純な問題を考えてみましょう — 選択範囲の行にある各単語の1文字目を、大文字に変えるにはどうすればいいでしょうか?こんな応用があります — テキストの見出しを書くとき、単語の1文字目を大文字にすると見栄えがよくなりますが、私はそれを自分でやるのが面倒なのです。そこで、テキストを小文字で書いてから、この機能を呼び出したいのです。

スクリプトの基本となるテンプレートから始めましょう。次のスクリプトを capitalize.vim として保存してください。

" Vim global plugin for capitalizing first letter of each word
"       in the current line.
" Last Change: 2008-11-21 Fri 08:23 AM IST
" Maintainer: www.swaroopch.com/contact/
" License: www.opensource.org/licenses/bsd-license.php

if exists("loaded_capitalize")
    finish
endif
let loaded_capitalize = 1

" TODO: ここに実際の機能を搭載します。

次のように動きます。

  • 1行目では、ファイルの内容の説明をします。
  • ファイルの情報を示す、2-3 の標準的なヘッダがあります。たとえば ‘Last Changed:’ でスクリプトの更新日が、’Maintainer:’ で、スクリプトのユーザが制作者にコンタクトをとる(問題が発生したときや、感謝のメッセージを送るときなど)ために使われます。
  • ‘License:’ というヘッダは必須ではありませんが、推奨されます。あなたの書いた Vim スクリプトやプラグインは、ほかの多くの人に使われる可能性があるので、スクリプトのライセンスを明示できるのです。これにより、ほかの人があなたの作ったものを改良できるようになり、それが今度はあなた自身の利益ともなり得るのです。
  • スクリプトは、複数回、読み込まれる可能性があります。たとえば1つの Vim で2つの異なる .html ファイルを開いているとき、Vim はそれぞれのファイルについて HTML シンタックス色付けスクリプトを読み込みます。このとき同じスクリプトが2回読まれ、関数などを2回定義するのを防ぐため、 ‘loaded_capitalize’ という名前が存在するかチェックして、既に読まれているときはスクリプトを閉じます。

では、実際の機能を搭載していきましょう。

変換を行う関数を定義します。各単語の1文字目を大文字にするものなので、Capitalize() という名前にしましょう。関数は選択範囲に対して働くものですが、そのことをスクリプト中に明記することができます。

" Vim global plugin for capitalizing first letter of each word
"       in the current line
" Last Change: 2008-11-21 Fri 08:23 AM IST
" Maintainer: www.swaroopch.com/contact/
" License: www.opensource.org/licenses/bsd-license.php

" Make sure we run only once
if exists("loaded_capitalize")
    finish
endif
let loaded_capitalize = 1

" Capitalize the first letter of each word
function Capitalize() range
    for line_number in range(a:firstline, a:lastline)
        let line_content = getline(line_number)
        " Luckily, the Vim manual had the solution already!
        " Refer ":help s%" and see 'Examples' section
        let line_content = substitute(line_content, "\w\+", "\u\0", "g")
        call setline(line_number, line_content)
    endfor
endfunction

これは次のように働きます。

  • a:firstlinea:lastline はそれぞれ、範囲の開始行と最終行に対応する引数です。
  • 範囲内の各行(getline()で取得したもの)に対して処理をするために、’for’ ループを用いています。
  • 文字列に検索置換を施すために、substitute() 関数を使っています。ここでは ‘\w\+’ を指定して、単語(つまり連続する文字の集合)にマッチさせています。単語が見つかると、\u により変換されます。\u は、これに続く文字列の1文字目を大文字に変換することを示し、 は、substitute() 関数でマッチさせたもの(この場合は単語)に対応します。つまり結果として、各単語の1文字目を大文字に変換しているのです。
  • setline() 関数を用いて、Vim の行を新しい文字列で置き換えます。

このコマンドを実行するには、

  1. Vim を開き、テキストを何でもよいので(’this is a test’ のように)入力します。
  2. :source capitalize.vim を実行します。これでファイル中のコマンドを、今までのようにインラインで実行できるようになります。
  3. :call Capitalize() を実行します。
  4. 行が ‘This Is A Test’ に変わったはずです。

毎回 :call Capitalize() を実行するのは面倒なので、leader を用いたキーボードショートカットを用意しましょう。

" Vim global plugin for capitalizing first letter of each word
" in the current line
" Last Change: 2008-11-21 Fri 08:23 AM IST
" Maintainer: www.swaroopch.com/contact/
" License: www.opensource.org/licenses/bsd-license.php

" Make sure we run only once
if exists("loaded_capitalize")
    finish
endif
let loaded_capitalize = 1

" Refer ':help using-<Plug>'
if !hasmapto('<Plug>Capitalize')
    map <unique> <Leader>c <Plug>Capitalize
endif
noremap <unique> <script> <Plug>Capitalize <SID>Capitalize
noremap <SID>Capitalize :call <SID>Capitalize()<CR>

" Capitalize the first letter of each word
function s:Capitalize() range
    for line_number in range(a:firstline, a:lastline)
        let line_content = getline(line_number)
        " Luckily, the Vim manual had the solution already!
        " Refer ":help s%" and see 'Examples' section
        let line_content = substitute(line_content, "\w\+", "\u\0", "g")
        call setline(line_number, line_content)
    endfor
endfunction
  • 関数の名前を、’Capitalize’ から ‘s:Capitalize’ に変更しました。これは、関数が、定義されているスクリプトについてローカルであることを示しています。ほかのスクリプトを妨害しないように、グローバルには使えないようにしています。
  • ショートカットを定義するため、map を使っています。
  • <Leader キーは、通常はバックスラッシュです。
  • 機能を、<Leader>c(つまり leader キーと c キー)に割り当てています。
  • <Plug>Capitalize は、プラグイン、つまり我々のスクリプトに書かれた関数 Capitalize() を表します。
  • 全てのスクリプトは、<SID> で示される ID を持っています。従って、コマンド <SID>Capitalize をマップすることで、ローカル関数 Capitalize() を呼ぶことができます。

では、前に述べたのと同様にして、このスクリプトを試してみましょう。ただし、今回は :call Capitalize() のかわりに \c を実行することで、行を大文字にできます。

この最後のステップは複雑に見えますが、これは Vim スクリプトで複雑なことができるという、豊かな可能性をお見せするために書きました。

書いたスクリプトに不具合があった場合、v:errmsg を用いて直近のエラーメッセージを見ることができます。これが、何が悪いかを理解する助けになることがあります。

ここで述べたことは、全て :help に記載されています。たとえば :help \w:help setline() などです。

外部プログラミング言語の利用

Vim のスクリプト言語を新たに学ぶよりも、既知のプログラミング言語を使って Vim スクリプトを書きたいと思われる方も多いかもしれません。それは可能です。Vim は、Python, Perl, Ruby などの言語によるプラグインをサポートしています。

この章では、単純なプラグインを Python で作ってみますが、ほかのサポートされている言語でも簡単に同じことができます。

前に述べたように、Python を学びたい場合は、私のフリーブック A Byte of Python (英語) もお役に立てると思います。

まず、Python がサポートされているかチェックしておきましょう。

:echo has("python")

これで 1 が表示されれば大丈夫です。そうでなければ、マシンに Python をインストールしてからもう一度試してみてください。

さて、今、ブログの記事を書いているとしましょう。普通ブロガーは、自分の記事をできるだけ多くの人に読んでほしいと思うものです。ブログ記事には、よく検索エンジン経由でアクセスされます。従って、あるトピックについて(たとえば、光の散乱に関する研究でノーベル賞を受賞したインドの物理学者、C V Raman について)のブログ記事を書いているときは、そのトピックを検索するのに役に立つフレーズを入れておくとよいです。たとえば、’C V Raman’ について検索する人は、ほかにも ‘Raman 効果’ で検索する可能性があるので、そのブログ記事の中で言及したいと思われるかもしれません。

どのように、関連するフレーズを探せばよいのでしょうか。Yahoo! Search を使えば、簡単にできます。

まずは Python を用いて、関連するフレーズを検索するのに使う単語を現在のテキストから抽出する方法を見ましょう。

" Vim plugin for looking up popular search queries related
"       to the current line
" Last Updated: 2008-11-21 Fri 08:36 AM IST
" Maintainer: www.swaroopch.com/contact/
" License: www.opensource.org/licenses/bsd-license.php

" Make sure we run only once
if exists("loaded_related")
    finish
endif
let loaded_related = 1

" Look up Yahoo Search and show results to the user
function Related()
python <<EOF

import vim

print 'Length of the current line is', len(vim.current.line)

EOF
endfunction

外部言語でプラグインを書く方法は、組み込みのスクリプト言語で書く場合と同様です。

重要な違いは、Python で書いたコードを Python のインタプリタに通す必要があることです。これは上のように、EOF を用いることで実現します — python <<EOF コマンドから、次の EOF までのテキストが全て Python インタプリタを通されるのです。

このプログラムを試してみるには、Vim を開きなおして :source related.vim を実行してから、:call Related() を実行します。これで、Length of the current line is 54 のような表示が得られるはずです。

では、プログラムに実際の機能を搭載していきましょう。Yahoo! Search には 関連語句検索 というものがあり、web を通してアクセスできます。このサービスは Yahoo! Search の提供する Python API pYsearch を用いて利用できます。

" Vim plugin for looking up popular search queries related
" to the current line
" Last Updated: 2008-11-21 Fri 08:36 AM IST
" Maintainer: www.swaroopch.com/contact/
" License: www.opensource.org/licenses/bsd-license.php

" Make sure we run only once
if exists("loaded_related")
    finish
endif
let loaded_related = 1

" Look up Yahoo Search and show results to the user
function Related()
python <<EOF

import vim
from yahoo.search.web import RelatedSuggestion

search = RelatedSuggestion(app_id='vimsearch', query=vim.current.line)
results = search.parse_results()

msg = 'Related popular searches are:n'
for i, result in enumerate(results):
    msg += '%d.%sn' % (i + 1, result)
print msg

EOF
endfunction

ここでは現在の行を検索語句にしていますが、たとえば現在の単語のように、好きなテキストに変更できることに注意しましょう。

関連語句を Yahoo! Search に問い合わせるのに、yahoo.search.web.RelatedSuggestion クラスを使いました。結果を parse_results() 関数を呼び出すことで取得し、得られた結果についてループさせ、ユーザに表示しています。

  1. :source related.vim を実行します。
  2. c v raman というテキストを入力します。
  3. :call Related() を実行します。
  4. 次のような出力が得られます:
   Related popular searches are:
   1. raman effect
   2. c v raman india
   3. raman research institute
   4. chandrasekhara venkata raman

おわりに

Vim 独自のスクリプト言語や、外部のプログラム言語を用いたスクリプトを学んできました。これは、Vim の機能が無限に広がるという点で重要です。

詳しくは、:help eval, :help python-commands, :help perl-using:help ruby-commands をご覧ください。


Advertisements