2014-07-09 22:59:03 +02:00
|
|
|
#!/usr/bin/env ruby
|
2014-12-03 00:13:28 +01:00
|
|
|
# Encoding: UTF-8
|
2014-07-09 22:59:03 +02:00
|
|
|
|
|
|
|
##
|
2015-03-01 00:56:00 +01:00
|
|
|
## EasyOptions 2015.2.28
|
2014-07-09 22:59:03 +02:00
|
|
|
## Copyright (c) 2013, 2014 Renato Silva
|
2015-03-01 00:56:00 +01:00
|
|
|
## BSD licensed
|
2014-07-09 22:59:03 +02:00
|
|
|
##
|
|
|
|
## This script is supposed to parse command line arguments in a way that,
|
|
|
|
## even though its implementation is not trivial, it should be easy and
|
|
|
|
## smooth to use. For using this script, simply document your target script
|
|
|
|
## using double-hash comments, like this:
|
|
|
|
##
|
|
|
|
## ## Program Name v1.0
|
|
|
|
## ## Copyright (C) Someone
|
|
|
|
## ##
|
|
|
|
## ## This program does something. Usage:
|
|
|
|
## ## @#script.name [option]
|
|
|
|
## ##
|
|
|
|
## ## Options:
|
|
|
|
## ## -h, --help All client scripts have this by default,
|
|
|
|
## ## it shows this double-hash documentation.
|
|
|
|
## ##
|
|
|
|
## ## -o, --option This option will get stored as true value
|
2014-12-02 01:48:25 +01:00
|
|
|
## ## under EasyOptions.options[:option]. Long
|
|
|
|
## ## version is mandatory, and can be specified
|
|
|
|
## ### before or after short version.
|
2014-07-09 22:59:03 +02:00
|
|
|
## ##
|
|
|
|
## ## --some-boolean This will get stored as true value under
|
2014-12-02 01:48:25 +01:00
|
|
|
## ## EasyOptions.options[:some_boolean].
|
2014-07-09 22:59:03 +02:00
|
|
|
## ##
|
|
|
|
## ## --some-value=VALUE This is going to store the VALUE specified
|
2014-12-02 01:48:25 +01:00
|
|
|
## ## under EasyOptions.options[:some_value].
|
|
|
|
## ## The equal sign is optional and can be
|
|
|
|
## ## replaced with blank space when running the
|
|
|
|
## ## target script. If VALUE is composed of
|
|
|
|
## ## digits, it will be converted into an
|
|
|
|
## ## integer, otherwise it will get stored as a
|
|
|
|
## ## string. Short version is not available in
|
|
|
|
## ## this format.
|
2014-07-09 22:59:03 +02:00
|
|
|
##
|
|
|
|
## The above comments work both as source code documentation and as help
|
|
|
|
## text, as well as define the options supported by your script. There is no
|
|
|
|
## duplication of the options specification. The string @#script.name will be
|
|
|
|
## replaced with the actual script name.
|
|
|
|
##
|
|
|
|
## After writing your documentation, you simply require this script. Then all
|
2014-12-02 01:48:25 +01:00
|
|
|
## command line options will get parsed into the EasyOptions.options hash, as
|
|
|
|
## described above. You can then check their values for reacting to them. All
|
|
|
|
## regular arguments will get stored into the EasyOptions.arguments array.
|
2014-07-09 22:59:03 +02:00
|
|
|
##
|
|
|
|
## In fact, this script is an example of itself. You are seeing this help
|
|
|
|
## message either because you are reading the source code, or you have called
|
|
|
|
## the script in command line with the --help option.
|
|
|
|
##
|
2014-12-03 00:15:46 +01:00
|
|
|
## Note: the options and arguments are also available as global variables in
|
|
|
|
## current version, but their use is discouraged and is supposed to be
|
|
|
|
## eventually removed.
|
|
|
|
##
|
2014-07-09 22:59:03 +02:00
|
|
|
## This script can be used from Bash scripts as well. If the $from environment
|
|
|
|
## variable is set, that will be assumed as the source Bash script from which to
|
|
|
|
## parse the documentation and the provided options. Then, instead of parsing
|
2014-09-11 05:48:49 +02:00
|
|
|
## the options into Ruby variables, evaluable assignment statements will be
|
|
|
|
## generated for the corresponding Bash environment variables. For example:
|
2014-07-09 22:59:03 +02:00
|
|
|
##
|
2014-07-24 03:31:27 +02:00
|
|
|
## eval "$(from="$0" @script.name "$@" || echo exit 1)"
|
2014-07-09 22:59:03 +02:00
|
|
|
##
|
|
|
|
## If the script containing this command is documented as in the example above,
|
|
|
|
## and it is executed from command line with the -o and --some-value=10 options,
|
|
|
|
## and one regular argument abc, then the evaluable output would look like this:
|
|
|
|
##
|
2014-09-11 05:48:49 +02:00
|
|
|
## option="yes"
|
|
|
|
## some_value="10"
|
2014-07-09 22:59:03 +02:00
|
|
|
## unset arguments
|
|
|
|
## arguments+=("abc")
|
2014-09-11 05:48:49 +02:00
|
|
|
## arguments
|
2014-07-09 22:59:03 +02:00
|
|
|
##
|
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
module EasyOptions
|
|
|
|
class Option
|
2014-12-03 00:22:35 +01:00
|
|
|
def initialize(long_version, short_version, boolean = true)
|
|
|
|
fail ArgumentError.new('Long version is mandatory') if !long_version || long_version.length < 2
|
2014-12-02 01:48:25 +01:00
|
|
|
@short = short_version.to_sym if short_version
|
2014-12-03 00:11:18 +01:00
|
|
|
@long = long_version.to_s.gsub('-', '_').to_sym
|
2014-12-02 01:48:25 +01:00
|
|
|
@boolean = boolean
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def to_s
|
|
|
|
"--#{long_dashed}"
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def in?(string)
|
2014-12-03 00:22:35 +01:00
|
|
|
string =~ /^--#{long_dashed}$/ || (@short && string =~ /^-#{@short}$/)
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def in_with_value?(string)
|
|
|
|
string =~ /^--#{long_dashed}=.*$/
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def long_dashed
|
2014-12-03 00:11:18 +01:00
|
|
|
@long.to_s.gsub('_', '-')
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
|
|
|
attr_accessor :short
|
|
|
|
attr_accessor :long
|
|
|
|
attr_accessor :boolean
|
2014-07-09 22:59:03 +02:00
|
|
|
end
|
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
class Parser
|
|
|
|
def initialize
|
|
|
|
@known_options = [Option.new(:help, :h)]
|
|
|
|
@documentation = parse_doc
|
|
|
|
@arguments = []
|
|
|
|
@options = {}
|
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def parse_doc
|
|
|
|
begin
|
2014-12-03 00:22:35 +01:00
|
|
|
doc = File.readlines($PROGRAM_NAME)
|
2014-12-02 01:48:25 +01:00
|
|
|
rescue Errno::ENOENT
|
|
|
|
exit false
|
|
|
|
end
|
|
|
|
doc = doc.find_all do |line|
|
|
|
|
line =~ /^##[^#]*/
|
|
|
|
end
|
2014-12-03 00:40:41 +01:00
|
|
|
doc.map do |line|
|
2014-12-02 01:48:25 +01:00
|
|
|
line.strip!
|
2014-12-03 00:11:18 +01:00
|
|
|
line.sub!(/^## ?/, '')
|
2014-12-03 00:22:35 +01:00
|
|
|
line.gsub!(/@script.name/, File.basename($PROGRAM_NAME))
|
2014-12-03 00:11:18 +01:00
|
|
|
line.gsub(/@#/, '@')
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def parse
|
|
|
|
# Parse known options from documentation
|
|
|
|
@documentation.map do |line|
|
|
|
|
line = line.strip
|
|
|
|
case line
|
2014-12-03 00:40:41 +01:00
|
|
|
when /^-h, --help.*/ then next
|
|
|
|
when /^--help, -h.*/ then next
|
|
|
|
when /^-.*, --.*/ then line = line.split(/(^-|,\s--|\s)/); @known_options << Option.new(line[4], line[2])
|
|
|
|
when /^--.*, -.*/ then line = line.split(/(--|,\s-|\s)/); @known_options << Option.new(line[2], line[4])
|
|
|
|
when /^--.*=.*/ then line = line.split(/(--|=|\s)/); @known_options << Option.new(line[2], nil, false)
|
|
|
|
when /^--.* .*/ then line = line.split(/(--|\s)/); @known_options << Option.new(line[2], nil)
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
# Format arguments input
|
|
|
|
raw_arguments = ARGV.map do |argument|
|
2014-12-03 00:22:35 +01:00
|
|
|
if argument =~ /^-[^-].*$/i
|
2014-12-03 00:11:18 +01:00
|
|
|
argument.split('')[1..-1].map { |char| "-#{char}" }
|
2014-12-02 01:48:25 +01:00
|
|
|
else
|
|
|
|
argument
|
|
|
|
end
|
|
|
|
end.flatten
|
2014-07-09 22:59:03 +02:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
# Parse the provided options
|
|
|
|
raw_arguments.each_with_index do |argument, index|
|
|
|
|
unknown_option = true
|
|
|
|
@known_options.each do |known_option|
|
|
|
|
|
|
|
|
# Boolean option
|
2014-12-03 00:22:35 +01:00
|
|
|
if known_option.in?(argument) && known_option.boolean
|
2014-12-02 01:48:25 +01:00
|
|
|
@options[known_option.long] = true
|
|
|
|
unknown_option = false
|
|
|
|
break
|
|
|
|
|
|
|
|
# Option with value in next parameter
|
2014-12-03 00:22:35 +01:00
|
|
|
elsif known_option.in?(argument) && !known_option.boolean
|
2014-12-02 01:48:25 +01:00
|
|
|
value = raw_arguments[index + 1]
|
2014-12-03 00:22:35 +01:00
|
|
|
Parser.finish("you must specify a value for #{known_option}") if !value || value.start_with?('-')
|
2014-12-02 01:48:25 +01:00
|
|
|
value = value.to_i if value =~ /^[0-9]+$/
|
|
|
|
@options[known_option.long] = value
|
|
|
|
unknown_option = false
|
|
|
|
break
|
|
|
|
|
|
|
|
# Option with value after equal sign
|
2014-12-03 00:22:35 +01:00
|
|
|
elsif known_option.in_with_value?(argument) && !known_option.boolean
|
2014-12-03 00:11:18 +01:00
|
|
|
value = argument.split('=')[1]
|
2014-12-02 01:48:25 +01:00
|
|
|
value = value.to_i if value =~ /^[0-9]+$/
|
|
|
|
@options[known_option.long] = value
|
|
|
|
unknown_option = false
|
|
|
|
break
|
|
|
|
|
|
|
|
# Long option with unnecessary value
|
2014-12-03 00:22:35 +01:00
|
|
|
elsif known_option.in_with_value?(argument) && known_option.boolean
|
2014-12-03 00:11:18 +01:00
|
|
|
value = argument.split('=')[1]
|
2014-12-02 01:48:25 +01:00
|
|
|
Parser.finish("#{known_option} does not accept a value (you specified \"#{value}\")")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Unrecognized option
|
2014-12-03 00:22:35 +01:00
|
|
|
Parser.finish("unrecognized option \"#{argument}\"") if unknown_option && argument.start_with?('-')
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# Help option
|
|
|
|
if @options[:help]
|
2014-12-03 00:22:35 +01:00
|
|
|
if BashOutput
|
2014-12-02 01:48:25 +01:00
|
|
|
print "printf '"
|
|
|
|
puts @documentation
|
|
|
|
puts "'"
|
2014-12-03 00:11:18 +01:00
|
|
|
puts 'exit'
|
2014-12-02 01:48:25 +01:00
|
|
|
else
|
|
|
|
puts @documentation
|
|
|
|
end
|
2014-12-03 00:40:41 +01:00
|
|
|
exit(-1)
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# Regular arguments
|
|
|
|
next_is_value = false
|
|
|
|
raw_arguments.each do |argument|
|
2014-12-03 00:22:35 +01:00
|
|
|
if argument.start_with?('-')
|
2014-12-03 00:40:41 +01:00
|
|
|
known_option = @known_options.find { |option| option.in?(argument) }
|
2014-12-03 00:22:35 +01:00
|
|
|
next_is_value = (known_option && !known_option.boolean)
|
2014-12-02 01:48:25 +01:00
|
|
|
else
|
2014-12-03 00:22:35 +01:00
|
|
|
arguments << argument unless next_is_value
|
2014-12-02 01:48:25 +01:00
|
|
|
next_is_value = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Bash support
|
2014-12-03 00:40:41 +01:00
|
|
|
return unless BashOutput
|
|
|
|
@options.keys.each do |name|
|
|
|
|
puts "#{name}=\"#{@options[name].to_s.sub('true', 'yes')}\""
|
|
|
|
end
|
|
|
|
puts 'unset arguments'
|
|
|
|
arguments.each do |argument|
|
|
|
|
puts "arguments+=(\"#{argument}\")"
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
end
|
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def self.finish(error)
|
2014-12-03 01:07:13 +01:00
|
|
|
warn "Error: #{error}."
|
|
|
|
warn 'See --help for usage and options.'
|
2014-12-03 00:11:18 +01:00
|
|
|
puts 'exit 1' if BashOutput
|
2014-12-02 01:48:25 +01:00
|
|
|
exit false
|
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def self.check_bash_output
|
2014-12-03 00:22:35 +01:00
|
|
|
$0 = ENV['from'] || $PROGRAM_NAME
|
|
|
|
$PROGRAM_NAME == ENV['from']
|
2014-12-02 01:48:25 +01:00
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
BashOutput = check_bash_output
|
|
|
|
attr_accessor :documentation
|
|
|
|
attr_accessor :arguments
|
|
|
|
attr_accessor :options
|
2014-07-09 22:59:03 +02:00
|
|
|
end
|
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
class << self
|
|
|
|
@@parser = Parser.new
|
|
|
|
@@parser.parse
|
|
|
|
def options
|
|
|
|
@@parser.options
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def arguments
|
|
|
|
@@parser.arguments
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def documentation
|
|
|
|
@@parser.documentation
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def all
|
|
|
|
[options, arguments, documentation]
|
|
|
|
end
|
2014-12-03 00:22:35 +01:00
|
|
|
|
2014-12-02 01:48:25 +01:00
|
|
|
def finish(error)
|
|
|
|
Parser.finish(error)
|
|
|
|
end
|
2014-07-09 22:59:03 +02:00
|
|
|
end
|
2014-12-02 01:48:25 +01:00
|
|
|
|
|
|
|
# This is supposed to be eventually removed
|
|
|
|
$documentation = @@parser.documentation
|
|
|
|
$arguments = @@parser.arguments
|
|
|
|
$options = @@parser.options
|
2014-07-09 22:59:03 +02:00
|
|
|
end
|