รู้ไหมว่า Ruby มีวิธีให้ script ใช้ source file ของตัวเองเป็น data source? Trick น่าสนใจประหยัดเวลา one-off scripts และ proofs of concept มาดู!
DATA และ END
ตัวอย่างใช้ keyword แปลก __END__ ทุกอย่างใต้ __END__ จะถูก ignore โดย Ruby interpreter แต่น่าสนใจกว่านั้น Ruby ให้ IO object ชื่อ DATA ให้อ่านทุกอย่างใต้ __END__ เหมือนอ่าน file อื่นๆ
ตัวอย่าง iterate แต่ละบรรทัด print:
DATA.each_line do |line| puts line end __END__ Doom Quake Diablo
Practical example โปรดของผม ใช้ DATA เก็บ ERB template ทำงานกับ YAML, CSV ได้ด้วย
require 'erb' time = Time.now renderer = ERB.new(DATA.read) puts renderer.result() __END__ The current time is <%= time %>.
อ่าน content เหนือ __END__ keyword ได้ด้วย เพราะ DATA เป็น pointer ไป entire source file fast-forwarded ไป __END__ keyword เห็นได้ถ้า rewind IO object ก่อน print ตัวอย่าง print entire source file
DATA.rewind puts DATA.read # prints the entire source file __END__ meh
The Multiple-File Delimma
Downside ใหญ่ technique นี้ใช้ได้ก็ต่อเมื่อ script fit single source file และรัน file นั้นโดยตรง ไม่ใช่ include
ตัวอย่าง 2 files แต่ละอันมี __END__ section แต่มีแค่หนึ่ง DATA global __END__ section ของ 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 # 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 ----------------------
A Work-around for Multiple Files
Sinatra มี feature cool ให้เพิ่ม multiple inline templates ใน apps โดยวางหลัง __END__ statement 长这样:
# 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.
แต Sinatra ทำยังไง? App อาจ load by rack ไม่รัน ruby myapp.rb ใน production! ต้องมีวิธีใช้ DATA กับ multiple files
ขุด Sinatra source จะเห็นพวกเขา kind of cheating ไม่ได้用 DATA เลย ทำแบบนี้:
# 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)
ซับซ้อนนิดหน่อยเพราะไม่อยาก read __FILE__ นั่นคือ sinatra/base.rb file อยากได้ content ของ file ที่ invoked function ได้จาก parsing caller result
caller function บอกที่ currently running function ถูก invoked ตัวอย่าง:
def some_method puts caller end some_method # => caller.rb:5:in `<main>'
Pull filename ออกมา extract DATA equivalent สำหรับ file นั้นง่าย:
def get_caller_data puts File.read(caller.first.split(":").first).split("__END__", 2).last end
Use It for Good, Not Evil
หวังว่าจะชัดเจน tricks เหล่านี้ไม่ใช่ของใช้ทุกวัน ไม่ทำให้ clean, maintainable large code bases
แต่บางครั้งต้องการ quick and dirty สำหรับ one-off utility script หรือ proof of concept ในกรณีนั้น DATA และ __END__ useful ค่อนข้าง