From: Ross Bamford Date: 2006-11-20T05:19:43+09:00 Subject: Re: [QUIZ] Literate Ruby (#102) --=-rn003KyEaXMD4XKovKt1 Content-Type: text/plain Content-Transfer-Encoding: 7bit (Apologies if this turns out to be a dupe - I posted this about an hour ago on the newsgroup and it doesn't seem to be showing up anywhere). Here's my solution to this quiz. It's basically a non-too-efficient regex-based solution wrapped in a combination command-line script and library. The main file (lrb.rb) contains both the library and script code, while the other three files make up some tests of the functionality in lrb.rb. To run the tests, use the lrb script: $ ./lrb.rb test.lrb If you like, lrb will output the code it's about to run before running it if you enable ruby debugging: $ ./lrb.rb test.lrb -d The first argument has to be the filename. If the second argument is a number, it's taken as the index of the code block to run. Following arguments are passed to the ruby interpreter that replaces the one running lrb. You can run individual tests using this, but you need to manually require test/unit. E.g: $ ./lrb.rb test.lrb 1 -rtest/unit $ ./lrb.rb test.lrb 2 -rtest/unit (the numbers are 0-based, but the tests have a 'require test/unit' line at block 0 which does nothing on it's own). If you need to pass a number as the first argument to your script, give the block number as -- . You can also get a dump of the extracted code, rather than running it: $ ./lrb.rb test.lrb --dump When used as a library, lrb provides a literate version of eval (lrb_eval) and an lrb-enabled require (lrb_require) that won't work with rubygems :(. The library is not auto-required into the interpreter that runs the lrb code. It has it's warts (especially, no handling of piped-in code or data, no nested \begin...\end blocks, and debugging can be a PITA) but generally it seems to work and is pretty flexible. Thanks for this quiz - literate programming is something I'm still finding out about (mostly via Haskell, which I've just started looking at properly) so this was an interesting quiz topic for me :) -- Ross Bamford - rosco@roscopeco.REMOVE.co.uk --=-rn003KyEaXMD4XKovKt1 Content-Disposition: attachment; filename=lrb.rb Content-Type: application/x-ruby; name=lrb.rb Content-Transfer-Encoding: 7bit #!/usr/bin/env ruby # # Literate ruby. # for Ruby Quiz #102 require 'tempfile' MAIN_BINDING = binding module Lrb LRB_RX = /(?:^\s*>(.*?)$) | (?:[^\\]\\begin\{code\} (.*?) [^\\]\\end\{code\})/mxu class << self # Get individual blocks of code def lrb_blocks(str) str.scan(LRB_RX).map do |rb| rb.compact.first.strip end end # Get the whole code def lrb_code(str) lrb_blocks(str).join("\n") end def lrb_dump(str, blk_idx = nil) get_lrb_code(str, blk_idx, false) end def lrb_exec(fn, args = [], blk_idx = nil, debug = $DEBUG) # Ruby will interpet the -d and set DEBUG in the new ruby # that replaces this process. However, to get output printed # now we need to check it manually. code = get_lrb_code(File.read(fn), blk_idx, debug) # Use a TempFile. We don't get the auto-delete stuff (finalizer # won't get run after we replace the process) but it's # convenient for the auto naming, and we can delete it # later, from the new interpreter. # # The added bonus is, if theres a parse error, the tempfile # gets left behind which helps with debugging... tf = Tempfile.open(File.basename(fn), '.') tf << "at_exit { File.unlink(__FILE__) }\n" tf << code tf.close(false) # finalizer will never run exec('ruby', *(args << tf.path)) end def lrb_eval(code, blk_idx = nil, fn = nil, debug = $DEBUG) fn ||= '(lrb_eval)' eval(get_lrb_code(code, blk_idx, debug), MAIN_BINDING, "#{fn} #{('(#' + blk_idx.to_s + ')') if blk_idx}") end def lrb_require(fn, blk_idx = nil, debug = $DEBUG) unless $".include?(fn) fn += '.lrb' if File.extname(fn).empty? unless File.exists?(fn + blk_idx.to_s) # nil.to_s == "" so all good rfn = nil fn = $:.detect { |dir| File.exists?(rfn = File.join(dir,fn)) } fn = rfn if fn end if fn begin code = get_lrb_code(File.read(fn), blk_idx, debug) rescue raise LoadError, $!.message end eval(code, MAIN_BINDING, "#{fn} #{('(#' + blk_idx.to_s + ')') if blk_idx}") $" << fn + blk_idx.to_s true else raise LoadError, "no such file to load - #{fn}" end else false end end private def get_lrb_code(str, blk_idx = nil, debug = false) if blk_idx code=Lrb.lrb_blocks(str)[blk_idx] or raise "LRB: No block ##{blk_idx}" else code=Lrb.lrb_code(str) or raise "LRB: No code found" end if debug $stderr.puts " ************ LRB: Will execute ************** " $stderr.puts code $stderr.puts " ***************** LRB: END ****************** " end code end end end unless ($NO_CORE_LRB ||= false) module Kernel private def lrb_exec(fn, args = [], blk_idx = nil, debug = $DEBUG) Lrb.lrb_exec(fn, args, blk_idx, debug) end def lrb_eval(code, blk_idx = nil, fn = nil, debug = $DEBUG) Lrb.lrb_eval(code, blk_idx, fn, debug) end def lrb_require(fn, blk_idx = nil, debug = $DEBUG) Lrb.lrb_require(fn, blk_idx, debug) end end end if $0 == __FILE__ fn = ARGV.shift or raise ArgumentError, "LRB: lrb [--dump | filename [block#] [ruby / program opts...]]" if (poss_idx = ARGV.first).to_i.to_s == poss_idx blk_idx = ARGV.shift.to_i elsif poss_idx == '--' ARGV.shift end if File.extname(fn) == '.lrb' if ARGV.include?('--dump') puts Lrb.lrb_dump(File.read(fn), blk_idx) else lrb_exec(fn, ARGV, blk_idx, ARGV.include?('-d')) end else exec('ruby', *(ARGV << fn)) end end --=-rn003KyEaXMD4XKovKt1 Content-Disposition: attachment; filename=quiz.lrb Content-Type: text/plain; name=quiz.lrb; charset=utf-8 Content-Transfer-Encoding: 7bit The three rules of Ruby Quiz: 1. Please do not post any solutions or spoiler discussion for this quiz until 48 hours have passed from the time on this message. 2. Support Ruby Quiz by submitting ideas as often as you can: http://www.rubyquiz.com/ 3. Enjoy! Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone on Ruby Talk follow the discussion. Please reply to the original quiz message, if you can. -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= by Justin Bailey "Literate Programming"[1] is an idea popularized by Donald Knuth, where the traditional order of code and comments in a source file is switched. Instead of using special delimiters to mark comments, special delimiters are used to mark *code*. Innocuous as it sounds, this style of programming makes for a great way to post code snippets, tutorials, or even whole libraries to mailing lists, blogs, and web pages. It's also an excellent way to develop your CS homework There are, of course, a variety of ways to make a source file "literate". One popular method is called "bird notation". Code is delimited by lines starting with ">": > puts "The first line of literate Ruby you may have ever seen" Another method, used in the Haskell language, is borrowed from LateX and makes it very easy to embed working code into longer papers: \begin{code} puts "And here, we have" puts "the second and third lines of literate Ruby to be produced." \end{code} Beyond *how* to represent literate code, a host of issues present themselves. Can a class, method, or even string span multiple code sections? Can the different styles of code demarcation be mixed in one file? How do you "escape" code demarcation? What about inserting the output of code lines into the same literate file? Your task is to enable literate Ruby. What that means is up to you. Is literate programming only available at the file level (e.g. only files ending in ".lrb" are considered literate)? Or is literate programming supported with eval/class_eval/module_eval? Would this enable embedded literated here (i.e. <<) docs? At the minimum, this quiz should be seen as a literate program, and your code should be able to run it! [Editor's Note: The indention added to Ruby blocks in this quiz are a side effect of the Ruby Quiz software. Feel free to remove them when treating this quiz as literate code. --JEG2] Justin > puts "Here's to hoping you enjoyed the quiz!" [1] http://en.wikipedia.org/wiki/Literate_programming --=-rn003KyEaXMD4XKovKt1 Content-Disposition: attachment; filename=test.lrb Content-Type: text/plain; name=test.lrb; charset=utf-8 Content-Transfer-Encoding: 7bit This is a literate ruby test file. You can run the whole file by simply running it through lrb, or individual tests by supplying the block number to execute. In that case though, you'll have to > require 'test/unit' yourself with a command like: lrb test.lrb 2 -rtest/unit Test numbers are 1-based (zero is that require 'test/unit' above) You can have lrb show the code it's about to execute using e.g: lrb test.lrb -d So, here's the first test. \begin{code} class TC_LRB < Test::Unit::TestCase def test_01 assert true end end \end{code} We need to make sure we can \begin{code}class TC_LRB < Test::Unit::TestCase def test_02 assert true end end \end{code} anywhere on a line. Also, we need to make sure we can escape \\begin{code} raise "Not code!" \\end{code} both within and at start of a line. Okay, let's just check that lines starting with > aren't treated as single line blocks if they're inside a \begin...\end block. \begin{code} class TC_LRB < Test::Unit::TestCase def test_03 s = " > This is not a line of code > And wouldn't compile on it's own " e="\n> This is not a line of code\n> And wouldn't compile on it's own\n" assert_equal e, s.gsub(/\n\s*/, "\n") end end \end{code} Make sure lrb_require works properly: \begin{code} require 'lrb' class TC_LRB < Test::Unit::TestCase def test_04 assert_raise(NoMethodError) { meth_one() } lrb_require('testreq', 0) assert_equal 1, meth_one() assert_raise(NoMethodError) { meth_two() } lrb_require('testreq', 1) assert_equal 2, meth_two() end end \end{code} Finally, make sure lrb_eval works. \begin{code} require 'lrb' class TC_LRB < Test::Unit::TestCase def test_06 lrb_require('testreq', 2) # does it's own assertions meth_three() end end \end{code} That's it. Now just run this test through lrb. --=-rn003KyEaXMD4XKovKt1 Content-Disposition: attachment; filename=testreq.lrb Content-Type: text/plain; name=testreq.lrb; charset=utf-8 Content-Transfer-Encoding: 7bit Just test that lrb_require works \begin{code} class TC_LRB < Test::Unit::TestCase def meth_one 1 end end \end{code} \\begin{code} a red herring \begin{code} class TC_LRB < Test::Unit::TestCase def meth_two 2 end end \end{code} But we also need to make sure lrb_eval works too. \begin{code} require 'stringio' class TC_LRB < Test::Unit::TestCase def meth_three test_block_0 test_block_1 test_block_2 end def test_block_0 $stdout = (s = StringIO.new) lrb_eval(File.read('quiz.lrb'), 0) assert_equal( "The first line of literate Ruby you may have ever seen", s.string.chomp) $stdout = STDOUT end def test_block_1 $stdout = (s = StringIO.new) lrb_eval(File.read('quiz.lrb'), 1) assert_equal( "And here, we have\nthe second and third lines of literate Ruby to be produced.", s.string.chomp) $stdout = STDOUT end def test_block_2 $stdout = (s = StringIO.new) lrb_eval(File.read('quiz.lrb'), 2) assert_equal "Here's to hoping you enjoyed the quiz!", s.string.chomp $stdout = STDOUT end end \end{code} The End. --=-rn003KyEaXMD4XKovKt1--