Here is my solution for this quiz. Nothing too fancy really, but it does 
the job. I suspect if I used this more for various applications I'd find 
more clever things to do. But I did replace some command-line 
input/output in another program of mine with this library, and it 
definitely cleaned things up. It certainly makes sense to put this kind 
of stuff in a library instead of always rolling your own.

I never did look at the EasyPrompt code, but I suspect this code might 
look very similar because of my use of a MockIO object (I did notice 
that much from the EasyPrompt summary.) But I suppose it is good that 
the unit tests and MockIO object are more lines of code than the library :)

I would appreciate feedback on the API design as well as the unit test 
design. I feel the unit tests are too coupled to the implementation 
(particularly on my checking of the output.)

Ryan Leavengood

CODE (beware of wrapping):
#--------------------------------------
# HighLine command-line input library
#
# Copyright (C) 2005 Ryan Leavengood
#
# Released under the Ruby license
#--------------------------------------

class String
   def pad_if_needed
     self[-1].chr != ' ' ? self + ' ' : self
   end
end

class HighLine
   attr_accessor :io_out, :io_in
   def initialize(io_out=$stdout, io_in=$stdin)
     @io_out, @io_in = io_out, io_in
   end

   def ask(question, default=nil)
     q = question.pad_if_needed
     q += "[#{default}] " if default
     answer = validation_loop(q) do |input|
       input.size > 0 or default
     end
     answer.size > 0 ? answer : default
   end

   def ask_if?(question)
     answer = validation_loop(question.pad_if_needed+'(y,n) ') do |input|
       %w(y n yes no).include?(input.downcase)
     end
     answer.downcase[0,1] == 'y'
   end

   def ask_int(question, range=nil)
     validation_loop(question) do |input|
       input =~ /\A\s*-?\d+\s*\Z/ and (not range or 
range.member?(input.to_i))
     end.to_i
   end

   def ask_float(question, range=nil)
     validation_loop(question) do |input|
       input =~ /\A\s*-?\d+(.\d*)?\s*\Z/ and (not range or 
range.member?(input.to_f))
     end.to_f
   end

   def header(title)
     dashes = '-'*(title.length+4)
     io_out.puts(dashes)
     io_out.puts("| #{title} |")
     io_out.puts(dashes)
   end

   def list(items, prompt=nil)
     items.each_with_index do |item, i|
       @io_out.puts "#{i+1}. #{item}"
     end
     valid_range = '1'..items.length.to_s
     prompt = "Please make a selection: " unless prompt
     answer = validation_loop(prompt) do |input|
       valid_range.member?(input)
     end
     # Though the list is shown using a 1-indexed list, return 0-indexed
     return answer.to_i-1
   end

   def validation_loop(prompt)
     loop do
       @io_out.print prompt.pad_if_needed
       answer = @io_in.gets
       if answer
         answer.chomp!
         if yield answer
           return answer
         end
       end
     end
   end
end

# Unit Tests
if $0 == __FILE__
   class MockIO
     attr_accessor :output, :input

     def initialize
       reset
     end

     def reset
       @index = 0
       @input=nil
       @output=''
     end

     def print(*a)
       @output << a.join('')
     end

     def puts(*a)
       if a.size > 1
         @output << a.join("\n")
       else
         @output << a[0] << "\n"
       end
     end

     def gets
       if @input.kind_of?(Array)
         @index += 1
         @input[@index-1]
       else
         @input
       end
     end
   end

   require 'test/unit'

   class TC_HighLine < Test::Unit::TestCase
     def initialize(name)
       super(name)
       @mock_io = MockIO.new
       @highline = HighLine.new(@mock_io, @mock_io)
     end

     def setup
       @mock_io.reset
     end

     def test_ask
       question = 'Am I the coolest?'
       @mock_io.input = [nil, '', "\n", "yes\n"]
       assert_equal(@mock_io.input[-1].chomp, @highline.ask(question))
       assert_equal((question+' ')*4, @mock_io.output)
     end

     def test_ask_default
       question = 'Where are you from? '
       default = 'Florida'
       @mock_io.input = [nil, "\n"]
       assert_equal(default, @highline.ask(question, default))
       assert_equal((question+"[#{default}] ")*2, @mock_io.output)
     end

     def test_ask_if
       question = 'Is Ruby the best programming language? '
       @mock_io.input = [nil, "0\n", "blah\n", "YES\n"]
       assert_equal(true, @highline.ask_if?(question))
       assert_equal((question+'(y,n) ')*4, @mock_io.output)
     end

     def test_ask_int
       question = 'Give me a number:'
       @mock_io.input = [nil, '', "\n", ' ', "blah\n", "  -4   \n"]
       assert_equal(-4, @highline.ask_int(question))
       assert_equal((question+' ')*6, @mock_io.output)
       @mock_io.reset
       @mock_io.input = [nil, '', "\n", ' ', "blah\n", "3604\n"]
       assert_equal(3604, @highline.ask_int(question))
       assert_equal((question+' ')*6, @mock_io.output)
     end

     def test_ask_int_range
       question = 'How old are you?'
       @mock_io.input = [nil, '', "\n", ' ', "blah\n", "106\n", "28\n"]
       assert_equal(28, @highline.ask_int(question, 0..105))
       assert_equal((question+' ')*7, @mock_io.output)
     end

     def test_ask_float
       question = 'Give me a floating point number:'
       @mock_io.input = [nil, '', "\n", ' ', "blah\n", "  -4.3   \n"]
       assert_equal(-4.3, @highline.ask_float(question))
       assert_equal((question+' ')*6, @mock_io.output)
       @mock_io.reset
       @mock_io.input = [nil, '', "\n", ' ', "blah\n", "560\n"]
       assert_equal(560.0, @highline.ask_float(question))
       assert_equal((question+' ')*6, @mock_io.output)
     end

     def test_ask_float_range
       question = 'Give me a floating point number between 5.0 and 13.5:'
       @mock_io.input = [nil, '', "\n", ' ', "blah\n", "  -4.3   \n", 
"4.9\n", "13.6\n", "7.55\n"]
       assert_equal(7.55, @highline.ask_float(question, 5.0..13.5))
       assert_equal((question+' ')*9, @mock_io.output)
     end

     def test_header
       title = 'HighLine Manual'
       @highline.header(title)
       output = "-------------------\n| HighLine Manual 
|\n-------------------\n"
       assert_equal(output, @mock_io.output)
     end

     def test_list
       items = ['Ruby','Python','Perl']
       prompt = 'Please choose your favorite programming language: '
       @mock_io.input = [nil, "0\n", "blah\n", "4\n", "1\n"]
       assert_equal(0, @highline.list(items, prompt))
       assert_equal("1. Ruby\n2. Python\n3. Perl\n#{prompt * 5}", 
@mock_io.output)
     end
   end
end