eval.rb を読む

eval.rbを読む

ruby を使いこなすためには他の人が書いた Ruby のスクリプトを読むのが一番です。ruby 1.6.4 にはたくさんのサンプルスクリプトが添付されていますが、そのなかから eval.rb を取り上げてみたいと思います。eval.rb は次のように使います。

$ ruby eval.rb 
ruby> for i in 1..3
ruby|   puts i
ruby| end
1
2
3
1..3
ruby> 

実行例から分かるようにこのスクリプトは irb (Interactive Ruby) のプロトタイプです。eval.rb のスクリプトの全文は次のようになります。

line = ''
indent=0
$stdout.sync = TRUE
print "ruby> "
while TRUE
  l = gets
  unless l
    break if line == ''
  else
    line = line + l 
    if l =~ /,\s*$/
      print "ruby| "
      next
    end
    if l =~ /^\s*(class|module|def|if|unless|case|while|until|for|begin)\b[^_]/
      indent += 1
    end
    if l =~ /^\s*end\b[^_]/
      indent -= 1
    end
    if l =~ /\{\s*(\|.*\|)?\s*$/
      indent += 1
    end
    if l =~ /^\s*\}/
      indent -= 1
    end
    if indent > 0
      print "ruby| "
      next
    end
  end
  begin
    print eval(line).inspect, "\n"
  rescue ScriptError, StandardError
    $! = 'exception raised' unless $!
    print "ERR: ", $!, "\n"
  end
  break if not l
  line = ''
  print "ruby> "
end
print "\n"

それでは一行目から読んでいきましょう。最初は、文字列変数 line を空文字列に、整数変数 indent を 0 に初期化します。line は コンソールから入力する Ruby の文を納めておくためのものです。文が完成したら eval( line ) で文を実行します。また、indent は文の中に for や if などのキーワードによるブロック構造が発生したときのループの深さを保持します。indent = 0 で文が終了すると line に納められたスクリプトが実行されます。

line = ''
indent=0

$stdout は、IO クラスの事前定義変数で標準出力をあらわしています。IO#sync= メソッドは出力同期モードを true または false にセットします。出力同期モードが真のときは、すべての出力が即座に下層のオペレーティングシステムにフラッシュされ、Ruby で内部的にバッファリングされません。

$stdout.sync = TRUE

つぎにプロンプト ruby> を表示します。

print "ruby> "

while 以下がメインループになります。while TRUE ですから無限ループです。ループから脱出するときは break を使います。

while TRUE

メインループ内ではまず標準入力から変数 l に一行入力します。

  l = gets

l に何も入力されていないときは、コンソールから Ctrl-d が押されたときですが、line に何も入っていなければメインループから抜け出して eval.rb を終了します。

  unless l
    break if line == ''

else 以下が入力があったときの処理です。まず line に l の内容を追加します。

  else
    line = line + l 

l の末尾がコンマのあと空白文字(空白、タブ、改行)しかないときは、文が終了していないことを示すプロンプト ruby| を表示して、メインループの先頭の l = gets の行へとびます。next は最も内側のループの終りにジャンプするので、結局ループのつぎの繰り返しをはじめることになります。要するに文がコンマ(,)で終っていると文が終了していないと判断していることになります。

    if l =~ /,\s*$/
      print "ruby| "
      next
    end

次の if 文では l の文頭が class や if など文の終りに end を必要とするキーワードがある場合の処理を行います。これらのキーワードがあると end が来るまでブロック構造の階層が一つ深くなるので indent の値を一つ増やします。

    if l =~ /^\s*(class|module|def|if|unless|case|while|until|for|begin)\b[^_]/
      indent += 1
    end

次の if 文では l の文頭に end が来たときの処理を行います。end が来るとブロック構造が一つ浅くなるので indent の値を一つ減らします。ただし、end_ の形の場合は end キーワードではないのでこの処理をしません。

    if l =~ /^\s*end\b[^_]/
      indent -= 1
    end

