453 lines
14 KiB
Ruby
Executable File
453 lines
14 KiB
Ruby
Executable File
#
|
|
# 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 <david.muir@rockstarnorth.com> (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
|