sci

最果て風呂

BecomeAnVimscripter

ナイショ話

こんな事を知られたら袋叩きにあってしまうので、ここだけの話としてあなたの胸の中に仕舞っておいて欲しい。実はこのグループに出入りしている人達っていうのは、まるで息を吸うように Vim を使っているのです。たぶんコーディング中に何も躊躇しないんじゃないかな?

それに比べて私とくれば、何をするにもマニュアルとにらめっこしなければカーソルの移動すらおぼつかない初心者です。だから高くて厚い壁が存在しているの。群衆の中の猫って感じ。そんなレベルの人間が書くものを読むのは時間の無駄じゃないかって?その意見は正解なんだけど、おじさんの愚痴にしばらく付き合って欲しいのです。

「ユーザと開発者を結ぶ」と称しているのだけど、私からすれば「想定しているユーザのレベルが高すぎる」と言わざるを得ない。最低限コンピュータ業界でご飯を食べている人が対象となっているように思えるのだ。普及先は庶民にあらずって感じ。

「専門家のみが使うことを許された至高のエディタなのか?」なんて印象を持ってしまうのは私だけではないだろう。でも安心して。Vim ってそんな遠く離れた存在ではありません。日記の入力しかしない素人でも遊べる楽しいエディタなんです。

ナイショ話しはこのくらいにして、はたしてその楽しい部分はどこなのか?おじさんと一緒に探求してみよう。Vim タンのような萌えキャラじゃなくてゴメンネ。想像力を働かせて脳内変換してください。みなさんお得意でしょう?脳内変換。

Vimscript を書く--とあるケース

ここから先は私が初めて作成したスクリプトを題材にして、その過程を追うことで読者に追体験してもらうことにします。「コマンドまとめ」みたいなチートシートは沢山あるけど、筋道を提示しているものってあまり見掛けないのでアリですよね?ナイナイ。

間違いが多々あると思いますが、初心者の目線で書けるのは今だけだと思うから、今残しておくのです。あくまでも見習うべきでない遠回りのケースとして読んでくださいね。初心者の初心者による初心者のための...です。

決まった単語を別の単語に置き換えたい

ひと通り文字の入力ができるようになって、追記や削除に続いてやりたい事と言えば検索・置換だよね。検索・置換の一番簡単な方法は対象となる行にカーソルを移動してからコマンドモードになって下記を実行すれば良いんだって。これって 2ch で良く見掛ける誤字を指摘する方法と同じだ。ねら〜は Vim 使い多し。

:s/笑え/笑へ/
````

    笑えばいいと思うよ。そんな事言われても笑えないわ。
    ↓
    笑へばいいと思うよ。そんな事言われても笑えないわ。

のようになる。あれれ?これだと最初の「笑え」は置換されるものの、後ろの「笑え」は変換されない。困っちゃったな。`:help substitute@ja` としてヘルプで調べてみると g オプションを付けると良いらしい。早速試してみよう。

Vim では履歴を使えるので、似たようなコマンドを再入力する時に便利なんだよ。「:」を押してから上矢印キー(もしくはCtrl-p)を押してみよう。さっき入力したコマンドが表示されるね(今の場合は `:help substitute@ja` が表示されるので、もう一度上矢印キーを押す)。今度は g を追加して実行してみよう。

:s/笑え/笑へ/g ````

笑えばいいと思うよ。そんな事言われても笑えないわ。
↓
笑へばいいと思うよ。そんな事言われても笑へないわ。

やったね!うまくいった。でも、変換する対象が一行じゃなくて複数行あったらどうすれば良いのだろう?またまたヘルプで調べよう。さっき substitute を調べた時に range という範囲を指定する部分があったような気がするなぁ。

:help range まで入力してから Tab キーで補完すると :help range()@ja になる。とりあえず移動して読んでみたけど、求めている情報じゃなかった。もう一度 :help range まで入力(さっきの履歴を使っても良い)して Tab キーを 2 回押してみると、今度は :help :range@ja になった。読んでみると「%」というのが当りらしいぞ。

ということで例文を 2 行にして頭に % を付けて実行してみよう。

