set me free

ex コマンド、set のテスト。
set [option[=value] | nooption | option? | all]*
単純なコマンドに見えて、意外に奥が深い。

引数がまったくない場合、デフォルトの値から変更されているオプションが表示される。

set

引数が all を含む場合、すべてのオプションが表示される。posix では all が排他的とは定義されていない。つまり、:set report all などと入力すると report の値を表示し、次に全オプションを表示する……ような実装が定義にもっとも素直に従っている。vim もそう動作する。これに対し、wasavi では最初に引数を走査し、all を含んでいたら他の引数は無視し、単に全オプションを 1 度だけ表示する。

set all

オプション名に続き、文字 ? を入力した場合、現在のオプションの値が表示される。? とオプション名の間には 0 文字以上の空白を挟むことができる。2 値タイプのオプションでは、現在のオプション値を表示するのに ? の入力は必須である。2 値タイプ以外のオプションでは、? を付けても付けなくても、代入式でない限り現在の値を表示する。

2 値タイプのオプションが set option の形式で指定された場合、option の値はオンになる。一方、set nooption の形式ではオフになる。2 値タイプ以外のオプションで nooption 形式を指定するとエラーになる。

2 値タイプ以外の、数値や文字タイプのオプションでは set option=value 形式で値を与えることができる。このとき、value 内に空白を含めるには、空白の前にバックスラッシュを置く必要がある。1 文字以上の空白は引数の区切りとみなされる。これにより、単体の set コマンドで複数のオプションをセットしたり表示したりできる。オプション名と = の間には 0 文字以上の空白を含めることができる。

substitute!

ex コマンドの華、s コマンドのテスト。
[addresses] s[/pattern/replacement/[options][count][flags]]

指定された範囲 addresses の各行について、正規表現 pattern にマッチする箇所を replacement で置換する。pattern を囲む区切り文字は / の代わりに \、|、改行、” 以外の非アルファベット文字を用いることができる。\ は区切り文字や \ そのものなどの特殊な文字をエスケープすることができる。

pattern、および replacement の直後の、コマンドラインの末尾となる区切り文字は、省略することができる。pattern と replacement が両方省略された場合、最後に実行された s コマンドが繰り返される。pattern が指定されないか空の場合、vi 全体で最後に使用された正規表現が用いられる。replacement が指定されないか空の場合、pattern にマッチする箇所は空文字列で置換される。replacement 全体が % である場合、最後に s コマンドで使用された置換パターンが用いられる。

replacement 内でのキャリッジリターン(ex モードでは \、open/vi モードでは ^V によるエスケープで入力)は、入力箇所で行を分割してバッファ内に改行を生成することを意味する。キャリッジリターン自体は無視される。

options が文字 g(global を意味する)を含んでいる場合、行内において pattern にマッチする範囲が互いに重ならない形式ですべて置換される。

options が文字 c(confirm を意味する)を含んでいる場合、確認モードになる。pattern にマッチした箇所が画面に現れる。vi はユーザーからの反応を待つ。肯定的な応答(y など)が入力されると置換が行われる。他の入力では行われない。

検索方向が未保存の場合、s コマンドはそれを「前方」にセットする。

s コマンドの亜種として & コマンドがある。& コマンドは
s/pattern/replacement/
として置き換えられる。このとき pattern および replacement は前回の s、&、~ コマンドで使われたものを用いる。

もうひとつの亜種として ~ コマンドがある。~ コマンドは
s/pattern/replacement/
として置き換えられる。このとき pattern は vi 全体で最後に使用された正規表現、replacement は前回の置換(&、~ を含む)で使われたものを用いる。

コマンド終了後、カーソル位置は、置換が行われなかった場合は変化しない。置換が行われた場合は、最後に置換が行われた行の、最初の非空白文字に置かれる。

replacement の書式 

文字 &(magic オプションがセットされていない場合は \&)は、pattern にマッチし置換されるべき文字列を示す。文字 ~(magic オプションがセットされていない場合は \~)は前回の s コマンドで使用された replacement を示す。\n(n は整数)は、pattern で指定される後方参照式に対応するものを示す。n は後方参照式の出現インデックスを示す。対応する後方参照式がない場合、\n は空文字で置き換えられる。

