diff options
Diffstat (limited to 'lib/optparse.rb')
| -rw-r--r-- | lib/optparse.rb | 754 |
1 files changed, 523 insertions, 231 deletions
diff --git a/lib/optparse.rb b/lib/optparse.rb index 272b99d411..a61eff30c4 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # optparse.rb - command-line option analysis with the OptionParser class. # @@ -8,6 +9,7 @@ # +#-- # == Developer Documentation (not for RDoc output) # # === Class tree @@ -42,8 +44,14 @@ # | all instances)| # +---------------+ # +#++ +# # == OptionParser # +# === New to \OptionParser? +# +# See the {Tutorial}[optparse/tutorial.rdoc]. +# # === Introduction # # OptionParser is a class for command-line option analysis. It is much more @@ -68,10 +76,10 @@ # require 'optparse' # # options = {} -# OptionParser.new do |opts| -# opts.banner = "Usage: example.rb [options]" +# OptionParser.new do |parser| +# parser.banner = "Usage: example.rb [options]" # -# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| +# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v| # options[:verbose] = v # end # end.parse! @@ -79,6 +87,181 @@ # p options # p ARGV # +# === Generating Help +# +# OptionParser can be used to automatically generate help for the commands you +# write: +# +# require 'optparse' +# +# Options = Struct.new(:name) +# +# class Parser +# def self.parse(options) +# args = Options.new("world") +# +# opt_parser = OptionParser.new do |parser| +# parser.banner = "Usage: example.rb [options]" +# +# parser.on("-nNAME", "--name=NAME", "Name to say hello to") do |n| +# args.name = n +# end +# +# parser.on("-h", "--help", "Prints this help") do +# puts parser +# exit +# end +# end +# +# opt_parser.parse!(options) +# return args +# end +# end +# options = Parser.parse %w[--help] +# +# #=> +# # Usage: example.rb [options] +# # -n, --name=NAME Name to say hello to +# # -h, --help Prints this help +# +# === Required Arguments +# +# For options that require an argument, option specification strings may include an +# option name in all caps. If an option is used without the required argument, +# an exception will be raised. +# +# require 'optparse' +# +# options = {} +# OptionParser.new do |parser| +# parser.on("-r", "--require LIBRARY", +# "Require the LIBRARY before executing your script") do |lib| +# puts "You required #{lib}!" +# end +# end.parse! +# +# Used: +# +# $ ruby optparse-test.rb -r +# optparse-test.rb:9:in `<main>': missing argument: -r (OptionParser::MissingArgument) +# $ ruby optparse-test.rb -r my-library +# You required my-library! +# +# === Type Coercion +# +# OptionParser supports the ability to coerce command line arguments +# into objects for us. +# +# OptionParser comes with a few ready-to-use kinds of type +# coercion. They are: +# +# - Date -- Anything accepted by +Date.parse+ +# - DateTime -- Anything accepted by +DateTime.parse+ +# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ +# - URI -- Anything accepted by +URI.parse+ +# - Shellwords -- Anything accepted by +Shellwords.shellwords+ +# - String -- Any non-empty string +# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040) +# - Float -- Any float. (e.g. 10, 3.14, -100E+13) +# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3) +# - DecimalInteger -- Like +Integer+, but no octal format. +# - OctalInteger -- Like +Integer+, but no decimal format. +# - DecimalNumeric -- Decimal integer or float. +# - TrueClass -- Accepts '+, yes, true, -, no, false' and +# defaults as +true+ +# - FalseClass -- Same as +TrueClass+, but defaults to +false+ +# - Array -- Strings separated by ',' (e.g. 1,2,3) +# - Regexp -- Regular expressions. Also includes options. +# +# We can also add our own coercions, which we will cover below. +# +# ==== Using Built-in Conversions +# +# As an example, the built-in +Time+ conversion is used. The other built-in +# conversions behave in the same way. +# OptionParser will attempt to parse the argument +# as a +Time+. If it succeeds, that time will be passed to the +# handler block. Otherwise, an exception will be raised. +# +# require 'optparse' +# require 'optparse/time' +# OptionParser.new do |parser| +# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| +# p time +# end +# end.parse! +# +# Used: +# +# $ ruby optparse-test.rb -t nonsense +# ... invalid argument: -t nonsense (OptionParser::InvalidArgument) +# $ ruby optparse-test.rb -t 10-11-12 +# 2010-11-12 00:00:00 -0500 +# $ ruby optparse-test.rb -t 9:30 +# 2014-08-13 09:30:00 -0400 +# +# ==== Creating Custom Conversions +# +# The +accept+ method on OptionParser may be used to create converters. +# It specifies which conversion block to call whenever a class is specified. +# The example below uses it to fetch a +User+ object before the +on+ handler receives it. +# +# require 'optparse' +# +# User = Struct.new(:id, :name) +# +# def find_user id +# not_found = ->{ raise "No User Found for id #{id}" } +# [ User.new(1, "Sam"), +# User.new(2, "Gandalf") ].find(not_found) do |u| +# u.id == id +# end +# end +# +# op = OptionParser.new +# op.accept(User) do |user_id| +# find_user user_id.to_i +# end +# +# op.on("--user ID", User) do |user| +# puts user +# end +# +# op.parse! +# +# Used: +# +# $ ruby optparse-test.rb --user 1 +# #<struct User id=1, name="Sam"> +# $ ruby optparse-test.rb --user 2 +# #<struct User id=2, name="Gandalf"> +# $ ruby optparse-test.rb --user 3 +# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError) +# +# === Store options to a Hash +# +# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash. +# +# require 'optparse' +# +# options = {} +# OptionParser.new do |parser| +# parser.on('-a') +# parser.on('-b NUM', Integer) +# parser.on('-v', '--verbose') +# end.parse!(into: options) +# +# p options +# +# Used: +# +# $ ruby optparse-test.rb -a +# {:a=>true} +# $ ruby optparse-test.rb -a -v +# {:a=>true, :verbose=>true} +# $ ruby optparse-test.rb -a -b 100 +# {:a=>true, :b=>100} +# # === Complete example # # The following example is a complete Ruby program. You can run it and see the @@ -91,110 +274,143 @@ # require 'pp' # # class OptparseExample +# Version = '1.0.0' # # CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary] # CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" } # -# # -# # Return a structure describing the options. -# # -# def self.parse(args) -# # The options specified on the command line will be collected in *options*. -# # We set default values here. -# options = OpenStruct.new -# options.library = [] -# options.inplace = false -# options.encoding = "utf8" -# options.transfer_type = :auto -# options.verbose = false -# -# opts = OptionParser.new do |opts| -# opts.banner = "Usage: example.rb [options]" -# -# opts.separator "" -# opts.separator "Specific options:" -# -# # Mandatory argument. -# opts.on("-r", "--require LIBRARY", -# "Require the LIBRARY before executing your script") do |lib| -# options.library << lib +# class ScriptOptions +# attr_accessor :library, :inplace, :encoding, :transfer_type, +# :verbose, :extension, :delay, :time, :record_separator, +# :list +# +# def initialize +# self.library = [] +# self.inplace = false +# self.encoding = "utf8" +# self.transfer_type = :auto +# self.verbose = false +# end +# +# def define_options(parser) +# parser.banner = "Usage: example.rb [options]" +# parser.separator "" +# parser.separator "Specific options:" +# +# # add additional options +# perform_inplace_option(parser) +# delay_execution_option(parser) +# execute_at_time_option(parser) +# specify_record_separator_option(parser) +# list_example_option(parser) +# specify_encoding_option(parser) +# optional_option_argument_with_keyword_completion_option(parser) +# boolean_verbose_option(parser) +# +# parser.separator "" +# parser.separator "Common options:" +# # No argument, shows at tail. This will print an options summary. +# # Try it and see! +# parser.on_tail("-h", "--help", "Show this message") do +# puts parser +# exit # end +# # Another typical switch to print the version. +# parser.on_tail("--version", "Show version") do +# puts Version +# exit +# end +# end # -# # Optional argument; multi-line description. -# opts.on("-i", "--inplace [EXTENSION]", -# "Edit ARGV files in place", -# " (make backup if EXTENSION supplied)") do |ext| -# options.inplace = true -# options.extension = ext || '' -# options.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot. +# def perform_inplace_option(parser) +# # Specifies an optional option argument +# parser.on("-i", "--inplace [EXTENSION]", +# "Edit ARGV files in place", +# "(make backup if EXTENSION supplied)") do |ext| +# self.inplace = true +# self.extension = ext || '' +# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot. # end +# end # +# def delay_execution_option(parser) # # Cast 'delay' argument to a Float. -# opts.on("--delay N", Float, "Delay N seconds before executing") do |n| -# options.delay = n +# parser.on("--delay N", Float, "Delay N seconds before executing") do |n| +# self.delay = n # end +# end # +# def execute_at_time_option(parser) # # Cast 'time' argument to a Time object. -# opts.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| -# options.time = time +# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| +# self.time = time # end +# end # +# def specify_record_separator_option(parser) # # Cast to octal integer. -# opts.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger, -# "Specify record separator (default \\0)") do |rs| -# options.record_separator = rs +# parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger, +# "Specify record separator (default \\0)") do |rs| +# self.record_separator = rs # end +# end # +# def list_example_option(parser) # # List of arguments. -# opts.on("--list x,y,z", Array, "Example 'list' of arguments") do |list| -# options.list = list +# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list| +# self.list = list # end +# end # +# def specify_encoding_option(parser) # # Keyword completion. We are specifying a specific set of arguments (CODES # # and CODE_ALIASES - notice the latter is a Hash), and the user may provide # # the shortest unambiguous text. -# code_list = (CODE_ALIASES.keys + CODES).join(',') -# opts.on("--code CODE", CODES, CODE_ALIASES, "Select encoding", -# " (#{code_list})") do |encoding| -# options.encoding = encoding +# code_list = (CODE_ALIASES.keys + CODES).join(', ') +# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding", +# "(#{code_list})") do |encoding| +# self.encoding = encoding # end +# end # -# # Optional argument with keyword completion. -# opts.on("--type [TYPE]", [:text, :binary, :auto], -# "Select transfer type (text, binary, auto)") do |t| -# options.transfer_type = t +# def optional_option_argument_with_keyword_completion_option(parser) +# # Optional '--type' option argument with keyword completion. +# parser.on("--type [TYPE]", [:text, :binary, :auto], +# "Select transfer type (text, binary, auto)") do |t| +# self.transfer_type = t # end +# end # +# def boolean_verbose_option(parser) # # Boolean switch. -# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| -# options.verbose = v +# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v| +# self.verbose = v # end +# end +# end # -# opts.separator "" -# opts.separator "Common options:" -# -# # No argument, shows at tail. This will print an options summary. -# # Try it and see! -# opts.on_tail("-h", "--help", "Show this message") do -# puts opts -# exit -# end +# # +# # Return a structure describing the options. +# # +# def parse(args) +# # The options specified on the command line will be collected in +# # *options*. # -# # Another typical switch to print the version. -# opts.on_tail("--version", "Show version") do -# puts OptionParser::Version.join('.') -# exit -# end +# @options = ScriptOptions.new +# @args = OptionParser.new do |parser| +# @options.define_options(parser) +# parser.parse!(args) # end +# @options +# end # -# opts.parse!(args) -# options -# end # parse() -# +# attr_reader :parser, :options # end # class OptparseExample # -# options = OptparseExample.parse(ARGV) -# pp options +# example = OptparseExample.new +# options = example.parse(ARGV) +# pp options # example.options +# pp ARGV # # === Shell Completion # @@ -203,16 +419,15 @@ # # === Further documentation # -# The above examples should be enough to learn how to use this class. If you -# have any questions, file a ticket at http://bugs.ruby-lang.org. +# The above examples, along with the accompanying +# {Tutorial}[optparse/tutorial.rdoc], +# should be enough to learn how to use this class. +# If you have any questions, file a ticket at http://bugs.ruby-lang.org. # class OptionParser - # :stopdoc: - RCSID = %w$Id$[1..-1].each {|s| s.freeze}.freeze - Version = (RCSID[1].split('.').collect {|s| s.to_i}.extend(Comparable).freeze if RCSID[1]) - LastModified = (Time.gm(*RCSID[2, 2].join('-').scan(/\d+/).collect {|s| s.to_i}) if RCSID[2]) - Release = RCSID[2] + OptionParser::Version = "0.2.0" + # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze @@ -232,7 +447,7 @@ class OptionParser candidates = [] block.call do |k, *v| (if Regexp === k - kn = nil + kn = "" k === key else kn = defined?(k.id2name) ? k.id2name : k @@ -327,8 +542,9 @@ class OptionParser def initialize(pattern = nil, conv = nil, short = nil, long = nil, arg = nil, - desc = ([] if short or long), block = Proc.new) + desc = ([] if short or long), block = nil, &_block) raise if Array === pattern + block ||= _block @pattern, @conv, @short, @long, @arg, @desc, @block = pattern, conv, short, long, arg, desc, block end @@ -337,7 +553,7 @@ class OptionParser # Parses +arg+ and returns rest of +arg+ and matched portion to the # argument pattern. Yields when the pattern doesn't match substring. # - def parse_arg(arg) + def parse_arg(arg) # :nodoc: pattern or return nil, [arg] unless m = pattern.match(arg) yield(InvalidArgument, arg) @@ -362,7 +578,7 @@ class OptionParser # conversion. Yields at semi-error condition instead of raising an # exception. # - def conv_arg(arg, val = []) + def conv_arg(arg, val = []) # :nodoc: if conv val = conv.call(*val) else @@ -384,7 +600,7 @@ class OptionParser # +max+ columns. # +indent+:: Prefix string indents all summarized lines. # - def summarize(sdone = [], ldone = [], width = 1, max = width - 1, indent = "") + def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "") sopts, lopts = [], [], nil @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long @@ -396,8 +612,8 @@ class OptionParser while s = lopts.shift l = left[-1].length + s.length l += arg.length if left.size == 1 && arg - l < max or sopts.empty? or left << '' - left[-1] << if left[-1].empty? then ' ' * 4 else ', ' end << s + l < max or sopts.empty? or left << +'' + left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s end if arg @@ -447,8 +663,8 @@ class OptionParser return if sopts.empty? and lopts.empty? # completely hidden (sopts+lopts).each do |opt| - # "(-x -c -r)-l[left justify]" \ - if opt =~ /^--\[no-\](.+)$/ + # "(-x -c -r)-l[left justify]" + if /^--\[no-\](.+)$/ =~ opt o = $1 yield("--#{o}", desc.join("")) yield("--no-#{o}", desc.join("")) @@ -596,7 +812,7 @@ class OptionParser # +lopts+:: Long style option list. # +nlopts+:: Negated long style options list. # - def update(sw, sopts, lopts, nsw = nil, nlopts = nil) + def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc: sopts.each {|o| @short[o] = sw} if sopts lopts.each {|o| @long[o] = sw} if lopts nlopts.each {|o| @long[o] = nsw} if nsw and nlopts @@ -658,6 +874,10 @@ class OptionParser __send__(id).complete(opt, icase, *pat, &block) end + def get_candidates(id) + yield __send__(id).keys + end + # # Iterates over each option, passing the option to the +block+. # @@ -733,7 +953,7 @@ class OptionParser # OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL) # # Use like --switch=argument (long style) or -Xargument (short style). For - # short style, only portion matched to argument pattern is dealed as + # short style, only portion matched to argument pattern is treated as # argument. # ArgumentStyle = {} @@ -877,6 +1097,7 @@ XXX @summary_width = width @summary_indent = indent @default_argv = ARGV + @require_exact = false add_officious yield self if block_given? end @@ -950,12 +1171,16 @@ XXX # Strings to be parsed in default. attr_accessor :default_argv + # Whether to require that options match exactly (disallows providing + # abbreviated long option as short option). + attr_accessor :require_exact + # # Heading banner preceding summary. # def banner unless @banner - @banner = "Usage: #{program_name} [options]" + @banner = +"Usage: #{program_name} [options]" visit(:add_banner, @banner) end @banner @@ -984,14 +1209,14 @@ XXX # Version # def version - @version || (defined?(::Version) && ::Version) + (defined?(@version) && @version) || (defined?(::Version) && ::Version) end # # Release code # def release - @release || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE) + (defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE) end # @@ -999,7 +1224,7 @@ XXX # def ver if v = version - str = "#{program_name} #{[v].join('.')}" + str = +"#{program_name} #{[v].join('.')}" str << " (#{v})" if v = release str end @@ -1056,7 +1281,8 @@ XXX # +indent+:: Indentation, defaults to @summary_indent. # def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk) - blk ||= proc {|l| to << (l.index($/, -1) ? l : l + $/)} + nl = "\n" + blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)} visit(:summarize, {}, {}, width, max, indent, &blk) to end @@ -1080,7 +1306,7 @@ XXX # +prv+:: Previously specified argument. # +msg+:: Exception message. # - def notwice(obj, prv, msg) + def notwice(obj, prv, msg) # :nodoc: unless !prv or prv == obj raise(ArgumentError, "argument #{msg} given twice: #{obj}", ParseError.filter_backtrace(caller(2))) @@ -1089,64 +1315,12 @@ XXX end private :notwice - SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} - # - # Creates an OptionParser::Switch from the parameters. The parsed argument - # value is passed to the given block, where it can be processed. - # - # See at the beginning of OptionParser for some full examples. - # - # +opts+ can include the following elements: - # - # [Argument style:] - # One of the following: - # :NONE, :REQUIRED, :OPTIONAL - # - # [Argument pattern:] - # Acceptable option argument format, must be pre-defined with - # OptionParser.accept or OptionParser#accept, or Regexp. This can appear - # once or assigned as String if not present, otherwise causes an - # ArgumentError. Examples: - # Float, Time, Array - # - # [Possible argument values:] - # Hash or Array. - # [:text, :binary, :auto] - # %w[iso-2022-jp shift_jis euc-jp utf8 binary] - # { "jis" => "iso-2022-jp", "sjis" => "shift_jis" } - # - # [Long style switch:] - # Specifies a long style switch which takes a mandatory, optional or no - # argument. It's a string of the following form: - # "--switch=MANDATORY" or "--switch MANDATORY" - # "--switch[=OPTIONAL]" - # "--switch" - # - # [Short style switch:] - # Specifies short style switch which takes a mandatory, optional or no - # argument. It's a string of the following form: - # "-xMANDATORY" - # "-x[OPTIONAL]" - # "-x" - # There is also a special form which matches character range (not full - # set of regular expression): - # "-[a-z]MANDATORY" - # "-[a-z][OPTIONAL]" - # "-[a-z]" - # - # [Argument style and description:] - # Instead of specifying mandatory or optional arguments directly in the - # switch parameter, this separate parameter can be used. - # "=MANDATORY" - # "=[OPTIONAL]" - # - # [Description:] - # Description string for the option. - # "Run verbosely" - # - # [Handler:] - # Handler for the parsed argument value. Either give a block or pass a - # Proc or Method as an argument. + SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc: + + # :call-seq: + # make_switch(params, block = nil) + # + # :include: ../doc/optparse/creates_option.rdoc # def make_switch(opts, block = nil) short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], [] @@ -1155,6 +1329,7 @@ XXX default_pattern = nil klass = nil q, a = nil + has_arg = false opts.each do |o| # argument class @@ -1204,7 +1379,8 @@ XXX default_style = Switch::NoArgument default_pattern, conv = search(:atype, FalseClass) unless default_pattern ldesc << "--no-#{q}" - long << 'no-' + (q = q.downcase) + (q = q.downcase).tr!('_', '-') + long << "no-#{q}" nolong << q when /^--\[no-\]([^\[\]=\s]*)(.+)?/ q, a = $1, $2 @@ -1214,10 +1390,11 @@ XXX default_pattern, conv = search(:atype, o) unless default_pattern end ldesc << "--[no-]#{q}" - long << (o = q.downcase) + (o = q.downcase).tr!('_', '-') + long << o not_pattern, not_conv = search(:atype, FalseClass) unless not_style not_style = Switch::NoArgument - nolong << 'no-' + o + nolong << "no-#{o}" when /^--([^\[\]=\s]*)(.+)?/ q, a = $1, $2 if a @@ -1226,13 +1403,16 @@ XXX default_pattern, conv = search(:atype, o) unless default_pattern end ldesc << "--#{q}" - long << (o = q.downcase) + (o = q.downcase).tr!('_', '-') + long << o when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ q, a = $1, $2 o = notwice(Object, klass, 'type') if a default_style = default_style.guess(arg = a) default_pattern, conv = search(:atype, o) unless default_pattern + else + has_arg = true end sdesc << "-#{q}" short << Regexp.new(q) @@ -1255,6 +1435,9 @@ XXX default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern if !(short.empty? and long.empty?) + if has_arg and default_style == Switch::NoArgument + default_style = Switch::RequiredArgument + end s = (style || default_style).new(pattern || default_pattern, conv, sdesc, ldesc, arg, desc, block) elsif !block @@ -1272,14 +1455,20 @@ XXX nolong end + # :call-seq: + # define(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # def define(*opts, &block) top.append(*(sw = make_switch(opts, block))) sw[0] end + # :call-seq: + # on(*params, &block) # - # Add option switch and handler. See #make_switch for an explanation of - # parameters. + # :include: ../doc/optparse/creates_option.rdoc # def on(*opts, &block) define(*opts, &block) @@ -1287,13 +1476,22 @@ XXX end alias def_option define + # :call-seq: + # define_head(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # def define_head(*opts, &block) top.prepend(*(sw = make_switch(opts, block))) sw[0] end + # :call-seq: + # on_head(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc # - # Add option switch like with #on, but at head of summary. + # The new option is added at the head of the summary. # def on_head(*opts, &block) define_head(*opts, &block) @@ -1301,13 +1499,23 @@ XXX end alias def_head_option define_head + # :call-seq: + # define_tail(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # def define_tail(*opts, &block) base.append(*(sw = make_switch(opts, block))) sw[0] end # - # Add option switch like with #on, but at tail of summary. + # :call-seq: + # on_tail(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + # The new option is added at the tail of the summary. # def on_tail(*opts, &block) define_tail(*opts, &block) @@ -1324,20 +1532,25 @@ XXX # # Parses command line arguments +argv+ in order. When a block is given, - # each non-option argument is yielded. + # each non-option argument is yielded. When optional +into+ keyword + # argument is provided, the parsed option values are stored there via + # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other + # similar object). # # Returns the rest of +argv+ left unparsed. # - def order(*argv, &block) + def order(*argv, into: nil, &nonopt) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - order!(argv, &block) + order!(argv, into: into, &nonopt) end # # Same as #order, but removes switches destructively. + # Non-option arguments remain in +argv+. # - def order!(argv = default_argv, &nonopt) - parse_in_order(argv, &nonopt) + def order!(argv = default_argv, into: nil, &nonopt) + setter = ->(name, val) {into[name.to_sym] = val} if into + parse_in_order(argv, setter, &nonopt) end def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: @@ -1349,8 +1562,12 @@ XXX # long option when /\A--([^=]*)(?:=(.*))?/m opt, rest = $1, $2 + opt.tr!('_', '-') begin sw, = complete(:long, opt, true) + if require_exact && !sw.long.include?(arg) + raise InvalidOption, arg + end rescue ParseError raise $!.set_option(arg, true) end @@ -1364,16 +1581,18 @@ XXX # short option when /\A-(.)((=).*|.+)?/m - opt, has_arg, eq, val, rest = $1, $3, $3, $2, $2 + eq, rest, opt = $3, $2, $1 + has_arg, val = eq, rest begin sw, = search(:short, opt) unless sw begin sw, = complete(:short, opt) # short option matched. - val = arg.sub(/\A-/, '') + val = arg.delete_prefix('-') has_arg = true rescue InvalidOption + raise if require_exact # if no short options match, try completion with long # options. sw, = complete(:long, opt) @@ -1385,7 +1604,12 @@ XXX end begin opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} + rescue ParseError + raise $!.set_option(arg, arg.length > 2) + else raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}" + end + begin argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-') val = cb.call(val) if cb setter.call(sw.switch_name, val) if setter @@ -1416,19 +1640,23 @@ XXX # # Parses command line arguments +argv+ in permutation mode and returns - # list of non-option arguments. + # list of non-option arguments. When optional +into+ keyword + # argument is provided, the parsed option values are stored there via + # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other + # similar object). # - def permute(*argv) + def permute(*argv, into: nil) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - permute!(argv) + permute!(argv, into: into) end # # Same as #permute, but removes switches destructively. + # Non-option arguments remain in +argv+. # - def permute!(argv = default_argv) + def permute!(argv = default_argv, into: nil) nonopts = [] - order!(argv, &nonopts.method(:<<)) + order!(argv, into: into, &nonopts.method(:<<)) argv[0, 0] = nonopts argv end @@ -1436,31 +1664,36 @@ XXX # # Parses command line arguments +argv+ in order when environment variable # POSIXLY_CORRECT is set, and in permutation mode otherwise. + # When optional +into+ keyword argument is provided, the parsed option + # values are stored there via <code>[]=</code> method (so it can be Hash, + # or OpenStruct, or other similar object). # - def parse(*argv) + def parse(*argv, into: nil) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - parse!(argv) + parse!(argv, into: into) end # # Same as #parse, but removes switches destructively. + # Non-option arguments remain in +argv+. # - def parse!(argv = default_argv) + def parse!(argv = default_argv, into: nil) if ENV.include?('POSIXLY_CORRECT') - order!(argv) + order!(argv, into: into) else - permute!(argv) + permute!(argv, into: into) end end # # Wrapper method for getopts.rb. # - # params = ARGV.getopts("ab:", "foo", "bar:") - # # params[:a] = true # -a - # # params[:b] = "1" # -b1 - # # params[:foo] = "1" # --foo - # # params[:bar] = "x" # --bar x + # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option") + # # params["a"] = true # -a + # # params["b"] = "1" # -b1 + # # params["foo"] = "1" # --foo + # # params["bar"] = "x" # --bar x + # # params["zot"] = "z" # --zot Z # def getopts(*args) argv = Array === args.first ? args.shift : default_argv @@ -1479,13 +1712,14 @@ XXX end if single_options long_options.each do |arg| + arg, desc = arg.split(';', 2) opt, val = arg.split(':', 2) if val result[opt] = val.empty? ? nil : val - define("--#{opt} VAL") + define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact) else result[opt] = false - define("--#{opt}") + define("--#{opt}", *[desc].compact) end end @@ -1504,9 +1738,9 @@ XXX # Traverses @stack, sending each element method +id+ with +args+ and # +block+. # - def visit(id, *args, &block) + def visit(id, *args, &block) # :nodoc: @stack.reverse_each do |el| - el.send(id, *args, &block) + el.__send__(id, *args, &block) end nil end @@ -1515,7 +1749,7 @@ XXX # # Searches +key+ in @stack for +id+ hash and returns or yields the result. # - def search(id, key) + def search(id, key) # :nodoc: block_given = block_given? visit(:search, id, key) do |k| return block_given ? yield(k) : k @@ -1527,35 +1761,50 @@ XXX # Completes shortened long style option switch and returns pair of # canonical switch and switch descriptor OptionParser::Switch. # - # +id+:: Searching table. + # +typ+:: Searching table. # +opt+:: Searching key. # +icase+:: Search case insensitive if true. # +pat+:: Optional pattern for completion. # - def complete(typ, opt, icase = false, *pat) + def complete(typ, opt, icase = false, *pat) # :nodoc: if pat.empty? search(typ, opt) {|sw| return [sw, opt]} # exact match or... end - raise AmbiguousOption, catch(:ambiguous) { + ambiguous = catch(:ambiguous) { visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw} - raise InvalidOption, opt } + exc = ambiguous ? AmbiguousOption : InvalidOption + raise exc.new(opt, additional: self.method(:additional_message).curry[typ]) end private :complete + # + # Returns additional info. + # + def additional_message(typ, opt) + return unless typ and opt and defined?(DidYouMean::SpellChecker) + all_candidates = [] + visit(:get_candidates, typ) do |candidates| + all_candidates.concat(candidates) + end + all_candidates.select! {|cand| cand.is_a?(String) } + checker = DidYouMean::SpellChecker.new(dictionary: all_candidates) + DidYouMean.formatter.message_for(all_candidates & checker.correct(opt)) + end + def candidate(word) list = [] case word + when '-' + long = short = true when /\A--/ word, arg = word.split(/=/, 2) argpat = Completion.regexp(arg, false) if arg and !arg.empty? long = true - when /\A-(!-)/ - short = true when /\A-/ - long = short = true + short = true end - pat = Completion.regexp(word, true) + pat = Completion.regexp(word, long) visit(:each_option) do |opt| next unless Switch === opt opts = (long ? opt.long : []) + (short ? opt.short : []) @@ -1578,13 +1827,26 @@ XXX # is not present. Returns whether successfully loaded. # # +filename+ defaults to basename of the program without suffix in a - # directory ~/.options. + # directory ~/.options, then the basename with '.options' suffix + # under XDG and Haiku standard places. # def load(filename = nil) - begin - filename ||= File.expand_path(File.basename($0, '.*'), '~/.options') - rescue - return false + unless filename + basename = File.basename($0, '.*') + return true if load(File.expand_path(basename, '~/.options')) rescue nil + basename << ".options" + return [ + # XDG + ENV['XDG_CONFIG_HOME'], + '~/.config', + *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR), + + # Haiku + '~/config/settings', + ].any? {|dir| + next if !dir or dir.empty? + load(File.expand_path(basename, dir)) rescue nil + } end begin parse(*IO.readlines(filename).each {|s| s.chomp!}) @@ -1630,15 +1892,22 @@ XXX decimal = '\d+(?:_\d+)*' binary = 'b[01]+(?:_[01]+)*' hex = 'x[\da-f]+(?:_[\da-f]+)*' - octal = "0(?:[0-7]*(?:_[0-7]+)*|#{binary}|#{hex})" + octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?" integer = "#{octal}|#{decimal}" - accept(Integer, %r"\A[-+]?(?:#{integer})"io) {|s,| Integer(s) if s} + + accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,| + begin + Integer(s) + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } # # Float number format, and converts to Float. # - float = "(?:#{decimal}(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?" - floatpat = %r"\A[-+]?#{float}"io + float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?" + floatpat = %r"\A[-+]?#{float}\z"io accept(Float, floatpat) {|s,| s.to_f if s} # @@ -1646,33 +1915,57 @@ XXX # for float format, and Rational for rational format. # real = "[-+]?(?:#{octal}|#{float})" - accept(Numeric, /\A(#{real})(?:\/(#{real}))?/io) {|s, d, n| + accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,| if n Rational(d, n) - elsif s - eval(s) + elsif f + Float(s) + else + Integer(s) end } # # Decimal integer format, to be converted to Integer. # - DecimalInteger = /\A[-+]?#{decimal}/io - accept(DecimalInteger) {|s,| s.to_i if s} + DecimalInteger = /\A[-+]?#{decimal}\z/io + accept(DecimalInteger, DecimalInteger) {|s,| + begin + Integer(s, 10) + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } # # Ruby/C like octal/hexadecimal/binary integer format, to be converted to # Integer. # - OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))/io - accept(OctalInteger) {|s,| s.oct if s} + OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io + accept(OctalInteger, OctalInteger) {|s,| + begin + Integer(s, 8) + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } # # Decimal integer/float number format, to be converted to Integer for # integer format, Float for float format. # DecimalNumeric = floatpat # decimal integer is allowed as float also. - accept(DecimalNumeric) {|s,| eval(s) if s} + accept(DecimalNumeric, floatpat) {|s, f| + begin + if f + Float(s) + else + Integer(s) + end + rescue ArgumentError + raise OptionParser::InvalidArgument, s + end if s + } # # Boolean switch, which means whether it is present or not, whether it is @@ -1692,7 +1985,7 @@ XXX # # List of strings separated by ",". # - accept(Array) do |s,| + accept(Array) do |s, | if s s = s.split(',').collect {|ss| ss unless ss.empty?} end @@ -1723,15 +2016,18 @@ XXX # class ParseError < RuntimeError # Reason which caused the error. - Reason = 'parse error'.freeze + Reason = 'parse error' - def initialize(*args) + def initialize(*args, additional: nil) + @additional = additional + @arg0, = args @args = args @reason = nil end attr_reader :args attr_writer :reason + attr_accessor :additional # # Pushes back erred argument(s) to +argv+. @@ -1769,14 +2065,14 @@ XXX end def inspect - "#<#{self.class.to_s}: #{args.join(' ')}>" + "#<#{self.class}: #{args.join(' ')}>" end # # Default stringizing method to emit standard error message. # def message - reason + ': ' + args.join(' ') + "#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}" end alias to_s message @@ -1786,42 +2082,42 @@ XXX # Raises when ambiguously completable string is encountered. # class AmbiguousOption < ParseError - const_set(:Reason, 'ambiguous option'.freeze) + const_set(:Reason, 'ambiguous option') end # # Raises when there is an argument for a switch which takes no argument. # class NeedlessArgument < ParseError - const_set(:Reason, 'needless argument'.freeze) + const_set(:Reason, 'needless argument') end # # Raises when a switch with mandatory argument has no argument. # class MissingArgument < ParseError - const_set(:Reason, 'missing argument'.freeze) + const_set(:Reason, 'missing argument') end # # Raises when switch is undefined. # class InvalidOption < ParseError - const_set(:Reason, 'invalid option'.freeze) + const_set(:Reason, 'invalid option') end # # Raises when the given argument does not match required format. # class InvalidArgument < ParseError - const_set(:Reason, 'invalid argument'.freeze) + const_set(:Reason, 'invalid argument') end # # Raises when the given argument word can't be completed uniquely. # class AmbiguousArgument < InvalidArgument - const_set(:Reason, 'ambiguous argument'.freeze) + const_set(:Reason, 'ambiguous argument') end # @@ -1928,9 +2224,5 @@ end # ARGV is arguable by OptionParser ARGV.extend(OptionParser::Arguable) -if $0 == __FILE__ - Version = OptionParser::Version - ARGV.options {|q| - q.parse!.empty? or print "what's #{ARGV.join(' ')}?\n" - } or abort(ARGV.options.to_s) -end +# An alias for OptionParser. +OptParse = OptionParser # :nodoc: |
