丁稚な日々 - apprenticeship days

Rubyで遊んだ日々の記録。あくまで著者視点の私的な記録なので、正確さを求めないように。
Rubyと関係ない話題にはその旨注記しているはず。なので、一見関係無いように見える話題もどこかで関係あるのかもしれません。または、注記の書き忘れかもしれません...

[直前] [Top]

Apr.1,2016 (Fri)

Revision: 1.1 (Apr.01,2016 01:48)

おしらせ

_ 去年出せなかったけど、2年ぶりにオブジェクト指向言語slapの新バージョンがリリースされました!
sampleにあの「ズンドコ」もあるよ!

Dec.11,2015 (Fri)

Revision: 1.4 (Dec.11,2015 20:16)

TRICK2015で審査員賞を頂きました

_ 表題の通りです。ありがとうございました。

_ 受賞プログラムはこんなの。

lines = Array.new
open(__FILE__) do |fl|
  fl.each_line{|line|
    lines.push(line)
  }
  m=15+15
  n=62
  $e = lines.
   map do|ln|
    sz = ln.size
;
    (sz<5?sz+m:sz+n).
     chr().swapcase
  end.join
  $e
  eval "#$e"
end

_ さて、今回は、「見た目はふつーのRubyスクリプト、なんだけど」を自分なりのテーマとしました。
というのは、TRICKには見た目でインパクトのあるプログラムが多数登場することはわかっていたので、その辺のセンスがないぼくとしては逆張りしたほうがいいかな、と。
その上で、幾つか温めていたアイデアのうち、プログラム自体をデータとして利用する、という手法を使うことにしました。
長いプログラムを書くのは面倒だし、TRICK自体もあまり長いのは推奨されない(守るかどうかは別として、4096バイトまで、という規定もある)ので、今回は単純に短い文字列を出力するだけのプログラムにすることにしました。
なので、最終的に実行される真のプログラムは、puts"..."となります。
で、このプログラム(最終プログラム)をどう元のプログラム(元プログラム)に埋め込むか、なんですが、今回は単純にASCIIコードとして埋め込むことにしました。
具体的には、最終プログラムの各1文字を元プログラムの各行に対応させ、その長さを利用するようにしています。

_ ただ、単純にASCIIコード分の長さの行を書くと長いので(例えばpのASCIIコードは112)、適当に変換をかましてもうちょっと実用的な数値になるようにしています。
この変換についてですが、endを元プログラム末尾に置くことは決めていたので、最後の行の長さはendの3文字と改行文字を合わせて4文字となります。
最終プログラムの末尾の文字は"ですので、これが4となる、という基準で変換をかけています。
具体的には、"のASCIIコードは34なので、単に30を引いているわけです。
上に上げた元プログラムの6行目にm=15+15とありますが、これがそれですね。
また、このままだと、先頭のpが112-30で82となり、最初の行がいきなり81文字+改行文字という長いものになってしまいます。
1行の長さが80文字を超えるのはぼくの美意識に反したので、String#swapcaseを使って大文字と小文字を入れ替えることによって、必要な長さを短くするようにしています。

_ で、以上を踏まえて適当に行の長さを調整しながら実行プログラムを書き下ろしていったわけですが、この方法だと、途中に現れる空白文字(スペース)のASCIIコードが32であるため、1文字+改行文字という行を唐突に仕込む必要があることが判明しました。
空白文字は別の文字(,とか)に変えるか、とか、変換式を変えるか、とかいろいろ考えたのですが、ま、謎の行が一つくらい入ってた方が「あれ?」と思わせる効果があるかもな、とか考えて、敢えてそのまま残すことにしました。
どっちかというと、その次の2行が怪しい途中改行を含む変なインデントになってるので、そっちをどうにかしたかったのですが、うまく処理できないままで終わってしまい、「なかだるみ」というご指摘を受けることとなってしまいました。
あと$eしかない行もひどいですね。

_ あと、実はtrunkでしか実行できないコードになっていると指摘を受けたのですが、今確認すると確かにそうですね。すみませんすみません。
投稿直前までは別のコードだったので通ったのですが、最後にぐにょぐにょ微調整した結果がまずかったようです。

_ そんなわけで、いろいろ不備の多い作品となってしまいましたが、意外にも賞をいただくことができました。
審査員の皆様、どうもありがとうございました。

