people, people.
Every time your code takes a branch, loops the loop, takes an early
return, etc, it’s executing a “goto”.
Commonly used goto-es:
next
continue
break
return
The switch from a semi-unstructured language to a high-structured,
quasi functional, oo language can be as difficult as learning
idiomatic 15th century classical French for some.
The heart of a text-based, menu-driven task system is the
Read-Eval-Print loop (REPL). The basic arrangement for this in most
every structured language is so:
#!/usr/bin/env ruby
Read-Eval-Print Loop basic command processor
def show_menu
puts “some menu”
end
def get_response
:done
end
def perform(request)
puts request.to_s
request
end
loop do
show_menu
request = get_response
result = perform(request)
break if result == :done
end
The next vital part of the tiny app will be how you want to link your
menu items, valid responses, and tasks. Thinking data structures here.
The simplest thing I can think of is a hash, where the key is the
response, and the value is a hash containing an entry for the menu
item, and an entry for the procedure/method to execute.
@commands = {
‘1’ => {
:menu => “Thing 1”,
:action => lambda {thing1}},
‘2’ => {
:menu => “Thing 2”,
:action => lambda {thing2}},
‘0’ => {
:menu => “Quit”,
:action => lambda { :done }}
}
“@commands” is an /instance variable/ with the scope of the current
instance, in this case of the global class Object. We’re not actually
doing anything OO here, but using ruby idioms to get something done.
In this context, where there are no classes, it acts pretty much like
a global.
“lambda” defines an anonymous procedure, which will be handy when we
get to fleshing out the perform method from above.
“thing1” and “thing2” are going to be where you build your tasks. For
now, they’re just place-holder methods:
def thing1
puts “THING ONE RULES!!”
end
def thing2
puts “THING TWO FTW!”
end
The last menu option merely returns the symbol value “:done”, which is
used to exit your REPL.
Now we can look at how to show the menu. Given the above @commands
data structure, this becomes trivial:
def show_menu
@commands.each do |key, value|
puts “#{value[:menu]} - Press #{key}”
end
end
This says to loop through each of the hash’s members, and using the
key and value, display the action, which is in the value’s :menu item,
and the input needed, which is the entry’s key.
Getting the response is also rather trivial:
def get_response
gets.chomp.downcase
end
This retrieves a line from input (user types the appropriate choice
and presses return), then removes the trailing white space, and
converts it to lower case. In Ruby, the last thing evaluated by a
method is what gets returned as the result of that method.
Now, however, we need to add something more to our REPL. What happens
if the user type “XyZzY!!” ? The program as it stands will give some
kind of error and die. Instead, we need to validate what the user
typed in and give an appropriate response if we don’t recognize it.
So, to our REPL, we add:
loop do
show_menu
request = get_response
if valid?(request)
result = perform(request)
else
puts “Unknown request #{request}. Try again”
result = nil
end
break if result == :done
end
And the associated “valid?” method:
def valid?(response)
@commands.keys.include?(response)
end
Which returns true if the value passed in, “response” is included in
the array of keys in @commands. This sort of thing is called
“chaining” in Ruby, and can be quite expressive, and is what gives
Ruby some of it’s functional programming flavour.
Okay, now we should have a complete program:
#!/usr/bin/env ruby
Read-Eval-Print Loop basic command processor
@commands = {
‘1’ => {
:menu => “Thing 1”,
:action => lambda {thing1}},
‘2’ => {
:menu => “Thing 2”,
:action => lambda {thing2}},
‘0’ => {
:menu => “Quit”,
:action => lambda { :done }}
}
def thing1
puts “THING ONE RULES!!”
end
def thing2
puts “THING TWO FTW!”
end
def show_menu
@commands.each do |key, value|
puts “#{value[:menu]} - Press #{key}”
end
end
def get_response
gets.chomp!.downcase
end
def valid?(response)
@commands.keys.include?(response)
end
def perform(request)
@commands[request][:action].call
end
loop do
show_menu
request = get_response
if valid?(request)
result = perform(request)
else
puts “Unknown request #{request}. Try again”
result = nil
end
break if result == :done
end
If you load this into irb, you can see how it works:
1.9.3-head :010 > load ‘repl.rb’
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
1
THING ONE RULES!!
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
1
THING ONE RULES!!
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
2
THING TWO FTW!
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
2
THING TWO FTW!
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
1
THING ONE RULES!!
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
2
THING TWO FTW!
Thing 1 - Press 1
Thing 2 - Press 2
Quit - Press 0
0
=> true
1.9.3-head :011 >
The one thing I can see missing from your original BASIC version is
that this one does not do a clear-screen at the top of each loop. That
actually gets a bit more complicated, because you have to mess with
terminal control sequences. That’s beyond my knowledge.