Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Sử dụng DATA và __END__ của Ruby để nhúng Code và Data liền mạch

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.