_ (追記)
書き忘れたことが2点ありました。

_ まず、大文字小文字変換をしてもまだアルファベットは1行の長さとしては長いので(今回登場する最短のeでも39になる)、アルファベットの場合は大文字小文字変換した上で30ではなく62を引いています。これでeは7文字(改行文字含む)で表現できるようになりました。
具体的にはまさにそのn=62の行がeに該当します。

_ また、当初はDATAを使ってスクリプトを読もうと考えていたのですが、DATA__END__のないスクリプトでは定義されず、さりとてこんな長い識別子をコードの末尾につけると、必然的に他の行も何文字かずつ長くする必要が発生するので、今回はDATAの使用は諦めました。

Nov.8,2015 (Sun)

Revision: 1.3 (Nov.09,2015 22:53)

大江戸Ruby会議05

_ この文章は翌日に書いています。

_ 大江戸Ruby会議05に参加して、基調講演をやらせていただきました。
なぜぼくだったのかはよくわかんないんですが(本当に某国との闘いが評価された可能性も微レ存)、頼まれた後の経緯は講演内で話したとおりです。
講演内容に関して、幾つか反省点はあるんですが、やっぱりちゃんと気合を入れてフォントにマティスE(B)を使うべきだったかなあ、というのが最大の後悔ポイントです。
でも誰もあのテーマについて一切反応してくれなかったですね……。
なお、なぜかまとめに入れてもらえない模様ですが、スライドは松田さんにお勧めされたSpeaker Deckで公開してあります

_ 自分の発表が一番最初だったので、後は気楽に楽しく他の皆さんの発表を聞かせて頂いたんですが、どれも非常に濃ゆくて大変おもしろかったです。
そして最後が江渡さんの招待講演だったわけですが、聞いてると2006年の日本Rubyカンファレンス(第1回RubyKaigi)のことを思い出したりして、というのはあの時の最後の発表も実は江渡さんのLTだったからで、あー、そういや昔のRubyKaigiってこんなんだったよなー、などと懐かしくなったりもしてたのでした。

_ 懇親会では、ソフトドリンクと一緒に、非常に度数は低いながらもアルコール含有カクテルが並べてあったせいで、間違えてアルコールを摂取してしまい、途中意識を失ったりもしてましたが、こちらもとても楽しませていただきました。
今回聞いた発表の個人的ベストは、懇親会でのakrさんのpow(-1, INFINITY)ネタです(実際にはぼくは休憩時間に事前に教えてもらって既にウケまくってたんですが)

_ 発表者の皆さん、スタッフの皆さん、楽しいイベントを本当にありがとうございました。
そして、ぼくの講演を聞いて笑ってくださった皆さんにも感謝を。

Oct.7,2015 (Wed)

Revision: 1.1 (Oct.07,2015 12:58)

frozen string literalネタ

_ Ruby界を席巻しているfrozen string literalですが、「社会問題」、つまり「.freezeをバカスカ付けてコードを汚すクソパッチ攻撃に耐えられない!」という意見には頷ける面もあるものの、frozen string literalが導入された場合、.dupを付けて回ってコードを汚さないといけないことには人々は耐えられるんでしょうか?

_ そもそも.freezeであればメソッド名が意図を示していますが、.dupが「mutableな文字列オブジェクトがほしいの!」という意味だとメソッド名から読み取れますか?
そういう意味では.dupの方が.freezeより遥かに汚くコードを汚すシロモノなわけです。許しがたし!

_ というわけで、ぼくとしてはこの用途にはString.newを強く推奨するわけですが、この場合は意図は明確であるものの、長い。むちゃくちゃ長い。これでは流行るはずがないですね。
しかし、どーせお前ら速ければなんでもいいんだろ? ああん? という気もするので、.dupに比してString.newが有意に速いならワンチャンあるのではないか、という気もしてきました。

_ というわけで、測定だッ!

# -*- frozen-string-literal: true -*-
require "benchmark"

puts RUBY_DESCRIPTION
N = 10_000_000
Benchmark.bm(16) do |bm|
  bm.report("empty loop") do
    N.times do
      # 空
    end
  end

  bm.report("plain literal") do
    N.times do
      ""
    end
  end

  bm.report(".freeze") do
    N.times do
      "".freeze
    end
  end

  bm.report(".dup") do
    N.times do
      "".dup
    end
  end

  bm.report("String.new") do
    N.times do
      String.new("")
    end
  end