:%s/笑え/笑へ/
笑えばいいと思うよ。そんな事言われても笑えないわ。
笑えよベジータ。笑えない状況になったぞクソッ。
↓
笑へばいいと思うよ。そんな事言われても笑えないわ。
笑へよベジータ。笑えない状況になったぞクソッ。

確かに複数行の置換は出来たけど、各行の後ろの単語は変換されなかった。ヘルプには「ファイル全体」と書いてあるものの、処理は行単位でやるみたいだね。g オプションを付ければ期待した通りの動作になる。

:%s/笑え/笑へ/g
笑えばいいと思うよ。そんな事言われても笑えないわ。
笑えよベジータ。笑えない状況になったぞクソッ。
↓
笑へばいいと思うよ。そんな事言われても笑へないわ。
笑へよベジータ。笑へない状況になったぞクソッ。

よ〜しよし。どんどん要求が増えちゃってアレだけど、「笑え」だけじゃなく「思う」や「言わ」も一発で変換したいんだけどなぁ。ちょっと Vimscript を勉強してみるかな。

歩きはじめて立ち止まる

自分の検索能力のせいかもしれないけど、Web 上には系統立てた Vimscript の解説サイトが少ないみたい。developerworks連載が良さそうだったので、これで勉強することにしよう。もちろん日本語訳版で。

この連載は 5 話まであるのだけど 1 話ですら後半部分は理解不能だった。書いてある内容はわかるのだけど、頭の中に像が浮かばないのだ。3 回読んでぼんやりとしたイメージが出来てきた感じ。

変数への代入に let を使わなければいけないとか、g, s, w 等を使ってスコープを指定するとか変わってるなぁ。関数の名前は大文字で始めないといけない(実はスコープ接頭辞付きの関数うんぬんがあるけど)とか function! ってまんまやんけ〜。ちょっとキモイけどそれがまた良かったりしてね。

っとと、Vimscript のおおまかな把握が出来れば良いと思っていたのだけど、「リスト10.誤用しがちな単語の強調表示」の例を見て何かがひらめいた(ピコーン)。頭の中でハマショーの「終りなき疾走」が奏でられてしまったわけだね(やばい年齢がバレちゃうお)。まぁ背中を押されたという事です。

スクリプトの内容が何をしているのかは当然の事ながら分らないのだけど、例題を「入力して、実行して、体感する」ことで、「こりゃ便利やわ!」と衝撃を受けたのね。で、こんなことを想像した。

  1. キーワードを色付けできるんだね。これを利用すれば「よゐこ」「かほり」のような間違い易いかな使いをチェックすることが出来る。
  2. 色付けする部分を置換することが出来たら便利なんじゃないかな?しかも置換後の文字列に色まで付けちゃうという。
  3. キーワードがスクリプトの中に書いてあるとメンテナンスが大変だなぁ。これ外部ファイルから読み込むように出来ればいいのに。でもどうやって読み込めばいいんだろう?

ちょっとここで補足しておくと、もともと Emacs Lisp で同じようなスクリプトは作ってあって、それを Vim に移植できないものかと頭の中にあったので上のような発想になった。ただそれをどうやって実現すれば良いのか「取っ掛かり」が見付からずに居たわけね。

さて、話を元に戻してっと。リスト10の例題は英文に対しては動くのだけど、キーワードを日本語にして、日本語の文章に対して実行すると何かおかしい。何がおかしいのか忘れちゃったので、その時の日記を引用する。

