Edited at

ruby勉強して三ヶ月たったので"絵文字を降らせるスクリプト"を頑張って解読した

More than 1 year has passed since last update.

scivolaさんに全面的に指摘していただいたのを元に大幅修正しました。本当にありがとうございます!


ターミナルで絵文字を降らせるスクリプト

http://melborne.github.io/2013/12/13/translate-let-it-snow-in-the-terminal/

このスクリプトが好きすぎていろんな友達に教えてます。好きすぎてどんな仕組みなのか知りたくなってきました。調べよ。


これちょっと初心者にはめっちゃむずいんすけど

ruby -e 'C=`stty size`.scan(/\d+/)[1].to_i;S=["2743".to_i(16)].pack("U*");a={};puts "\033[2J";loop{a[rand(C)]=0;a.each{|x,o|;a[x]+=1;print "\033[#{o};#{x}H \033[#{a[x]};#{x}H#{S} \033[0;0H"};$stdout.flush;sleep 0.1}'

どうやら組み込み関数っぽくやってるっぽい。そういうのは勉強してない。でもやる。

まず横に長すぎなので、ワンライナー全体を

ruby -e ' '

' ' の中身とに分けて考え,中身を整理する。

C = `stty size`.scan(/\d+/)[1].to_i

S = ["2743".to_i(16)].pack("U*")
a = {}
puts "\033[2J"
loop{
a[rand(C)] = 0
a.each{ |x,o|
a[x] += 1
print "\033[#{o};#{x}H \033[#{a[x]};#{x}H#{S} \033[0;0H"
}
$stdout.flush
sleep 0.1
}

|x,o|後の;は余分なので削除。だいぶ見やすくなった。。。


行毎の解説

上から調べて行く。
一番最初の

ruby -e

は「スクリプトファイル名を取らずに実行する」ということ。つまり〜.rbみたいにスクリプト名を入力せずにrubyで実行しますってこと。

https://docs.ruby-lang.org/ja/latest/doc/spec=2frubycmd.html#cmd_option

中身に入る。

1行目はCに色々代入されている。

まず最初に`stty size`はなんなのか。

https://docs.ruby-lang.org/ja/2.4.0/method/Kernel/m/=60.html

ここらへんをみると、``部分はKernelモジュールというもので、「``の中身を外部コマンドとして実行し、その標準出力を文字列として返す」とある。

これだけじゃわからなかったので

https://www.google.co.jp/amp/s/techracho.bpsinc.jp/hachi8833/2017_02_08/35090/amp

ここをみると、Kernelモジュールはモジュール関数と言われていて、Objectクラスに含まれている(インクルード)。つまり「全てのクラスから参照できて」「メソッドの再定義に用いられる」らしい。実際、殆どのメソッドはこのクラス出身。ルールとして.Kernelは要らずnewも要らんらしい。

ちなみにモジュール関数とは「プライベートメソッドであると同時に モジュールの特異メソッドでもあるようなメソッド」とのこと。

この説明絶対別ページのが良かったな、、、

話を戻すと、stty sizeの部分は,「stty size を端末のコマンドとして実行してその標準出力を得る,ということ」

いやでも結局sttyって何やねんって話だが、これはどうやら「端末の出力とかに関する」コマンドらしい。それにオプションでsizeとやると端末 (ウィンドウ) のサイズを、標準出力に表示することができる (最初に行、次に桁)。つまり「画面サイズはかってる」のね。

https://www.ibm.com/support/knowledgecenter/ja/ssw_aix_72/com.ibm.aix.cmds5/stty.htm

ここまでですごく長くなってしまった。

残りを順番に見て行くと、.scanメソッドはgsubメソッドのように、「引数で指定した正規表現のパターンとマッチする部分を文字列からすべて取り出し、配列にして返す」メソッド。(gsubのように置換は行わない)https://ref.xaio.jp/ruby/classes/string/scan

正規表現のパターン早見。https://qiita.com/agotoh/items/87960d256e427d5673a9

(/\d+/)[1]を読み解くと、\dは「0-9までの数字」で+は「1回以上の繰り返し (のうち最長)」なので、「stty sizeによって出力された数字(=画面サイズ。[1]なので桁)を配列にして返している」

irbで確認すると

>> `stty size`

=> "12 78\n"
>> `stty size`.scan(/\d+/)
=> ["12", "78"]
>> `stty size`.scan(/\d+/)[1]
=> "78"
>> `stty size`.scan(/\d+/)[1].to_i
=> 78

その通りになった。

2行目のSについてだが、まず.packというメソッドは「引数に入れたフォーマットに従って、レシーバの配列からバイト列を作成して文字列を返すもの。

全然なんのことだかわからんのでバイト列を調べる。

バイト列http://e-words.jp/w/%E3%83%90%E3%82%A4%E3%83%88%E5%88%97.html

バイナリ列のことで、バイナリ列ってのはいわゆる“パソコンで使えるデータ“のこと。http://wa3.i-3-i.info/word1146.html