end

_ では結果を見てみましょう。

ruby 2.3.0dev (2015-10-07 trunk 52067) [x64-mswin64_100]
                       user     system      total        real
empty loop         0.484000   0.000000   0.484000 (  0.511896)
plain literal      0.546000   0.000000   0.546000 (  0.538358)
.freeze            0.514000   0.000000   0.514000 (  0.529058)
.dup               3.978000   0.000000   3.978000 (  3.975763)
String.new         2.808000   0.000000   2.808000 (  2.804275)

お、おお? String.new速いじゃん! およそ40%かな。
これは凄い。
そもそもstring literalをfreezeしないといられない人々は速度のためにそういうこと言い出してるわけで、ならば誰もこの用途に.dupを使うことはないだろう、と結論付けてもいいレベルやね。

_ ところで、frozen string literalのマジコメを理解しないRuby、つまり過去のバージョンだと、当然.dupString.newはペナルティを受けるわけですが、それはどんなレベルなのか、ついでに測定してみましょう。

_ まず2.2。

ruby 2.2.3p173 (2015-08-18 revision 51635) [x64-mswin64_100]
                       user     system      total        real
empty loop         0.609000   0.000000   0.609000 (  0.604623)
plain literal      1.419000   0.000000   1.419000 (  1.446351)
.freeze            0.656000   0.000000   0.656000 (  0.654756)
.dup               5.007000   0.000000   5.007000 (  5.035140)
String.new         3.838000   0.000000   3.838000 (  3.871682)

.freezeの最適化が既に入ってることがわかりますね。
.dupのペナルティはplain literalと比較しておよそ250%増し、といったところでしょうか。
一方でString.newであれば170%増し程度で済んでいることがわかります。でもまーどっちもツラいな。

_ 続いて2.1。

ruby 2.1.7p400 (2015-08-18 revision 51632) [x64-mswin64_100]
                       user     system      total        real
empty loop         0.639000   0.000000   0.639000 (  0.647037)
plain literal      1.420000   0.000000   1.420000 (  1.425081)
.freeze            0.671000   0.000000   0.671000 (  0.685040)
.dup               4.883000   0.000000   4.883000 (  4.894280)
String.new         3.790000   0.000000   3.790000 (  3.793217)

概ね2.2と同じですね。
すこーし2.2より.dupString.newがそれぞれ速いようにも見えますが、気のせいでしょうかねえ。

_ 最後に2.0.0。

ruby 2.0.0p647 (2015-08-18 revision 51630) [x64-mswin64_100]
                       user     system      total        real
empty loop         0.639000   0.000000   0.639000 (  0.639037)
plain literal      1.358000   0.000000   1.358000 (  1.389079)
.freeze            1.762000   0.000000   1.762000 (  1.802103)
.dup               4.790000   0.000000   4.790000 (  4.856278)
String.new         3.634000   0.000000   3.634000 (  3.649209)

これには.freezeの最適化が入ってないため、plain literalと比べるとメソッド呼び出しの分遅くなっているのがわかります。
.dupString.newのペナルティは2.2や2.1と同じようなものですね。

_ というわけで、結論。
.dup使うなString.new使え!

Sep.18,2015 (Fri)

Revision: 1.6 (Sep.18,2015 17:06)

『あなたの知らない超絶技巧プログラミングの世界』

_ 著者の遠藤さんから頂いた。ありがとうございます。

_ この本は、えーと……絵画のガイドブックに類するものだと思います。
ただし題材は絵画ではなくてプログラム。つまり、ある種の芸術としてのプログラムの紹介とその解説をしている本、です。

_ 芸術としてのプログラミングは、今までそれに触れたことがない人にとっては縁遠く感じられるかもしれません。
しかし、あなたがプログラマであるならば、試しにこの本を手に取ってみてほしい、と思います。
いつも書いているはずのプログラムが、ちょっと目的と表現方法を変えるだけで、こんなにも驚くべき芸術へと変化するという驚きを、ぜひ味わってください。

_ あ、既にコードゴルフや競技プログラミングとかに手を染めてしまっている人は、無条件で買えばいいと思います。これが世界レベルや!

Dec.17,2014 (Wed)

Revision: 1.5 (Dec.17,2014 01:17)