\l、\u、\L、\U は置換文字列内の要素(リテラル、&、\n)の大文字小文字を操作する。\l と \u は後続する 1 文字の大文字小文字を操作する。\L と \U は後続する文字列を \e または \E が現れるか、置換文字列の最後まで、大文字小文字を操作する。

この他の文字はその文字そのものとして扱われる。

……だそうです。

print and list

デフォルトの ex コマンド、print。このコマンドの書式は:
[addresses]print [count][flags]
addresses は前の記事の通り。

コマンド名 print は最短で p まで省略できる。

count が指定された場合、addresses で指定された最後のアドレス + count – 1 というアドレスが仮想的に生成される。つまり、1,2p3 は 1,2,4p とみなされる。アドレスは末尾から必要な分だけ取られるルールにより、最終的に 2 行目から 4 行目が対象になる。

flags は、コマンド実行後にカレント行をコンソールへ表示する際の細かい動作を指定する。+-#lp のいずれかの文字の連なりで指定する。l または p が指定されたとき、コマンド実行後のカレント行をコンソールへ出力する。これらの違いは:

  • p(print): 普通に表示する
  • l(list):
    • 特定の文字(\a、\b、\t、\n、\v、\f、\r、\)はそのエスケープシーケンスの形で表示する
    • 制御文字のような表示できない文字のうち上記にないものは、\ のあとにその文字コードを 3 桁の 8 進数で表示する。ただしタブ文字だけはそのまま表示する
    • 行の末尾には $ を表示する。行内の $ そのものは、\$ と表示する

だいたいこんな感じである。

# は表示する行の前に行番号をつける。

+- は表示される行を上下にずらすためのオフセットを指定する。+ は 1 行下へ、- は 1 行上へ。

ただこれら、特に flags は、vi を vi ではなく ex として使っている場合にはそれなりに役立つのだろうけど、ビジュアルモードでははっきり言ってまったく使う必要はない。vim においても、#lp のみ認識、list 表示は上記の posix の定義には従っていない(その代わり色分けとかはする)などけっこう違いがある。

wasavi は上記の仕様にだいたい従っているが、posix の定義では flags の各文字は空白を挟んでもよいのに対し、必ずひとまとまりにして指定する必要があるという違いがある。めんどくさいので直すつもりもない。まあフラグの間に空白を挟めないと明日おかーさんが死んでしまうんです! なんとかしてください! などと詰め寄られたら考えないではないが……。

map extension #2

map された lhs が rhs へ展開される際、オプション remap が有効な場合、rhs もまた map 対象となる。すなわち、再帰が発生する。
:map! Q Q_key_pressed
などと定義し、テキスト編集中に Q キーを押したりすると、恐ろしいことが起きる。

再帰を収束させるために、vim においてはオプション maxmapdepth の数値が参照される。デフォルトの値は 1000 である。これ以上の再帰は発生しないようになっている。wasavi でもソース上は再帰の上限があり、それ以上は再帰が発生しない。ただしオプションとしては公開していない。値はとりあえず 100 固定。

再帰を発生させない map を行うには、vim では map の代わりに noremap を使う。しかし前の記事にも書いたが、あまりほいほいと ex コマンドを新設したくない。そこで、昨日の記事で考えたシンタックスに則り、

  • remap の発生する(正確には、オプション remap によって動作が変わる)map
    :map! Q Q_key_pressed
  • remap の発生しない map
    :map! [noremap] Q Q_key_pressed

というような書き方にしようと思う。

またもや vimmer から殺意を向けられそうではあるが。

map extension

例によって map コマンドのテスト。

テストといいつつ、新しい機能なのだけど。ピュアな vi が備える map の機能はかなりストイックである。map の一覧を表示するか、新しく定義するかの 2 パターンしかない。

  • map の全定義を削除する
  • すでにマップされたストローク lhs の検索

あたりはあってよいと思うが、ない。まあ map なんて exrc で一括で行うものだろうし、vi にこれらの機能を埋め込むより exec 編集したほうが早いじゃん、ということなのだろう。

