Files
2025-09-29 00:52:08 +02:00

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