scriptライブラリでRubyスクリプトそのものをオブジェクトにする!

Script

scriptというライブラリがある。これは、Rubyスクリプトファイルをオブジェクトとして扱うものだ。
gem化されてないのでインストールは手作業で。

wget http://redshift.sourceforge.net/script/script-0.3.tgz
cd script-0.3
ruby install.rb config
ruby install.rb setup
sudo ruby install.rb install

ふつうにloadやrequireでRubyスクリプトを読み込んだらグローバルな名前空間を汚染してしまうので、時として困ることがある。そこでscriptを使えば、特定のモジュール(に毛が生えたやつ)でスクリプトを実行してくれる。おまけに実行後の状態にアクセスすることもできるぞ。

require 'script'
EXTERN = "/tmp/hoge.rb"
open(EXTERN, "w"){|f| f.write <<END_OF_EXTERN_SCRIPT }
FOO = 7
def hoge_meth
  :hoge
end
@foo = :iv
lv = :local
END_OF_EXTERN_SCRIPT

# スクリプトファイルをオブジェクトとして扱う。
s = Script.load EXTERN          # => #<Script:/tmp/hoge.rb>
# ファイル名
s.__main_file                   # => "/tmp/hoge.rb"
# 定数参照
s::FOO                          # => 7
# メソッド呼び出し
s.hoge_meth                     # => :hoge
# インスタンス変数参照
s.instance_variable_get :@foo   # => :iv
# ローカル変数のリスト
s.__local_variables             # => ["__file__", "lv"]
# ローカル変数を得る
s.__local_variable_get :lv      # => :local
# Moduleを継承している
Script.ancestors                # => [Script, Module, Object, Kernel]

Rubyスクリプトを設定ファイルとして使うときには便利だろうね。と思ったのでやってみた。
Script.to_structを定義すれば外部スクリプトのローカル変数の集合を構造体として得ることができる。

require 'script'

def Script.to_struct(file)
  s = Script.load file
  lvs = s.__local_variables.map {|e| e.to_sym }
  Struct.new(*lvs).new.tap do |struct|
    lvs.each {|lv| struct[lv] = s.__local_variable_get(lv) }
  end
end

EXTERN = "/tmp/vars.rb"
open(EXTERN, "w"){|f| f.write <<END_OF_EXTERN_SCRIPT }
name = "rubikitch"
site = "http://www.rubyist.net/~rubikitch/"
blog = "http://d.hatena.ne.jp/rubikitch/"
END_OF_EXTERN_SCRIPT

# スクリプト中のローカル変数の集合を構造体として得る
s = Script.to_struct EXTERN
# => #<struct 
#     __file__="/tmp/vars.rb",
#     name="rubikitch",
#     site="http://www.rubyist.net/~rubikitch/",
#     blog="http://d.hatena.ne.jp/rubikitch/">
# 構造体はアクセサとしてもハッシュ形式でもアクセスできるから萌え
s.name     # => "rubikitch"
s[:name]   # => "rubikitch"
s["name"]  # => "rubikitch"
s.site     # => "http://www.rubyist.net/~rubikitch/"
s.blog     # => "http://d.hatena.ne.jp/rubikitch/"

スクリプト中のインスタンス変数を[]でアクセスできるようにしてみた。
ただ、これだと未定義の場合にnilが返るのでタイプミスしたときに検出されないな。
構造体にした方が安全だろう。

require 'script'

class Script
  def [](var)
    instance_variable_get("@#{var}")
  end
end

EXTERN = "/tmp/vars.rb"
open(EXTERN, "w"){|f| f.write <<END_OF_EXTERN_SCRIPT }
@name = "rubikitch"
@site = "http://www.rubyist.net/~rubikitch/"
@blog = "http://d.hatena.ne.jp/rubikitch/"
END_OF_EXTERN_SCRIPT

s = Script.load(EXTERN)
# => #<Script:/tmp/vars.rb>
s[:name]       # => "rubikitch"
s["name"]      # => "rubikitch"
s[:site]       # => "http://www.rubyist.net/~rubikitch/"
s[:blog]       # => "http://d.hatena.ne.jp/rubikitch/"
s[:undefined]  # => nil

追記

名前空間、、なんだかさわやかな、 たとえばしゃぼんだまに名前が書いてあるみたいな、

う〜ん…どうなんだろ。

名前空間は名前とそれが指すものの対応を表すもの…と考えておくとよい。

身近な例では、人の名前だ。こんなケースを考えてみよう。

  • 1組に一人「山田」さんがいる。(山田1)
  • 2組にも一人「山田」さんがいる。(山田2)

1組で「山田さん」と言ったら山田1のことを指すよね。「1組のメンバー」という名前空間で「山田」という名前で表される人は山田1しかいないので特定できる。で、1組で山田2を表したいときは「『2組の』山田さん」と言わなきゃならない。

変数だとこんなケースになる。「a」と名前がついている変数はそれぞれ別物になっている。それぞれ別の名前空間にある変数だから。メソッド内では個別のローカル変数の名前空間が用意される。

def meth1
  a = 2
end

def meth2
  a = 3
end

クラス名(定数名)でも。*1で「HTTP」と書くとNet::HTTPのことだし、*2で書くとURI::HTTPになる。*3だとフルネームでNet::HTTPやURI::HTTPと書かないといけない。*3でHTTPと書くと「HTTPなんて定数はねえぞ」と怒られる。

module Net
  class HTTP; end
  # *1
end

module URI
  class HTTP; end
  # *2
end
# *3