Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyのDATAと__END__を使いこなす:スクリプト内にデータを埋め込むテクニック

Rubyには、スクリプト自身のソースファイルをデータ源として利用できる仕組みがあることをご存知でしょうか? 一見地味なトリックですが、使い捨てのスクリプトや概念実証(PoC)を素早く作りたいときに、意外と役立つテクニックです。早速見ていきましょう!

DATAと__END__の基本

以下の例では、__END__という少し変わったキーワードを使用しています。__END__より下に書かれた内容は、Rubyインタプリタによって完全に無視されます。

しかし、ここからが面白いところです。RubyはDATAというIOオブジェクトを提供しており、これを使うと__END__以降の内容を、通常のファイルと同じように読み取ることができます。

次の例では、各行を順番に取り出して表示しています。

DATA.each_line do |line|
  puts line
end

__END__
Doom
Quake
Diablo

このテクニックの実用的な応用例として筆者のお気に入りは、DATAにERBテンプレートを格納する方法です。もちろん、YAMLやCSVなどにも同じ要領で応用できます。

require 'erb'

time = Time.now
renderer = ERB.new(DATA.read)
puts renderer.result()

__END__
The current time is <%= time %>.

実は、DATAを使って__END__キーワードよりの内容を読み取ることも可能です。これは、DATAが実際にはソースファイル全体へのポインタであり、__END__の位置までシークされた状態になっているためです。IOオブジェクトを巻き戻してから出力してみると、その挙動がよくわかります。以下の例では、ソースファイル全体が出力されます。

DATA.rewind
puts DATA.read # ソースファイル全体が出力される

__END__
meh

複数ファイルでの落とし穴

このテクニックには大きな制約があります。スクリプトが単一のソースファイルに収まっていて、そのファイルを直接実行している場合(requireで読み込むのではなく)にしか、正しく機能しないのです。

以下の例では、それぞれ独自の__END__セクションを持つ2つのファイルを用意しました。しかし、DATAというグローバルオブジェクトは1つしか存在できないため、2番目のファイルの__END__セクションにはアクセスできません。

# first.rb
require "./second"

puts "First file\n----------------------"
puts DATA.read

print_second_data()

__END__
First end clause

# second.rb

def print_second_data
  puts "Second file\n----------------------"
  puts DATA.read # first.rbがファイル全体を読み済みのため、何も出力されない
end

__END__

Second end clause

snhorne ~/tmp $ ruby first.rb
First file
----------------------
First end clause

Second file
----------------------

複数ファイル問題の回避策

Sinatraには、__END__ステートメントの後にインラインテンプレートを記述することで、アプリに複数のテンプレートを組み込めるという便利な機能があります。具体的には次のような書き方です。

# Sinatra公式ドキュメント(https://www.sinatrarb.com/intro.html)より引用
require 'sinatra'

get '/' do
  haml :index
end

__END__

@@ layout
%html
  = yield

@@ index
%div.title Hello world.

では、Sinatraは一体どうやってこれを実現しているのでしょうか? 本番環境ではruby myapp.rbのように直接実行することはなく、アプリはRack経由で読み込まれるのが普通です。Sinatraは、複数ファイル環境でもDATA相当のものを使う方法を編み出したに違いありません。

実際にSinatraのソースコードを掘り下げてみると、彼らはある種の「裏技」を使っていることがわかります。なんとDATAは一切使っておらず、代わりに以下のようなコードに似た処理を行っています。

# 簡略化した擬似コード。元のコードは https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1284 を参照
app, data = File.read(__FILE__).split(/^__END__$/, 2)

実際の実装はもう少し複雑です。単純に__FILE__を読むだけでは、それはsinatra/base.rbファイル自身になってしまうためです。Sinatraが欲しいのは、「関数を呼び出した側のファイル」の内容です。そこで彼らは、callerの結果を解析することでこれを実現しています。

caller関数は、現在実行中のメソッドがどこから呼び出されたのかを教えてくれる便利なメソッドです。簡単な例を見てみましょう。

def some_method
  puts caller
end

some_method # => caller.rb:5:in `<main>'

あとはこの結果からファイル名を取り出し、そのファイルからDATA相当のデータを抽出するだけです。

def get_caller_data
  puts File.read(caller.first.split(":").first).split("__END__", 2).last
end

諸刃の剣として使う

こうしたトリックが日常的に使うべきものではないことは、お分かりいただけるでしょう。大規模で保守性の高いコードベースを構築する用途には、決して向いていません。

しかし、使い捨てのユーティリティスクリプトや概念実証のために、手早く手軽な方法が必要になる場面は時々あります。そんなときこそ、DATA__END__が真価を発揮してくれるのです。

  1. Rubyでファイルを読み書きする方法を実例つきで徹底解説

    Rubyでファイルの読み書きができるようになると、ファイルからデータを抽出したり、新しいファイルを作成したり、必要な情報を素早く見つけたりできるようになります。この記事では、Rubyにおけるファイル操作の基本を、実際のコード例とともにわかりやすく解説します。 この記事で学べること Rubyでファイルを読み込む方法 Rubyでファイルに書き込む方法 覚えておきたいFileクラスのメソッド ディレクトリ操作の基礎 FileUtilsモジュールの使い方 まとめ それでは始めましょう! Rubyでファイルを読み込む方法 Rubyでファイルを読み込む基本的な流れは、次の3ステップです。 op

  2. Windows 10の「ファイル履歴」でデータを自動バックアップ・復元する完全ガイド

    ハードディスクの故障、停電、その他の予期せぬトラブルから、大切なファイルやドキュメントを守ることは非常に重要です。本来なら定期的なバックアップを行うべきですが、Windowsでは重要なファイルをUSBメモリ(少量のデータ向け)や外付けハードディスク(大量のデータ向け)などの外部ストレージに手動でコピーする方法が一般的です。さらに、クラウドストレージへ毎日自動的にバックアップしてくれる有料ソフトを利用する選択肢もあります。実は、Windows 10には「ファイル履歴(File History)」という優れた機能が標準搭載されており、これを使えばPCのデータを自動的にバックアップできます。 Win