# # File:: getopt.rb # # Pipeline::OS::Getopt Class for reading command line arguments. # * Based off of the Getopt::Long class that's part of the Getopt Ruby Gem. # # Author:: David Muir (for trailing arguments) # Date:: 27 February 2008 # # Requirements: # * Getopt Ruby Gem # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/os/path' require 'pipeline/util/string' require 'getopt/long' require 'getopt/std' #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- module Pipeline module OS # # == Description # # Command line argument processor that improves on the Ruby Getopt Gem, by # supporting trailing arguments (i.e. typically filenames, paths at the end # of the command line). # # Note: The Getopt::NUMERIC type is not implemented in the upstream gem. # Use Getopt::REQUIRED or Getopt::OPTIONAL and the value will need to # be converted to a string in your app script. # # == Example Usage # # In script.rb: # # OPTIONS = [ # [ '--version', '-v', Getopt::BOOLEAN ], # [ '--help', '-h', Getopt::BOOLEAN ], # [ '--recurse', '-r', Getopt::BOOLEAN ], # [ '--dimensions', '-d', Getopt::BOOLEAN ], # [ '--compression', '-c', Getopt::BOOLEAN ], # [ '--filter', '-f', Getopt::REQUIRED ] # ] # # opts, targs = Pipeline::OS::Getopt.getopts( OPTIONS ) # # Invoked as: # # script.rb --version --help -f=*.dds dds_file_path # # Returns: # # opts => { # "version" => true, # "v" => true, # "help" => true, # "h" => true, # "filter" => "*.dds", # "f" => "*.dds" # } # targs => [ # "dds_file_path" # "test.dds" # ] # class Getopt class Error < ::Getopt::Long::Error; end #--------------------------------------------------------------------- # Constants #--------------------------------------------------------------------- REQUIRED = 0 BOOLEAN = 1 OPTIONAL = 2 INCREMENT = 3 NEGATABLE = 4 NUMERIC = 5 # Set of default options for our pipeline scripts; for log handling, # debug, help info etc. Member variables for these are stored in the # Pipeline::Globals object. DEFAULT_OPTIONS = [ ] # Takes an array of switches. Each array consists of up to four # elements that indicate the name, type of switch and help string. # Returns a hash containing each switch name, minus the '-', as a key # and an array of trailing arguments. The value for each key depends # on the type of switch and/or the value provided by the user. # # The long switch _must_ be provided. The short switch defaults to the # first letter of the short switch. The default type is BOOLEAN. # # == Example Usage # # opts, trailing = Pipeline::OS::Getopt.getopts( # ["--debug", "-d", Getopt::BOOLEAN, "Enable debugging information" ], # ["--verbose", "-v", Getopt::BOOLEAN, "Spout stuff as we are running" ], # ["--level", "-l", Getopt::NUMERIC, "Which level to run?" ] # ) # # See the README file for more information. # def self.getopts( switches ) switches = ( switches + DEFAULT_OPTIONS ) hash = {} trailing_args = [] # Preparse our ARGV array looking for trailing arguments. Using the # information in the specified switches for optional/required # arguments. These trailing arguments should be removed prior to # passing to the getopts_base() function. ARGV.each_with_index do |arg, index| # Ignore -- and - arguments next if ( arg.starts_with( '--' ) || arg.starts_with( '-' ) ) trailing_args << arg end # Remove trailing arguments from ARGV, for processing by the # ::Getopt::Long.getopts() function. trailing_args.each do |arg| ARGV.delete( arg ) end # Process the updated ARGV command line using ::Getopt::Long.getopts # function. begin hash = getopts_base( switches ) rescue ::Getopt::Long::Error => error raise Error.new( error.message ) end return hash, trailing_args end # Takes an array of switches. Each array consists of up to four # elements that indicate the name, type of switch and help string. # # Returns a string containing usage information generated from the # switches array in a nicely formatted way. This can then be output # to file or an appropriate IO device. # # == Example Usage # # puts Pipeline::OS::Getopt.usage( # ["--debug", "-d", Getopt::BOOLEAN, "Enable debugging information" ], # ["--verbose", "-v", Getopt::BOOLEAN, "Spout stuff as we are running" ], # ["--level", "-l", Getopt::NUMERIC, "Which level to run?" ] # ) # def self.usage( switches, trailing_args_desc = {} ) message = sprintf( "\n %s [OPTIONS] ", OS::Path.get_filename( $0 ) ) trailing_args_desc.each_key do |k| message += sprintf( "%s ", k ) end message += "\n" switches.each_with_index do |switch, index| if ( 4 == switch.size ) then case switch[2] when BOOLEAN then type = '' when NUMERIC then type = '=ARG' when REQUIRED then type = '=ARG' when OPTIONAL then type = '=[ARG]' end res = split_string( switch[3], 0 ) message += sprintf( " %-20s%27.50s\n", \ switch[0] + type, res[0] ) res.each_with_index do |str, index| next unless index > 0 message += sprintf( "%7s%-20s%27.50s\n", '', '', str ) end elsif ( ( 3 == switch.size ) or ( 2 == switch.size ) ) then message += sprintf( "%7s%-20s\tNo usage information defined.\n", \ switch[1] + ', ', switch[0] ) elsif ( 1 == switch.size ) then message += sprintf( "%7s%-20s\tNo usage information defined.\n", \ '', switch[0] ) end end # Output trailing arguments description trailing_args_desc.each do |k, v| res = split_string( v, 0 ) message += sprintf( "%7s%-20s%27.50s\n", '', k, res[0] ) res.each_with_index do |str, index| next unless index > 0 message += sprintf( "%7s%-20s%27.50s\n", '', '', str ) end end message end #--------------------------------------------------------------------- # Private Functions #--------------------------------------------------------------------- private # # A copy of Getopt::getopts function but modified to take an array # argument rather than the more conventional variable argument list. # # As then we can store the program options in a constant array and # pass them to Getopt::getopts and Getopt::usage. # def self.getopts_base( switches ) raise ArgumentError, "no switches provided" if switches.empty? hash = {} # Hash returned to user valid = [] # Tracks valid switches types = {} # Tracks argument types syns = {} # Tracks long and short arguments, or multiple shorts # If a string is passed, split it and convert it to an array of arrays if switches.first.kind_of?(String) switches = switches.join.split switches.map!{ |switch| switch = [switch] } end # Set our list of valid switches, and proper types for each switch switches.each { |switch| valid.push(switch[0]) # Set valid long switches # Set type for long switch, default to BOOLEAN. if switch[1].kind_of?(Fixnum) switch[2] = switch[1] types[switch[0]] = switch[2] switch[1] = switch[0][1..2] else switch[2] ||= BOOLEAN types[switch[0]] = switch[2] switch[1] ||= switch[0][1..2] end # Create synonym hash. Default to first char of long switch for # short switch, e.g. "--verbose" creates a "-v" synonym. The same # synonym can only be used once - first one wins. syns[switch[0]] = switch[1] unless syns[switch[1]] syns[switch[1]] = switch[0] unless syns[switch[1]] switch[1].each{ |char| types[char] = switch[2] # Set type for short switch valid.push(char) # Set valid short switches } } re_long = /^(--\w+[-\w+]*)?$/ re_short = /^(-\w)$/ re_long_eq = /^(--\w+[-\w+]*)?=((.*|\".*\")?)$|(-\w?)=((.*|\".*\")?)$/ re_short_sq = /^(-\w)(\S+?)$/ ARGV.each_with_index { |opt, index| # Allow either -x -v or -xv style for single char args if re_short_sq.match(opt) chars = opt.split("")[1..-1].map{ |s| s = "-#{s}" } chars.each_with_index{ |char, i| unless valid.include?(char) raise Error, "invalid switch '#{char}'" end # Grab the next arg if the switch takes a required arg if types[char] == REQUIRED # Deal with a argument squished up against switch if chars[i+1] arg = chars[i+1..-1].join.tr("-","") ARGV.push(char, arg) break else arg = ARGV.delete_at(index+1) if arg.nil? || valid.include?(arg) # Minor cheat here err = "no value provided for required argument '#{char}'" raise Error, err end ARGV.push(char, arg) end elsif types[char] == OPTIONAL if chars[i+1] && !valid.include?(chars[i+1]) arg = chars[i+1..-1].join.tr("-","") ARGV.push(char, arg) break elsif if ARGV[index+1] && !valid.include?(ARGV[index+1]) arg = ARGV.delete_at(index+1) ARGV.push(char, arg) end else ARGV.push(char) end else ARGV.push(char) end } next end if match = re_long.match(opt) || match = re_short.match(opt) switch = match.captures.first end if match = re_long_eq.match(opt) switch, value = match.captures.compact ARGV.push(switch, value) next end # Make sure that all the switches are valid. If 'switch' isn't # defined at this point, it means an option was passed without # a preceding switch, e.g. --option foo bar. unless valid.include?(switch) switch ||= opt raise Error, "invalid switch '#{switch}'" end # Required arguments if types[switch] == ::Getopt::REQUIRED nextval = ARGV[index+1] # Make sure there's a value for mandatory arguments if nextval.nil? err = "no value provided for required argument '#{switch}'" raise Error, err end # If there is a value, make sure it's not another switch if valid.include?(nextval) err = "cannot pass switch '#{nextval}' as an argument" raise Error, err end # If the same option appears more than once, put the values # in array. if hash[switch] hash[switch] = [hash[switch], nextval].flatten else hash[switch] = nextval end ARGV.delete_at(index+1) end # For boolean arguments set the switch's value to true. if types[switch] == ::Getopt::BOOLEAN if hash.has_key?(switch) raise Error, "boolean switch already set" end hash[switch] = true end # For increment arguments, set the switch's value to 0, or # increment it by one if it already exists. if types[switch] == ::Getopt::INCREMENT if hash.has_key?(switch) hash[switch] += 1 else hash[switch] = 1 end end # For optional argument, there may be an argument. If so, it # cannot be another switch. If not, it is set to true. if types[switch] == ::Getopt::OPTIONAL nextval = ARGV[index+1] if valid.include?(nextval) hash[switch] = true else hash[switch] = nextval ARGV.delete_at(index+1) end end } # Set synonymous switches to the same value, e.g. if -t is a synonym # for --test, and the user passes "--test", then set "-t" to the same # value that "--test" was set to. # # This allows users to refer to the long or short switch and get # the same value hash.each{ |switch, val| if syns.keys.include?(switch) syns[switch].each{ |key| hash[key] = val } end } # Get rid of leading "--" and "-" to make it easier to reference hash.each{ |key, value| if key[0,2] == '--' nkey = key.sub('--', '') else nkey = key.sub('-', '') end hash.delete(key) hash[nkey] = value } hash end # Return an array of strings def self.split_string( data, init = 0, size = LONG_DESC_CUT ) result = [] count = init while ( count < data.size ) d = data.slice( count, size ).strip() if d.starts_with( /[A-Za-z0-9_\.]/i ) and result.size > 0 d = " -" + d end result << d count += size end result end #--------------------------------------------------------------------- # Private Constants #--------------------------------------------------------------------- private LONG_DESC_CUT = 50 end end # OS module end # Pipeline module # End of getopt.rb