Bạn biết Ruby cung cấp cách script dùng chính source file của nó làm data source? Thủ thuật gọn gàng tiết kiệm thời gian viết one-off script và proof of concept. Xem ngay!
DATA và __END__
Ví dụ dưới dùng keyword lạ `__END__`. Mọi thứ dưới `__END__` bị Ruby interpreter bỏ qua. Thú vị hơn: Ruby cung cấp IO object `DATA`, cho phép đọc mọi thứ dưới `__END__` như đọc file bình thường.
Ví dụ sau lặp qua từng dòng và in ra:
DATA.each_line do |line|
puts line
end
__END__
Doom
Quake
Diablo
Ví dụ thực tế yêu thích: dùng `DATA` chứa ERB template. Cũng work với YAML, CSV...
require 'erb'
time = Time.now
renderer = ERB.new(DATA.read)
puts renderer.result()
__END__
The current time is <%= time %>.
Thực tế có thể dùng `DATA` đọc nội dung trên `__END__`. Vì `DATA` là con trỏ trỏ đến toàn bộ source file, fast-forward đến `__END__`. Thấy điều này nếu rewind IO object trước khi in. Ví dụ dưới in toàn bộ source file.
DATA.rewind
puts DATA.read # in toàn bộ source file
__END__
meh
Vấn đề nhiều file (The multiple-file dilemma)
Nhược điểm lớn: chỉ work nếu script gọn trong single source file, và chạy file đó trực tiếp, không include.
Ví dụ dưới: 2 file, mỗi file có `__END__` section. Tuy nhiên chỉ có một `DATA` global. Vì vậy `__END__` của file 2 inaccessible.
# 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 # Không output gì, vì first.rb đọc hết file
end
__END__
Second end clause
snhorne ~/tmp $ ruby first.rb
First file
----------------------
First end clause
Second file
----------------------
Work-around cho nhiều file
Sinatra có feature cool: cho nhiều inline template trong app bằng cách đặt sau `__END__`. Trông như:
# Code từ Sinatra docs tại https://www.sinatrarb.com/intro.html
require 'sinatra'
get '/' do
haml :index
end
__END__
@@ layout
%html
= yield
@@ index
%div.title Hello world.
Nhưng Sinatra làm sao? App của bạn load bởi rack. Không chạy `ruby myapp.rb` production! Chắc họ tìm cách dùng `DATA` với nhiều file.
Khoan, đào vào source Sinatra một chút, thấy họ "cheat" một chút. Không dùng `DATA` đâu. Thay vào đó làm giống code dưới:
# Paraphrase. Xem gốc tại https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1284
app, data = File.read(__FILE__).split(/^__END__$/, 2)
Hơi phức tạp hơn, vì không muốn đọc `__FILE__` (chỉ là sinatra/base.rb). Muốn nội dung file invoke function. Lấy bằng parse kết quả `caller`.
Function `caller` cho biết function đang chạy được invoke ở đâu. Ví dụ nhanh:
def some_method
puts caller
end
some_method # => caller.rb:5:in `'
Bây giờ dễ dàng pull filename ra, extract tương đương DATA cho file đó.
def get_caller_data
puts File.read(caller.first.split(":").first).split("__END__", 2).last
end
Dùng cho tốt, không dùng cho xấu
Hy vọng rõ ràng: thủ thuật này không phải dùng hàng ngày. Không tạo code base sạch, maintainable lớn.
Tuy nhiên đôi khi cần quick and dirty, cho one-off utility script hoặc proof of concept. Lúc đó, `DATA` và `__END__` khá hữu ích.