次の if 文では、{ かまたは、{ | string | で文が終っているときの処理で、ブロック構造の階層を一つ深くします。

    if l =~ /\{\s*(\|.*\|)?\s*$/
      indent += 1
    end

次の if 文では l の先頭に } がある場合の処理を行い、ブロック構造の階層を一つ浅くします。

    if l =~ /^\s*\}/
      indent -= 1
    end

次の if 文では最終的にブロック構造の階層をチェックし、階層が 0 でなければ、入力の追加を促すプロンプトを表示します。next で終っているのでその後の処理をとばしてメインループの先頭に戻ってしまいます。

    if indent " 0
      print "ruby| "
      next
    end

次の end は 8 行目から始まる else ブロックの終りです。

  end

次の begin から end までのブロックで line の内容を実行します。begin 〜 end ブロック内のコードの実行で発生したエラーはその中に記載された、rescue 節によって補足することができます。この場合は ScriptError と StandardError を rescue 節で補足します。つまり、print eval(line).inspect, "\n"で実行時エラーが発生すると、$! に納められたエラー情報が rescue 節に渡されます。rescue 節では $! にエラー情報がない場合は 'ecxeption raised' を $! に格納します。そうして、ERR: で始まるエラーメッセージを表示します。

eval(line)では、line の内容が実行され、戻り値が戻されます。eval(line).inspect で戻り値として戻されたオブジェクトの情報を表示することができます。

 
  begin
    print eval(line).inspect, "\n"
  rescue ScriptError, StandardError
    $! = 'exception raised' unless $!
    print "ERR: ", $!, "\n"
  end

line の実行後に l が空であればメインループを脱出します。スクリプトの入力を途中で中断した場合の処理です。

  break if not l

最後に line の内容を空にしてプロンプトを表示しメインループの先頭にもどります。

  line = ''
  print "ruby> "
end

メインループから抜け出た場合の処理です。改行してシェルに戻ります。

print "\n"

eval.rb の機能を拡張する

eval.rb は ruby を対話的に利用できるので、プログラム開発や Ruby の使い方の勉強には便利ですが、編集機能が貧弱です。そこで eval.rb の中から vi をシームレスに使う機能を追加してみましょう。次のスクリプトを eval.rb の語句解析のループの最後の行に追加します。

.........
    if indent " 0
      print "ruby| "
      next
    end
(ここから追加部分です)
    if l =~ /^\s*vi\s*$/
      line = "system 'vi temp.rb'"
    end
    if l =~ /^\s*run\s*$/
      line = ""
      IO.foreach("temp.rb") {|ln| line += ln}
    end
(ここまでが追加部分)
  end
  begin
    print eval(line).inspect, "\n"
........

上の改造プログラムで IO.foreach という見慣れないメソッドがありますが、これは、IOクラスのイテレータで引数にファイル名をとります。このメソッドが実行されると引数のファイルが開かれて一行ずつコードブロックの変数 ln に代入されます。ln は line の後ろに追加されます。この操作を一行ずつくりかえし、ファイルの終りまで続けます。ファイルの全ての行が読まれてしまうと、ファイルがクローズされます。このようにイテレータを使うとループのある処理を簡潔に表現することができます。プログラムを組む方からみると、ファイルの読み書きをする小さな部品があるようにも感じられます。このように、部品を組み立てる感じでプログラムを作っていけるのが Ruby の面白いところです。

eval.rb の改造後は ruby> のプロンプトが出ているときに vi とだけ入力すると vi の編集画面となり、編集を終了して vi を抜けると ruby> のプロンプトに戻ります。次に ruby> プロンプトで run と入力すると編集したプログラムが実行されます。

$ ruby eval.rb
ruby> vi

(vi の編集画面になります。)
for i in 1..3
  puts i
end
~
~
~
~
:x
( :xリターン で vi を終了すると ruby > のプロンプトに戻ります。)
ruby> 
( そこで run と入力してプログラムを実行します。)
ruby> run
1
2
3
true
ruby> 

昔々 Basic でプログラムをしていた人には懐かしい操作ではないでしょうか。

Vi (ヴィアイ)の使い方はHow to vi?をみてください。

readline モジュールを利用する

さらに欲を出して行編集で GNU Readline Library を使えるように改造してみましょう。スクリプトの文頭で require "readline" と include Readline の 2 行を追加すると、 line = readline("Prompt> ", true) で変数 line に取り込む文字列をターミナル上で入力するとき Emacs に似た操作で行を編集することができます。readline メソッドの第一引数にはプロンプトの文字列をいれます。第二引数に true をいれるとヒストリーが使えます。bash にも GNU Readline Library が使われているので使い方を知っている人も多いと思います。改造した eval2.rb のスクリプトは次のようになります。赤字の所が変更点ですが、eval.rb との違いはほとんどありません。改造があまりに簡単なので拍子抜けしてしまいます。使ってみると随分使いやすくなっていると思います。

require "readline"
include Readline

line = ''
indent = 0
prompt = "ruby> "
while true
  l = readline(prompt, true) + "\n"
  if not l
    break if line == ''
  else
    line = line + l
    if l =~ /,\s*$/
      prompt = "ruby| "
      next
    end
    if l =~ /^\s*(class|module|def|if|case|while|for|begin)\b[^_]/
      indent += 1
    end
    if l =~ /^\s*end\b[^_]/
      indent -= 1
    end
    if l =~ /\{\s*(\|.*\|)?\s*$/
      indent += 1
    end
    if l =~ /^\s*\}/
      indent -= 1
    end
    if indent > 0
      prompt = "ruby| "
      next
    end
    if l =~ /^\s*vi\s*$/
      line = "system 'vi temp.rb'"
    end
    if l =~ /^\s*run\s*$/
      line = ""
      IO.foreach("temp.rb") {|ln| line += ln}
    end
  end
  begin
    print eval(line).inspect, "\n"
  rescue ScriptError, StandardError
    $! = 'exception raised' if not $!
    print "ERR: ", $!, "\n"
  end
  break if not l
  line = ''
  prompt = "ruby> "
end
print "\n"