20 日に読むと言っていた「IBM developerworksVim エディターのスクリプトの作成第 1 回」だけど、その日はそのまま寝てしまったのだった。今日はちゃんと読んだのでその感想をば。
「誤用しがちな単語の強調表示」というサンプルが載っていた。例題では英単語になっているので、キーワードだけを日本語に書き換えて実行してみた。カラースキームの影響もあるのだけど、強調(bold)では平文との差が無いので確認しにくかった。そこで help を読んで調べて、ctermfg=Red のように設定すると赤色ではっきりと識別できるようになった。
こういった短かくて、それでいて実際に動くスクリプトの提示は、初心者にはとてもありがたい。どこかをちょっぴり修正すれば挙動が変わって、ちょっぴり追記すれば機能が追加されるので触っていて楽しいではないか。
雪を握って丸めた。それを転がしてみたら楽しくて、夢中で転がしていたらどんどんと大きくなって、いつの間にか雪だるまが出来ていた。そんな感じかな。子供心はすでに失なっているけどさ。
自分が確認のために使っている sample.txt を開いて実行してみると、期待した通りの色付けがされなかった。カーソルのワード単位での移動を思い出してみれば納得できるのだけど、日本語は句読点、漢字、ひらがな、カタカナなどで区切られている。例えば「笑う」。これは「笑」と「う」に分けて認識されるので、「笑う」ではヒットしない。また、「冗談でしょう」は「冗談」と「でしょう」に区切られるので「でしょう」でヒットして赤色になる。という予想を立てた。
ソースをつらつら眺めていくと match という関数がある。名前からしてこれが本体っぽい。またまた help で調べてみると、pattern という文書の中に含まれていて、これを一通り読めば解決策が見付かりそう。どうやら正規表現の部分にヒントがありそうだ。とりあえず match の部分を読んでみると、pattern は何も難しいことはなく普通に文字列とマッチするみたい。すると上での予想は間違ってるということか。
例題では大文字小文字うんぬんの余分な正規表現が入っているので、これが悪さをしているのかもしれない。c<()> という余分なものを取ってから再度実行してみると、登録した全ての単語に色付けがなされた(join については後で調べる)。
ふむふむ、あとはスクリプトの内部に書かれている単語のリストを外部ファイルから読み込んで作成することが出来れば、今まで作成してきた辞書が無駄にならない。また検索・置換を実行する方法がわかれば、ポーティングも夢ではなさそう。
Vim はモードの切り替えのあるエディタなので、こういった用途にはとても便利かもしれない。でも...。気付いたら泥の雪だるまで、自分は熱を出して寝込んでしまった、なんてことにもなりかねない。やっぱスクリプトまで手を出すのはやめとくべきかもね。二兎を追う者は一兎をも得ず。
Dive Into Vim もしくは Pro Vim のような日本語リソースは見付からない。
2011.10.21

という具合。developerworks にあるソースの著作権がどうなっているのか良くわからないのでここにリストは書けない。だって、引用というレベルを越えた丸パクリ状態(キーワードを変えただけ)なんだもん。改造後のものは興味があれば一番下の付録を見てください。

再び歩きはじめる

上記引用で触れている「外部ファイルを読み込む」部分がわからなくて暫く放置していたのだけど、vim-users.jp の「Hack #39: Vim の戦闘力を計測する」のにあった

let lines = readfile(a:file)

という部分を見て出来そうな予感がした。この書き方って「ファイルを開いて一行ずつ読み込む」定型文とそっくりなんだもん。相変わらず「a:」って何だったっけ?と思いつつも怒涛の勢いで完成させたような気がする。とりあえず変数はすべてグローバルにしてね。

またしても日記の引用をしちゃう。

仮名漢字変換スクリプトの Pure Vimscript 化をしている。外部ファイルから文字列を読み込んで配列や辞書にすることが出来た。変数のスコープが良くわからないので今のところ全てグローバルにしている。この辺は質問でもしてちゃんとしたことを教わらないとマズいと思う。変な癖は早いうちに矯正しておかないと大変なことになるから。
で、間違いやすい単語を色付けする機能は実装することが出来た。置換のほうは辞書の作成までが出来たので、あとは検索置換を実装するだけ。ただ、関数を作る時の仮引数がどうなっているのかわからないので、機能ごとに表示関数を使っているという冗長な作業。RubyPython などと違うみたいで、引数の変数名がそのまま渡っちゃう感じ。これも教えてもらうしかないなぁ。
よしよし、変換後の単語に色はまだ付けられないけど、単語の検索変換だけは出来るようになった。仮引数についてはある程度解決したので、表示関数を削除してスクリプトの行数をそれなりに短かくすることが出来た。
2011.11.25

質問したいとは思っていても結局しなかった。やっぱり質問しにくい雰囲気があるんだよね。初心者を寄せ付けないオーラがあるっていうか。階級が違うっていうか。そういうページを作ろうかという話はあるようだけど、その後の動きはない。やはり初心者の相手をするのは面倒なんだろうね。時間は有限だ。

