Previous ToC Up Next

10. The Clop Code

10.1. Test Drivers

Alice: I'm really happy with the command line option parser. We'll use it from now on. And your demonstration of the help facility was impressive, I must say.

Bob: You mean your demonstration.

Alice: I guess so; you were letting me struggle all by myself! But somehow there was just enough information delivered to let me figure it out. And I must say, I like the idea of having each class include its own test driver, at the end of the file that holds the class. Your trick of including the line

  if  _FILE_ == $0
was a great idea. I think we should do that from now on for any file that contains class definitions that will be used by other files.

Bob: Yes, I'm planning to do that.

Alice: Can you show me what you wrote, in clop.rb, as the test driver?

Bob: Here is the whole content of clop.rb, with the test driver at the end. Note that the first four lines of the `here document' are:

  Description: Command line option parser, (c) 2004, Piet Hut & Jun Makino, ACS
  Long description:
    Test program for the class Clop (Command line option parser),
    (c) 2004, Piet Hut and Jun Makino; see ACS at www.artcompsi.org
Alice: Who are those two people?

Bob: Oh, I just chose two pen names for us; as long as we are just building toy models, we may as well use toy names for our products, don't you think?

Alice: But why a Dutch and a Japanese name?

Bob: Why not? I had to pick something.

Alice: I might have picked a Liechtensteiner and a Fijian, but I won't argue with your taste. Can you show me the code?

10.2. Code Listing

Bob: Here it is:

 require "vector.rb"
 
 class Clop_Option
 
   attr_reader :shortname, :longname, :type,
               :description, :longdescription, :printname, :defaultvalue
   attr_accessor :valuestring
 
   def initialize(def_str)
     parse_option_definition(def_str)
   end
 
   def parse_option_definition(def_str)
     while s = def_str.shift
       break if parse_single_lines_done?(s)
     end
     while s = def_str.shift                                                  
       break if s =~ /^\s*$/ and  def_str[0] =~ /^\s*$/                       
       @longdescription += s + "\n"                                           
     end                                                                      
   end
 
   def parse_single_lines_done?(s)
     if s !~ /\s*(\w.*?)\s*\:/                                               
       raise "\n  option definition line has wrong format:\n==> #{s} <==\n"
     end
     name = $1
     content = $'
     case name
       when /Short\s+(N|n)ame/
         @shortname = content.split[0]
       when /Long\s+(N|n)ame/
         @longname = content.split[0]
       when /Value\s+(T|t)ype/
         @type = content.sub(/^\s+/,"").sub(/\s*(#.*|$)/,"")                 
         @valuestring = "false" if @type == "bool"                           
       when /Default\s+(V|v)alue/
         @defaultvalue = content.sub(/^\s+/,"").sub(/\s*(#.*|$)/,"")
         @valuestring = @defaultvalue
       when /Global\s+(V|v)ariable/
         @globalname = content.split[0]
       when /Print\s+(N|n)ame/
         @printname = content.sub(/^\s+/,"").sub(/\s*(#.*|$)/,"")
       when /Description/
         @description = content.sub(/^\s+/,"").sub(/\s*(#.*|$)/,"")
       when /Long\s+(D|d)escription/
         @longdescription = ""                                               
         return true                                                         
       else
         raise "\n  option definition line unrecognized:\n==> #{s} <==\n"
     end
     return false
   end
 
   def initialize_global_variable
     eval("$#{@globalname} = eval_value") if @globalname
   end
 
   def eval_value
     case @type
       when "bool"
         eval(@valuestring)
       when "string"
         @valuestring
       when "int"
         @valuestring.to_i
       when "float"
         @valuestring.to_f
       when /^float\s*vector$/
         @valuestring.gsub(/[\[,\]]/," ").split.map{|x| x.to_f}.to_v
       else
         raise "\n  type \"#{@type}\" is not recognized"
     end
   end
 
   def add_tabs(s, reference_size, n)
     (1..n).each{|i| s += "\t" if reference_size < 8*i}
     return s
   end
 
   def to_s
     if @type == nil                                                         
       s = @description + "\n"                                               
     elsif @type == "bool"                                                   
       if eval(@valuestring)                                                 
         s = @description + "\n"                                             
       else                                                                  
         s = ""                                                              
       end                                                                   
     else
       s = @description                                                      
       s = add_tabs(s, s.size, 4)                                            
       s += ": "                                                             
       if @printname                                                         
         s += @printname                                                     
       else                                                                  
         s += @globalname                                                    
       end                                                                   
       s += " = " unless @printname == ""                                    
       s += "\n  " if @type =~ /^float\s*vector$/                            
       s += "#{eval("$#{@globalname}")}\n"                                   
     end
     return s
   end
 
 end
 
 class Clop
 
   def initialize(def_str, argv_array = nil)
     parse_option_definitions(def_str)                                        
     if argv_array
       parse_command_line_options(argv_array)
     end
     print_values
   end
 
   def parse_option_definitions(def_str)
     a = def_str.split("\n")                                                  
     @options=[]
     while a[0]
       if a[0] =~ /^\s*$/                                                     
         a.shift                                                              
       else
         @options.push(Clop_Option.new(a))                                    
       end
     end
   end
 
   def parse_command_line_options(argv_array)
     while s = argv_array.shift
       if s == "-h"
         parse_help(argv_array, false)
         exit
       elsif s == "--help"
         parse_help(argv_array, true)
         exit
       elsif i = find_option(s)
         parse_option(i, s, argv_array)
       else
         raise "\n  option \"#{s}\" not recognized; try \"-h\" or \"--help\"\n"
       end
     end
     initialize_global_variables
   end
 
   def print_values
     @options.each{|x| STDERR.print x.to_s}
   end
 
   def find_option(s)
     i = nil
     @options.each_index do |x|
       i = x if s == @options[x].longname
       if @options[x].shortname
         i = x if s =~ Regexp.new(@options[x].shortname) and $` == ""         
       end
     end
     return i
   end
 
   def parse_option(i, s, argv_array)
     if @options[i].type == "bool"
       @options[i].valuestring = "true"
       return
     end
     if s =~ /^-[^-]/ and (value = $') =~ /\w/                                
       @options[i].valuestring = value                                        
     else
       unless @options[i].valuestring = argv_array.shift                      
         raise "\n  option \"#{s}\" requires a value, but no value given;\n" +
               "  option description: #{@options[i].description}\n"           
       end
     end
     if @options[i].type =~ /^float\s*vector$/                                
       while (@options[i].valuestring !~ /\]/)                                
         @options[i].valuestring += " " + argv_array.shift                    
       end
     end
   end
 
   def initialize_global_variables
     @options.each{|x| x.initialize_global_variable}
     check_required_options
   end    
 
   def check_required_options
     options_missing = 0
     @options.each do |x|
       if x.valuestring == "none"
         options_missing += 1
         STDERR.print "option "
         STDERR.print "\"#{x.shortname}\" or " if x.shortname
         STDERR.print "\"#{x.longname}\" required.  "
         STDERR.print "Description:\n#{x.longdescription}\n"
       end
     end
     if options_missing > 0
       STDERR.print "Please provide the required command line option"
       STDERR.print "s" if options_missing > 1
       STDERR.print ".\n"
       exit(1)
     end
   end
 
   def parse_help(argv_array, long)
     all = true
     while s = argv_array.shift
       if i = find_option(s)
         all = false
         print_help(long, i)
       end
     end
     print_help(long) if all
   end
 
   def print_help(long, i = nil)
     if i
       STDERR.print help_string(@options[i], long)
     else
       @options.each{|x| STDERR.print help_string(x, long)}
     end
   end
 
   def help_string(option, long_flag)
     s = ""
     if option.type
       s += option_name_string(option)
     end
     if option.type or not long_flag                                         
       s += "#{option.description}"                                          
       s += default_value_string(option)                                     
       s += "\n"                                                             
     end                                                                     
     if long_flag                                                            
       s += "\n#{option.longdescription}\n"                                  
     end                                                                     
     return s
   end
 
   def option_name_string(option)
     s = ""
     if option.shortname
       s += "#{option.shortname}  "
     end
     s += "#{option.longname}"
     s = option.add_tabs(s, s.size, 3)
     s += ": "
     return s
   end
 
   def default_value_string(option)
     s = ""
     if option.type and option.type != "bool"
       reference_size = "#{option.description}".size + 2
       s = option.add_tabs(s, reference_size, 4)
       s += " [default: #{option.defaultvalue}]"                             
     end
     return s
   end
 
 end
 
 def parse_command_line(def_str)
   Clop.new(def_str, ARGV)
 end
 
 if  _FILE_ == $0
 
   options_definition_string = <<-END
 
   Description: Command line option parser
   Long description:
     Test program for the class Clop (Command line option parser),
     (c) 2004, Piet Hut and Jun Makino; see ACS at www.artcompsi.org
 
     This program appears at the end of the file "clop.rb" that contains
     the definition of the Clop class.
     By running the file (typing "ruby clop.rb"), you can check whether
     it still behaves correctly.  Maximum help is provided by the command
     "ruby clop.rb --help".
 
 
   Short name:           -s
   Long name:            --softening_length
   Value type:           float              # double/real/...
   Default value:        0.0
   Global variable:      eps                # any comment allowed here
   Description:          Softening length   # and here too
   Long description:                        # and even here
     This option sets the softening length used to calculate the force
     between two particles.  The calculation scheme comforms to standard
     Plummer softening, where rs2=r**2+eps**2 is used in place of r**2.
 
 
   Short name:           -t
   Long name:            --end_time
   Value type:           float
   Default value:        10
   Global variable:      t_end
   Print name:           t
   Description:          Time to stop integration
   Long description:
     This option gives the time to stop integration.
 
 
   Short name:           -n
   Long name:            --number_of_particles
   Value type:           int
   Default value:        none
   Global variable:      n_particles
   Print name:           N
   Description:          Number of particles
   Long description:
     Number of particles in an N-body snapshot.
 
 
   Short name:           -x
   Long name:            --extra_diagnostics
   Value type:           bool
   Global variable:      xdiag
   Description:          Extra diagnostics
   Long description:
     The following extra diagnostics will be printed:
 
       acceleration (for all integrators)
       jerk (for the Hermite integrator)
 
 
   Short name:           -v
   Long name:            --shift_velocity
   Value type:           float vector          # numbers in between [ ] brackets
   Default value:        [3, 4, 5]
   Global variable:      vcom
   Description:          Shifts center of mass velocity
   Long description:
     The center of mass of the N-body system will be shifted by this amount.
     If the vector has fewer components than the dimensionality of the N-body
     system, zeroes will be added to the vector.
     If the vector has more components than the dimensionality of the N-body
     system, the extra components will be disgarded.
 
 
   Short name:           -o
   Long name:            --output_file_name
   Value type:           string
   Default value:        none
   Global variable:      output_file_name
   Print name:                                 # no name, hence name suppressed
   Description:          Name of the outputfile
   Long description:
     Name of the snapshot output file.
     The snapshot contains the mass, position, and velocity values
     for all particles in an N-body system.
 
 
   Long name:            --star_type             # no short option given here
   Value type:           string
   Default value:        star: MS                # parser cuts only at first ":"
   Global variable:      star_type
   Description:          Star type
   Long description:
     This options allows you to specify that a particle is a star, of a
     certain type T, and possibly of subtypes t1, t2, ..., tk by specifying
     --star_type "star: T: t1: t2: ...: tk".  The ":" separators are allowed
     to have blank spaces before and after them.
 
       Examples: --star_type "star: MS"
                 --star_type "star : MS : ZAMS"
                 --star_type "star: giant: AGB"
                 --star_type "star:NS:pulsar:millisecond pulsar"
 
   END
 
   parse_command_line(options_definition_string)
 
 end
Previous ToC Up Next