テキスト以外のデータのことを指すこともあるらしい。1バイトのデータの塊なのでバイト列。

つまり、packってのは引数の指示によってレシーバになってる配列の中身をバイト列に直して、それをさらに翻訳して文字列に直してるってことがわかる。あとは、引数でのフォーマットの決め方がわかればいいが、https://docs.ruby-lang.org/ja/latest/doc/pack_template.html

こんな感じでテンプレート文字ってので決まっているらしい。ここによると“U”はUTF-8(つまり普段使ってる文字コード。符号化文字集合をコンピューター上で扱えるように数値変換するやつ)を表していて、*は“残り全て“を表す。どういうことなのかというとテンプレート文字は後ろに数字を続けることで“適用させる文字数“みたいなのを決めれるっぽくて、*ってやるとそれ全部ってことになるらしい。

つまり、ここまで読み解いて2行目を読み直すと、「配列の中身“2743”を16進数の整数に直したやつを、UTF-8でバイト列に変換して文字列に直したものをSという定数に入れる」という意味。そして、この“配列に入っている数字が「装飾記号」というエリアの文字“を表している。
http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/u2700.html

(まあ、ここまでいろいろ調べて見たけど、こんな難しいことをする必要はなくてS = "\u2743”でいいみたい。)

3行目はaっていうハッシュを定義してる。誰でもわかる

4行目は“\033[2J”っていうのを出力している。これは“エスケープシーケンス“というのを表していて、「ターミナルを制御(カーソル位置や色など)する制御文字」をあらわしているらしい。制御文字ってのは「ESC 」(: 8進数で033 から始まる文字列)。これ打ち込むとターミナル動かせるってことや

http://7ujm.net/etc/esc.html

んで、これによると\033[2Jってのは“画面クリア“を表している。実際にやってみたら今まで入力してたのが消えたので、「何かを降らせるために画面を掃除した」って感じ。

5行目から終わりまで、loopメソッドによって無限ループがスタートする。

まずa=[rand(C)]=0を見ると、aっていうハッシュの中にCの中の整数をランダムに入れている。そして、それに0を代入していて初期化している。つまりどういうことなのかというと、“aというハッシュの中の、rand(C)で出た数字(これの数字がキーになる)の値を0にする“ということ。rand(C)が3だったらa={3=>0}ってこと。初期位置を一番上に決めてるのかな?

置いといて次に進む。さっきのハッシュaをeachで回している。ハッシュなのでブロック変数はキーと値の二つ。a[x]を+1してるので値が増えている。多分これでeachを回すたびにどこから装飾記号が降るか決めているような。

その次はprintで

print "\033[#{o};#{x}H \033[#{a[x]};#{x}H#{S} \033[0;0H"

これがまさに装飾記号を降らせている。これら全部5行目と同じ“エスケープシーケンス“なので、おそらくaの値によって装飾記号を動かしていると予想。

調べると、http://d.hatena.ne.jp/foussin/20140503/1399122536

[y;xH :カーソルを y行 x列に移動する(左上を原点とする絶対座標で指定)

(Perlで15行 10列に移動するなら「 print “\e[15;10H”; 」と記述。)らしいので、

区切りは

"\033[#{o};#{x}H"

"\033[#{a[x]};#{x}H#{S}"
"\033[0;0H"

と、oとxの値でxy座標を色々動いていることがわかる(#{S}はまさに「装飾記号」)。んで最後に原点に戻っとる。

・・・っていうのをeachで繰り返していると。むずい。

ラスト。$stdout.flushと書いてあるがなんのことだか全く分からん。ググると

https://docs.ruby-lang.org/ja/latest/method/Kernel/v/=3e.html

ここに「標準出力です」との一文が。標準出力についておさらいすると「UNIX系での単なる出力、その通り道」のこと。

http://uxmilk.jp/43259

そしてflushだが、どうやら「何らかの原因で標準出力されない時に出力するよう指示をだす」メソッド。https://hydrocul.github.io/wiki/programming_languages_diff/io/flush.html

保険みたいな感じ。

つまり要約すると「今までのをターミナルに出力します」ってこと!クソシンプル。

ついにラスト。ここでloopメソッドが閉じてる。

sleep 0.1

これは単純に「引数に指定した秒数分処理を止める」メソッド(https://www.sejuku.net/blog/16187)

つまりsleep(0.1)ってこと

以上。


補足

まず、タイトルでの「三ヶ月」という保険、大変失礼いたしました。保険にもなっていないでしょうか。少し煽り気味タイトルでみなさんに添削していただきたかったのです。

そう、このスクリプトが好きすぎてどういう仕組みになっているのかが知りたいだけなのです。検索しても誰も丁寧な解説は書いていませんでした。なのでもうみなさん指摘していただけたら指摘していただけただけ僕がすごく満足します。是非是非よろしくお願いします。(マークダウン記法の勉強不足による読みづらさも本当にすみません。徐々に修正していきます。。。)