日付を見てみると外部ファイルの読み込みを解決するまでにひと月かかっていることが分る。この間、Vim Hacks をすべて読んだり、vimtutor を繰り返したり、ヘルプを読んだりしていた。

Web 検索で情報収集もしていたけど、何を探しても必ず thinca さんの「永遠に未完成」がヒットした。例の最速...だけど、あまりにもヒットするので「ひとり StackOverflow かよ〜」と思ってしまった。いやいや良くできてますコレ。

肝心の試行錯誤の部分については記録にも記憶にも残っていないので、お見せすることが出来ない。こういった「どうやって試行錯誤しているのか」という部分が初心者にとっては必要なことで、一番書きたかった部分なのに...記録していなかったことが悔まれる。

こうなったら読者からお題を募集して、その回答をライブコーディングにて配信してもらうしかないね。最近では YouTube での配信も開始したようで、現在は「(」・ω・)」うー(/・ω・)/にゃー 」動画が配信されている。キャスティングについてはコーダーと影のボイス役はすでに決まっているし、エンディング曲を唄う人についても異論はないでしょう。

あとひとつだけ日記の引用をしてこの愚痴を終了しようと思います。

Vimscript 版でも色付けが出来るようになった。自分はほとんど、新 -> 旧 方向でしか使っていないので、色付けも旧かなと旧字体のみに付くようにした。これでかなり便利になったよ。あと、勉強にもなった。
Vimscript でも dictionary 型は順序を保持しない(それが普通か?)ので、意図した変換結果にならないこともありうる。というわけで、辞書型からリスト型を利用する方法に変更した。deepcopy が必要とか、かなりてこずったけど使えるようになった。
きちんと動くようになったので、高負荷(^_^;)にも堪えるかどうか実験してみた。題材は以前に速度調査をした時のものを利用する。結果は下記の通りとなった。

Emacs      130s
Python     346s
Ruby      2834s
Vimscript  746s (ストップウォッチ)

まずエラーも無く完了したことが嬉しい。実際には 3MB ものファイルを目視で編集することは無いので実用上は問題ないでしょう。辞書型でも配列型でも置換にかかる時間はほぼ同じだった。
変換が終るとカーソルが移動してしまうのが何ともね。関数を呼び出す前に位置を保存しておいて、変換が終了したら戻すという方法を取るようかな。cursor(), searchpos(), getpos(), virtcol('.') などが使えるみたい。

let c_point = searchpos('.')
call cursor(c_point)

みたいな感じだね。ダメだ。searchpos() だと取得するポイントがズレてる。

let c_point = getpos('.')
call cursor(c_point[1], c_point[2])

でオケ。
2011.11.26

Emacs と比べて Vim が遅いのはスクリプトのせいなのか、本体のせいなのかわからない。気楽に使えるタイマーってないのかな?ストップウォッチで計測とは何てアナログな事をしてるのだろう。

緑の光の射す方へ

いかがでしたでしょうか?キモイおっさんの独演を最後まで読んでくれた人はいるのかな?ピコーンとなってからひと月以上かかって目的のスクリプトが完成しましたが、なんと時間のかかることでしょうか。でもね、たとえそれがどんなものであっても一つのものを完成させることが出来たのは、自信というか次も出来るんじゃないかという希望になります。

Vim を使っていると不満な点が出てくると思いますが、幸いにして先人による優れたスクリプトが公開されているので、探せば自分の求めている挙動に近いものを見つける事ができます。それでも満足できなければちょっとだけ改造したり、ゼロから作ることにチャレンジしてみましょう。それが他の誰かが欲しているものになるかもしれません。そうやって継承されていくことにより Vim の歴史は作られるのです。

そうは言ってみたものの、やはり初心者からするとソースを見るのが辛い。便利なプラグインというのは、たいてい巨大であったりディレクトリが分かれていて、どのファイルを見れば良いのか分らなかったりします。手の付け所が見つからないんですよね。私もいまだにそうなんですよ。

この文章が Vimscript を書いてみようかなぁと思っていても、その取っ掛かりをつかめないでいる人の一助になれば幸いです。あ、Vim 自体の操作については vimtutor やヘルプで勉強しなければダメですよ。これらは良くできていますので、初級編くらいまでは読んでおきましょう。

