Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
7
Help us understand the problem. What is going on with this article?
@kazukimatsumoto

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

More than 3 years have passed since last update.

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

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

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

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

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)ってこと
以上。

補足

まず、タイトルでの「三ヶ月」という保険、大変失礼いたしました。保険にもなっていないでしょうか。少し煽り気味タイトルでみなさんに添削していただきたかったのです。
そう、このスクリプトが好きすぎてどういう仕組みになっているのかが知りたいだけなのです。検索しても誰も丁寧な解説は書いていませんでした。なのでもうみなさん指摘していただけたら指摘していただけただけ僕がすごく満足します。是非是非よろしくお願いします。(マークダウン記法の勉強不足による読みづらさも本当にすみません。徐々に修正していきます。。。)

7
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
kazukimatsumoto
音楽家ですがサーバーサイドも頑張ってます。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
7
Help us understand the problem. What is going on with this article?