Bạn có biết rằng Ruby cung cấp một cách để tập lệnh của bạn sử dụng tệp nguồn của chính nó làm nguồn dữ liệu không? Đó là một thủ thuật nhỏ gọn có thể giúp bạn tiết kiệm thời gian khi viết kịch bản một lần và các bằng chứng về khái niệm. Hãy cùng khám phá!
DATA và KẾT THÚC
Trong ví dụ dưới đây, tôi đang sử dụng một từ khóa vui nhộn có tên là __END__
. Mọi thứ bên dưới __END__
sẽ bị trình thông dịch Ruby bỏ qua. Nhưng thú vị hơn, ruby cung cấp cho bạn một đối tượng IO được gọi là DATA
, cho phép bạn đọc mọi thứ bên dưới __END__
giống như bạn có thể đọc từ bất kỳ tệp nào khác.
Trong ví dụ sau, chúng tôi lặp lại từng dòng và in nó.
DATA.each_line do |line|
puts line
end
__END__
Doom
Quake
Diablo
Ví dụ thực tế yêu thích của tôi về kỹ thuật này sử dụng DATA
để chứa một mẫu ERB. Nó cũng hoạt động với YAML, CSV, v.v. M
require 'erb'
time = Time.now
renderer = ERB.new(DATA.read)
puts renderer.result()
__END__
The current time is <%= time %>.
Bạn thực sự có thể sử dụng DATA
để đọc nội dung bên trên __END__
từ khóa. Đó là bởi vì DATA
thực sự là một con trỏ đến toàn bộ tệp nguồn, được chuyển tiếp nhanh tới __END__
từ khóa. Bạn có thể thấy điều này nếu bạn tua lại đối tượng IO trước khi in nó. Ví dụ dưới đây in ra toàn bộ tệp nguồn.
DATA.rewind
puts DATA.read # prints the entire source file
__END__
meh
Dấu phân tách nhiều tệp
Một trong những nhược điểm lớn của kỹ thuật này là nó chỉ thực sự hoạt động nếu tập lệnh của bạn phù hợp với một tệp nguồn duy nhất và bạn đang chạy trực tiếp tệp đó, thay vì bao gồm nó.
Trong ví dụ dưới đây, tôi có hai tệp, mỗi tệp có __END__
riêng của chúng tiết diện. Tuy nhiên, chỉ có thể có một DATA
toàn cầu. Vì vậy, __END__
phần của tệp thứ hai không thể truy cập được.
# 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 # Won't output anything, since first.rb read the entire file
end
__END__
Second end clause
snhorne ~/tmp $ ruby first.rb
First file
----------------------
First end clause
Second file
----------------------
Hoạt động xoay quanh nhiều tệp
Sinatra có một tính năng khá thú vị cho phép bạn thêm nhiều mẫu nội tuyến vào ứng dụng của mình bằng cách đặt chúng sau __END__
bản tường trình. Nó trông như thế này:
# This code is from the Sinatra docs at https://www.sinatrarb.com/intro.html
require 'sinatra'
get '/' do
haml :index
end
__END__
@@ layout
%html
= yield
@@ index
%div.title Hello world.
Nhưng chính xác thì sinatra có thể làm được điều này như thế nào? Rốt cuộc, ứng dụng của bạn có thể sẽ được tải bằng giá đỡ. Bạn sẽ không chạy ruby myapp.rb
trong sản xuất! Chắc hẳn họ đã tìm ra cách để sử dụng DATA
với nhiều tệp.
Mặc dù vậy, nếu bạn tìm hiểu kỹ nguồn Sinatra một chút, bạn sẽ thấy rằng họ đang gian lận. Họ không sử dụng DATA
ở tất cả. Thay vào đó, họ đang làm điều gì đó tương tự như mã bên dưới.
# I'm paraphrasing. See the original at https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1284
app, data = File.read(__FILE__).split(/^__END__$/, 2)
Nó thực sự phức tạp hơn một chút, vì họ không muốn đọc __FILE__
. Đó sẽ chỉ là tệp sinatra / base.rb. Thay vào đó, họ muốn lấy nội dung của tệp đã gọi một hàm. Họ nhận được điều này bằng cách phân tích cú pháp kết quả của trình gọi.
Hàm người gọi sẽ cho bạn biết vị trí mà hàm hiện đang chạy đã được gọi. Đây là một ví dụ nhanh:
def some_method
puts caller
end
some_method # => caller.rb:5:in `<main>'
Bây giờ, việc kéo tên tệp ra khỏi đó và giải nén một cái gì đó tương đương với DATA cho tệp đó là một vấn đề khá đơn giản.
def get_caller_data
puts File.read(caller.first.split(":").first).split("__END__", 2).last
end
Sử dụng nó vì điều tốt, chứ không phải điều xấu
Hy vọng rằng rõ ràng những thủ thuật như thế này không phải là thứ mà bạn sẽ muốn sử dụng hàng ngày. Chúng không chính xác tạo ra các cơ sở mã lớn sạch sẽ, có thể bảo trì được.
Tuy nhiên, đôi khi bạn cần một cái gì đó nhanh chóng và bẩn thỉu, cho một tập lệnh tiện ích một lần hoặc cho một bằng chứng về khái niệm. Trong trường hợp đó, DATA
và __END__
có thể khá hữu ích.