実は今まで散々書いてきた「質問することへの抵抗」は思い過しで、自分が勝手に展開していた AT Field なんですよね。というわけで、問題は独りで抱えていないで、ちょっと飛び込んでみようかなぁ。きっと優しく包み込んでくれるに違いない。とナイショ話の部分をフォローをしつつ終了です。う〜、やっぱりオチが無かった...。

付録

" 辞書ファイルの場所を設定
let s:kana_jisyo = fnamemodify(g:mto_dir . '/kana-jisyo', ":p")
let s:kanji_jisyo = fnamemodify(g:mto_dir . '/kanji-jisyo', ":p")
" 辞書フォルダはグローバルで良いでしょう。s: は g: でも良さそうだけど
" 動いているので s: のままでいいや

" 変換辞書を作成(毎回作り直す。二次元配列で再実装)
function! CreateDict(use_jisyo)
    let s:car_cdr_arr = [] " int a=0; みたいなもの。配列だけどね
    let s:cdr_car_arr = [] " 配列はこの関数を呼ぶ度に新しく作成する
    for line in readfile(a:use_jisyo) " 辞書ファイルを開いて一行ずつ読む
        if line !~ '^;.*\|^$'         " コメント等の不要な部分を無視
            let string = substitute(line, ' ;.*', '', '') " 同上
            let pairs = split(string, ' /') " key value の組にする
            " 配列が入れ替ってしまうので deepcopy() しておく
            call add(s:car_cdr_arr, deepcopy(pairs)) " 配列の配列を作成
            call add(s:cdr_car_arr, reverse(pairs))  " 気分は連想リスト
        endif
    endfor
endfunction
" 辞書ファイルは随時更新しているので、コマンドを呼ぶ度に新しく生成させる

" 文字列を置換する部分(ノーマルモード用)
function! ReplaceString(use_arr)
    let cur_point = getpos('.') " カーソルの位置を記憶しておく。あらやだかわいい
    for cons in a:use_arr " 辞書の全要素をナメ尽す。無駄が多いけんども
        execute ':silent! %substitute/' . cons[0] . '/' . cons[1] . '/g'
    endfor
    call cursor(cur_point[1], cur_point[2]) " カーソルの位置を復帰させる
endfunction

" 文字列を置換する部分(ビジュアルモード用)
function! ReplaceRegionString(use_arr) range
    let cur_point = getpos('.')
    for cons in a:use_arr
        execute ":silent! :'<,'>substitute/" . cons[0] . '/' . cons[1] . '/g'
    endfor
    call cursor(cur_point[1], cur_point[2])
endfunction

" 色付けのために単語リストを作成(毎回作り直す)
" developerworks を参考にしたけど、改造してあるからセーフ?
function! CreateColorList(use_jisyo)
    let b:color_list_cdr = []
    let b:color_list_car = []
    for line in readfile(a:use_jisyo)
        if line !~ '^;.*\|^$'
            let string = substitute(line, ' ;.*', '', '')
            let pairs = split(string, ' /')
            call add(b:color_list_cdr, pairs[1])
            call add(b:color_list_car, pairs[0])
        endif
    endfor
endfunction

" 色付けの設定
highlight OLDKANA cterm=bold ctermfg=Darkmagenta guifg=DarkMagenta
highlight OLDKANJI cterm=bold ctermfg=Darkmagenta guifg=Darkmagenta
 ...

" 置換された単語をカラー強調
function! OldKana()
    execute 'match OLDKANA /' . join(b:color_list_cdr, '\|') . '/'
endfunction

function! OldKanji()
    execute 'match OLDKANJI /' . join(b:color_list_cdr, '\|') . '/'
endfunction
 ...

" コマンドをまとめてキーマップで起動させる(冗長すぎるのでまとめたい)
" ノーマルモード用
function! MtoReplaceKanaTrad() " 新仮名から旧仮名へ(色あり)
    call CreateDict(s:kana_jisyo)
    call ReplaceString(s:car_cdr_arr)
    if g:mto_color_flag == 1
        call CreateColorList(s:kana_jisyo)
        call OldKana()
    endif
endfunction
 ...

" キーマッピング
nnoremap <silent> <Leader>t :call MtoReplaceKanaTrad()<CR>
 ...