[短期集中連載「Rubyスタートアップ」] 第17回 Windows用特別処理(その4)

_ 前回でw32_cmdvector()は終わったので、rb_w32_sysinit()(win32/win32.c)に戻ることにする。
残りは淡々とWindows固有初期化処理が続くだけである。

_ tzset()はCランタイム関数で、Unix系OSのtzset(3)と同じである(たぶん)。
環境変数TZがあれば、それをアプリ(ここではRubyインタプリタ)のタイムゾーンとして採用する。
Time.now.zone"東京 (標準時)"とかを返すと「ギャッ!」と叫びたくなる体質の人は、環境変数TZJST-9とか設定しておくとハッピーになれるかもしれない。

_ init_env()は環境変数関連の初期化。
Windowsでは普通設定されていないHOME環境変数・USER環境変数・TMPDIR環境変数を用意している。
HOMEは、もしあればそのまま採用、なければ、環境変数HOMEDRIVEHOMEPATHを繋いだもの、環境変数USERPROFILE(いじってなければHOMEDRIVE+HOMEPATHと同じはず)、ユーザーのプロファイルフォルダ(いじってなければUSERPROFILEと同じはず)、ユーザーのパーソナルフォルダ(現代だとマイドキュメントのこと)、のうち最初に見つかったものを採用する。
USERは、もしあればそのまま採用、なければ、環境変数USERNAME、システムのログインユーザー名(いじってなければUSERNAMEと同じはず)、の順で見つかったものを採用する。
なおここで得られた結果はgetlogin()の結果としても使われる。
TMPDIRは、もしあればそのまま採用、なければ、環境変数TMP、環境変数TEMP、ローカルアプリケーションデータフォルダの中のtempディレクトリ、システムディレクトリ(普通はC:\Windows)の中のtempディレクトリ、のうち最初に見つかったものを採用する。

_ init_func()はRubyインタプリタ内部で割と頻繁に使いたいんだけどOSのバージョンによってはあるかどうかわからない大事なAPI(具体的には現状はCancelIO()のみ)の存在確認。
ちなみにCancelIO()は自スレッド(*1)の指定されたI/O処理を中断するWindows APIである。

_ init_stdhandle()は標準入出力のファイルディスクリプタの初期化。
前に述べたように、汎用的なものは後でfill_standard_fds()で行われるが、Windows版Rubyにはrubyw.exeという生まれながらにして標準入出力を持たない版が添付されているので、もし標準入出力がオープンされていないなら、NULデバイスを結び付けている。
これをやっておかないと、スクリプトが標準出力に何か出力しようとしたり、警告などが標準エラー出力に出力されたりすると、当然出力先がないので例外が発生するのだが、その例外もどこにも出力できないので、はたから見るとrubyw.exeが黙って突然死したように見えてしまう。
結果、ユーザーの皆さんから「なんじゃこりゃゴルァ!」と怒られることになるので、それを避けるためである。

_ atexit()はC標準関数でプログラム終了時に行われる処理を登録する関数。
実際に登録しているexit_handler()では何をやっているかというと、Windows版固有のリソースの解放である。

_ 最後に、Windowsではソケットを使用する際には事前にソケットライブラリ(WinSock)の初期化処理が必要なので、StartSockets()関数を呼んでその中でそれを行っている。
require "socket"しない限りソケットは使えないはずだから、これはsocket拡張ライブラリの初期化処理にやらせればいいんじゃない? と思われるかもしれないが、例えばRubyインタプリタ内部でカジュアルに呼び出されてるselect(2)なんかはWindowsにおいてはWinsock提供機能の一つだとかいうことがあるので、もうソケットは使うものだとみなして最初に初期化しちゃっている。

_ というわけで、rb_w32_sysinit()はこれでおしまいである。

_ 後は、process_options()の中でちょっと触れた、コマンドライン文字列のエンコーディングについて。
これがlocaleエンコーディングになったりdefault externalになったりすることについて悩ましいという話をしたが、以上見てもらったとおり、WindowsではいったんUTF-8としてコマンドライン文字列を取得している。
その上で、process_options()の中でlocaleエンコーディングなりdefault externalなりに変換をしている。
マクロUTF8_PATHが有効の場合の処理がそれである。