さてこういうパターンの場合、vim はほぼ 100% 実装済みだったりする。定義の全削除は mapclear、lhs の検索は rhs を省略した map の実行といった具合だ。

ただ、map の検索はともかく、全削除のためだけに新しい ex コマンドを新設するのはどうなのかなーと思う。なんとか map のシンタックス内で完結したほうがいいのではないか?

map のシンタックスは、:map[!] [lhs rhs] である。lhs と rhs は両方を常に指定しないといけない。lhs と rhs は空白文字列で区切られる。lhs と rhs のそれぞれで空白文字を含める場合は CTRL-V でエスケープする必要がある。

これを踏まえた上で lhs が特別な値であった場合に、rhs を引数として特別な動作をさせたい。なおかつ特別な値というのは、vi コマンド列としてはエラーになるものでなければならない。そうしないと、その文字列に対するマッピングができなくなるので。

ということで考えてみると、\[\w+\] を使えばいいんじゃないかな。vi のコマンドでは [ のあとには [ が続くに決まってる(vim はここでもいろいろ拡張しているが)のだ。それから、lhs と rhs の両方指定制限は取り払う。そうすると、

  • マップの全削除 :map [clear]
  • マップ内の lhs のうち指定の引数で始まるもので検索し表示 :map Q

みたいな感じになる。この例では [ も ] もリテラルであり、そのままこの通り書く。

vimmer には「死ね!」って言われそうだけど、とりあえずこんな感じで組んでみる。

map, map, map

ex コマンドの map をテストしている。map コマンドは、任意のキーストロークをそれに対応するまったく別のストロークへ透過的に変換するためのルールを指定する。

:map[!] [lhs rhs]

あるいは何も引数を指定しない場合、現在定義されているルールの一覧を表示する。vi には 2 つのマップがあり、それぞれコマンドモード、テキスト入力モードで排他的に使用される。テキスト入力モードのマップを対象にするには、map! と指定する。なお vim にはもっといろいろなモード用のマップが用意されているが、wasavi は vi に則って、やはり 2 つのマップだけを備える。

で、これを実装する際の話として。まず仮に lhs、つまり変換元キーストロークが高々 1 キーなら話は簡単である。押されたら変換関数を通して変換する。これが最も簡単なパターン。

次に難しいのは、lhs が複数のキーストロークであった場合。:map qq 3w みたいな。これは、変換関数に状態を持たせるようにすればいい。

次に難しいのは、変換ルールに曖昧なものがあった場合。たとえば、
:map q 1G
:map qq G

みたいな。どうなるか想像できますか? q を押すとバッファ先頭へ、qq を押すとバッファ末尾へジャンプするつもりの定義だ。

このような曖昧な定義に対してどう振る舞うかを、posix は未定義のままにしている。つまり曖昧な定義をエラーにする vi があってもいいし、よきに計らう vi があってもいいということだ。エラーにするのは簡単だが、よきに計らうにはどうすればいいだろうか?

この定義が有効な状態で、まず q を押す。この状態では、ユーザはバッファ先頭へ飛びたいのか? それとも末尾か? は判断できない。そこで、タイマを仕掛け、1 秒後に 1G へ変換されるようにする。つまり q 単体の入力に対する変換が行われるようにする。また、1 秒以内にキー入力があった場合:

  • タイマを仕掛けていたら、それをキャンセルする
  • 新しいキー入力を加えた上で新しい仮変換候補を抜き出す
  • 抜き出した結果、新しいキー入力を加える前の仮変換候補が確定したら、それを実行する
  • そうではなく、新しいキー入力を加えた後の変換候補が 1 つに絞られれば、その変換候補を確定し実行する
  • そうではなく、新しいキー入力を加えた後の変換候補が複数あれば、変換候補の最初のものに対して 1 秒後に確定・実行されるようにタイマを仕掛ける

みたいな感じにする。

addressing in ex #2

若干の補足、あるいはトリビア:

  • 1 以上の整数として行番号を示すことができるが、実は 0 も許される。ただし多くの場合、0 は 1 に丸められる。ではどういう場合に 0 が役立つのかというと、たとえば :10,20co0 のようにバッファ先頭へコピーしたい場合など。copy コマンドで指定するコピー先アドレスは、実際にはコピーされる位置の直前の行番号である。したがって、バッファ先頭である 1 行目へコピーしたい場合は 0 を指定する必要があるのだ。
  • / ~ / の検索開始位置が曖昧な書き方になっていたが、正規表現を明示した場合であっても、省略した場合でも、検索開始位置はカーソルのある行の次の行からである。
  • アドレスは最大 2 つまで指定できるのだが、実はもっと多くのアドレスを指定してもエラーにはならない。単にそれぞれのコマンドが必要とする最大のアドレスが、アドレス群の末尾から取られるだけである。つまり :1,2,3p は 2 行目から 3 行目までが対象となる。
  • 指定の行へカーソルを移動させる場合、visual モードでは [行番号]G と入力するが、ex モードで :行番号 と入力する方法もある。このようにコマンドを省略した場合、内部的には暗黙的に print コマンドが選択される。ということはつまりコンソールに指定の行が出力されるはずであるが(:1p などと入力したかのように)そうならないのは、コマンドを省略したアドレス部分から後続する文字列が \n である場合は特別扱いで何も出力しないからだ。したがって、:||| などと入力すると、現在行が 3 回 出力される(4 回ではなく)。

addressing in ex

ex でアドレスを指定する場合の仕様。アドレス(正確には最大 2 つ指定できるので addresses)というのは
1,10d
などと指定する際のコマンド本体 d までの部分。これを単にカンマ区切りの数値としてパースすればいいんでしょ? などと考えるとやけどする。アドレスとして有効なシーケンスは:

  1. .(ピリオド): 現在行を示す
  2. $: 最終行を示す
  3. 1 以上の整数: 指定の行を示す
  4. ‘x: マーク x が指す行を示す
  5. / ~ /: 現在行から前方に正規表現検索して最初にマッチした行を示す。空の正規表現(//)の場合は、現在行の次の行から前方に前回用いた正規表現検索して最初にマッチした行を示す。2 つめのスラッシュは、コマンドラインの最後の文字である限り、省略可能である。前方に検索して何も見つからず、かつオプション wrapscan が指定されている場合、検索はバッファ先頭から最初に検索を開始した行の前までも対象となる。正規表現内では、\/ はスラッシュそのものを示すためのエスケープシーケンスである
  6. ? ~ ?: 現在行から後方に正規表現検索して最初にマッチした行を示す。その他こまごましたものは / ~ / と同様
  7. -正の整数。指定の数値と現在行を合計して導かれる行を示す。数値は省略でき、その場合は 1 とみなされる
  8. +正の整数。指定の数値と現在行を合計して導かれる行を示す。数値は省略でき、その場合は 1 とみなされる

となっている。さらに、それぞれのシーケンスの後ろに空白、[+-]、正の整数を続けることができ、示された行に対するオフセットとして使われる。空白は省略できる。数値もまた省略でき、その場合は 1 とみなされる。

これらのアドレスを最大 2 つ指定して addresses となるのだけど、その場合の区切りはカンマのほかにセミコロンも許される。セミコロンを指定すると、1 つめのアドレスがいったん現在行にセットされる。2 つめのアドレス算出は 1 つめのアドレスを基準として行われる。つまり、

/foo/;/bar/
はまず foo で検索し、さらに見つかった行の前方にある bar までの範囲を対象とする。

最後に、addresses として指定可能な特別なシーケンス % がある。これはバッファ全体を示す。すなわち 1,$ と同じ意味。

ということになっている。奥が深すぎますよ Bill Joy さん!

ex command test

テスト作業も ex コマンドを残すのみとなった。

すべてのテストを終えたら、各ブラウザの公式のエクステンション配布サイトに公開を申請することになると思う。

ところでそういうサイトに申請する場合ってエクステンションのソースのライセンスってどうなるんでしたっけ? と思い調べてみたら、

ということだ。今のところ wasavi は修正 BSD ライセンスということにしているのだが、Apache 2.0 License にするかもしれない。

本当は NYSL でいいのだが、いかんせん知名度がなさ過ぎる。