_ 昔々、つまりRuby 1.9.3以前は素のコマンドライン文字列取得の時点でWではないAPI、即ちGetCommandLineA()を利用していた。
当然出力はCP_ACP(ANSIコードページ、概ねlocaleエンコーディングみたいなもの)である。
Ruby 2.0.0以降はGetCommandLineW()を利用するようにはなったが、rb_w32_sysinit()から返されるコマンドライン解釈結果はCP_ACPのままだった。つまり、w32_cmdvector()の中でWide StringからCP_ACP文字列に変換していた。
整理すると、Ruby 1.9.3以前ではRubyインタプリタ実装に入る前、OSの中でこの変換が行われており、2.0.0ではRubyインタプリタの内部、rb_w32_sysinit()の中でこの変換が行われるようになり、trunkではさらに後のprocess_options()の中で変換されるようになったわけである。
Rubyの版が進むにつれて徐々に変換位置が後ろになっていっているのは、Rubyインタプリタでこの変換処理を制御することによって、不具合を取り除き、よりユーザーにとって扱いやすいようにしようという努力の現れである。

_ で、そもそも、なぜ変換を行うのかというと、互換性のためである。
要するに、Windowsでは(というか他のプラットフォームでもそうだが)昔からコマンドライン文字列はlocaleエンコーディングで与えられていたため、スクリプトレベルの互換性を維持するためにはそれを容易には変更できなかったのだ。
だが、Windowsの内部では結局全部Unicodeで取り扱っているわけで、いちいちlocaleエンコーディングに変換するのも馬鹿らしい話ではある。
互換性を多少壊してもいいタイミング(次回はRuby 3.0だろうか)で、このあたりは全部UTF-8になるようにして、仕様をすっきりさせたいところである。

_ と、いうわけで、Windows版特殊処理の解説もこれで終了である。
そして、長かった本連載も今度こそ本当にこれにておしまいとなる。
まさか全17回にもなるとは、書き始めた時には思いもしなかった……。

_ この連載を書いた理由は、「あれ、ひょっとして、人類で現在のRubyのスタートアップコードの中身を理解してるのって私だけ(*2)なんじゃね?」という怖いことを思ってしまったためであった。
たぶんこの連載のおかげであと10人くらいは理解してる人が増えただろう、と思いたい。
いや10人くらいは最後まで読んでくれたよね? よね?
では、その10人くらいの皆さん、長々とお付き合いありがとうございました。

付記

(*1) 自スレッド
この一言に深い闇が凝縮されているが、今回は省略。

(*2) 私だけ
さすがにそれはないとも思ったのだが、Ruby 1.8後に行われた数々の変更、特にエンコーディング絡みの処理や、Windowsで何をやってるのかということまで含めると、あながち冗談でもなさそうで……。
なお非人類なら私より詳しいのが一匹存在する。

Dec.16,2014 (Tue)

Revision: 1.20 (Dec.16,2014 01:57)

[短期集中連載「Rubyスタートアップ」] 第16回 Windows用特別処理(その3)

_ さて、rb_w32_sysinit()からw32_cmdvector()の実際のコード(win32/win32.c)をいよいよ見ていくことにする。

_ w32_cmdvector()を呼び出す際には、引数として、GetCommandLineW()の結果、CP_UTF8rb_utf8_encoding()の結果、を渡している。

_ まずGetCommandLineW()WindowsのAPIだが、コマンドライン引数をWide Character列(Windows用語ではWide String)のポインタとして返す。
Wide Characterとは、簡単に言っちゃえばUnicode、もう少しだけ深く言えばUTF-16LE(UCS-2という説もあり)な「文字」のことである。Cでの型はWCHARで、符号なし16bit整数。
で、その列であるWide StringというのはつまりいわゆるC文字列(型はchar *)のWide Character版であり、型はWCHAR *ということになる。
勘のいい人は気づいたと思うが、GetCommandLineW() APIの戻り値がWCHAR **ではなくてWCHAR *ということは、つまり、ここで取得されるのは要素ごとに分割されたargv形式の配列ではなく、実際にコマンドラインでユーザーが指定した文字列そのものである。
つまり、コマンドラインの分割処理もWindowsでは本来は各アプリの責務なのである。
もちろん、普通はCランタイムのスタートアップコードがargvに分割してくれてはいるし、CommandLineToArgvW()というAPIもあるのでそれを使うこともできるのだが、Rubyでは独自の追加ルールを用いてコマンドラインを分割するので(具体的には単一引用符(')の扱い)、これらは利用しない。

_ 次のCP_UTF8というのは、コマンドライン解析結果として期待されるエンコーディングの指定である。
CP_UTF8の意味はUTF-8コードページなのだが、コードページというのは、えーと、Windowsにおけるエンコーディング情報みたいなもの、ということでいいかな。詳細はMSDNで

_ その次のrb_utf8_encoding()はRubyのUTF-8エンコーディング情報を返す関数だが、これもやっぱり解析結果として期待されるエンコーディングの指定としてw32_cmdvector()に渡している。

_ しかし、なんでUTF-8コードページとRubyのUTF-8エンコーディング情報という2つのエンコーディング情報を渡す必要があるのだろうか?
まず、Rubyインタプリタとしては、コマンドライン解析結果は当然ながらRubyの理解できるエンコーディングの文字列で頂きたい。
しかし、この時点ではエンコーディングライブラリ本体がまだ初期化されていないので、Rubyは元ネタのWide String、つまりUTF-16LEからUTF-8への変換方法を知らない。
Windowsはもちろんその方法を知っている(APIがある)が、WindowsはRubyのエンコーディング情報を理解しない。
というわけで、WindowsにUTF-8に変換させるためにコードページが必要であり、また一方で変換してできた文字列がUTF-8であることをRubyに教えるためにRubyのエンコーディング情報もまた必要となるわけである。

_ あれ、この時点でRubyのエンコーディング情報も存在しないんじゃないの? と思ったあなたは鋭い。
実は全くその通りで、Init_Encoding()Init_String()もまだ呼ばれていないのだからRubyのStringオブジェクトとしてエンコーディングをいじるような機能はまだ使えない。
しかし、例の3つの組み込みエンコーディングの実装自体は(何しろ組み込まれてるんだから)すでに存在する。
なので、StringオブジェクトとしてではなくCレベルの文字列として操作する分には、その文字列のエンコーディングが何か、ということさえわかっていて、StringクラスのAPIを経由したりせずに組み込みエンコーディングの実装に含まれるプリミティブな関数を呼び出したりする範囲内であれば問題ない(*1)のである。

_ ……なんかここまででいつもの長さくらいになっちゃってるのだが、前回はコード読んでないし今回はまだほんの1行を説明しただけなので、心を鬼にして突き進むことにする。

_ それでは、w32_cmdvector()だ。
長いよー。難しいよー。
なので、読者にわかるように、仕様をもって処理の概要を説明してしまおう。

_ この関数でしないといけないことは、以下の通りである。

  1. 1個の文字列を、argv形式、つまり文字列の配列に分割しなければならない。
  2. 入力はWide Stringだが、出力は引数で指定されたエンコーディング(ここではUTF-8)の配列になっていなければならない。なお各要素の型はchar *である。
  3. ワイルドカード文字を含む要素はglobbingして展開しなければならない。
  4. 引用符の類はそのまま渡ってきているので、適切に処理しなければならない。
  5. Windowsユーザーのための処理なのだから、ユーザーの混乱を避けるため、Ruby独自の便利機能以外は、なるべくOS標準のコマンドライン分割処理に準じなければならない。
  6. Ruby独自の便利機能とは、独自メタ文字の展開、および、単一引用符(')も引用符として扱うことを指す。
  7. Cランタイムのワイルドカード展開処理では二重引用符でくくられた部分にあるメタ文字も容赦なく展開されるが、Ruby独自の便利機能では二重引用符については同様とするが、単一引用符にくくられた場合は展開を抑止するものとする。

_ OS標準のコマンドライン分割処理について公式ドキュメントに付け加えるとすると、二重引用符が連続した場合の扱いだろうか。
というわけでクイズ。以下のコマンドライン引数(の一部)は、Cランタイムスタートアップ処理による分割処理後にどのような文字列となるだろうか?

  1. "
  2. ""
  3. """
  4. """"
  5. """""
  6. """"""
  7. """""""
  8. """"""""

_ 正解は、それぞれ、

  1. 空文字列
  2. 空文字列
  3. "
  4. "
  5. "
  6. ""
  7. ""
  8. ""

である。
なんでかって? 実は公式のドキュメントには(見落としでなければ)書かれていないが、3つの連続した二重引用符は二重引用符1文字(引用符の開始/終了ではなく)とみなし、それを踏まえた上で二重引用符が1個だけ余るケースは単に引用符の開始または終了とみなす、という挙動になっているからである。
なお1個の場合は引用符の開始(または終了)、2個の場合は当たり前だが引用符を開いて閉じただけ、となる。
なお最終的に引用符が開始されたけど終了してない場合は、末尾で閉じ忘れただけといなされる。
Rubyの場合は、単一引用符もこの挙動の対象とする。

_ あと、OS標準では、ドキュメントにあるように、バックスラッシュ(フォントによっては円記号、\)の直後に二重引用符が来た場合はこの一組で二重引用符という1文字(引用符の開始/終了でなく)として扱い、それ以外の場合にはバックスラッシュはバックスラッシュという文字として扱う、というルールもある。
これを演繹して、バックスラッシュの次のメタ文字はglobbingの展開対象せずその文字そのものとして扱う、とかいう風に拡張したくなるのが人の常であるが、Windowsではバックスラッシュはパス区切り文字という重要な役割があるので、さすがにそれは採用できない(例えばC:\*が期待と異なる意味になってしまう)。
なので、あくまで引用符文字そのものを表す場合(つまりRubyの場合は二重引用符だけでなく単一引用符も対象)にのみバックスラッシュによるエスケープを有効とすることにする。

_ なお、ちょっと奇妙な仕様として、これもドキュメントにあるように、2n+1個のバックスラッシュの次に二重引用符が来た場合は、n個のバックスラッシュと1個の二重引用符とされる。
2n個のバックスラッシュの次に二重引用符が来た場合はn個のバックスラッシュになる(二重引用符はくくりの開始または終了とみなされる)。
Rubyでは、単一引用符もこの奇妙な挙動の対象とする。
ちなみに、バックスラッシュの後に二重引用符が連続しない場合、バックスラッシュの数が減らされたりはしない。

_ 仕様はこれで理解してもらえたと思うので、あとは難しいコードはあまり読まずに、処理の中身を文章でざっくり説明して終わることにする。

_ まず、説明した仕様のとおりに、引用符を意識したコマンドライン分割処理を行う。

_ 分割処理が終わったら、この時点での分割後の各文字列をWide Stringから指定のエンコーディング(UTF-8コードページでしたね)に変換する。
前に説明したように、RubyはWide StringからUTF-8への変換方法を(この時点では)知らないが、Windowsは当然知ってるので、この変換にはRubyのAPIは使わずにWindowsのAPIを使う。
rb_w32_wstr_to_mbstr()内で使用しているWideCharToMultiByte()それである。

_ こうしてできたそれぞれのC文字列(リストになっている)について、メタ文字が含まれていてかつ単一引用符の中ではない場合、globbingを行う。
そのためにcmdglob()関数を呼んでいるが、この中でバックスラッシュ(\)をスラッシュ(/)に変換している。
Windowsのルールではバックスラッシュはパス区切りだが、これから使うRubyのglobbing機能であるruby_brace_glob_with_enc()はバックスラッシュをエスケープ文字として扱ってしまうためである。
てなわけで、ruby_brace_glob_with_enc()(dir.c)を呼んでglobbing結果を受け取って、C文字列リストに挿入する。
ruby_brace_glob_with_enc()の中身はさすがに説明していくのは辛いし、本稿の範囲を超えると思うので省略。

_ いちおうこれで終りといえば終りなのだが、最後に出来上がったC文字列配列を連続したメモリ領域に配置しなおしたものに置き換えている。整理整頓ですな。

_ うん、コードは難しいんだけど、仕様がわかっていて文章で説明する分には問題ないよね。
いやしかし今回は長かった。
そして次回はようやく最終回!

付記

(*1) 問題ない
と言っておいてなんだが、私たちRubyコア開発者もよくそのことを忘れて痛い目にあっている。
エンコーディングライブラリ初期化前にRubyレベルのエンコーディング操作メソッドを呼ぶ処理を入れちゃってRubyインタプリタを動かなくしちゃった、というコミットが時たま発生している。
踏むのはたいていWindowsだけなので仕方ない面もあるのだが、普段Windows版を見てるはずのコミッタがやらかしたこともあるので(実は私も一度やった)、皆さん気をつけよう。


被捕捉アンテナ類
[Ant] [Antenna-Julia] [Rabbit's Antenna] [Ruby hotlinks]