diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..ee28f6b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,84 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '35 1 * * 0' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c2decb..d862b11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,29 @@ jobs: - name: Run test run: rake test + - id: fetch + run: | + : Fetch deeper for changelogs + set -x + latest=$(git tag --list --contains HEAD) + case "$latest" in + '') prev=;; + *.0.0) prev=${latest%.0.0}; prev=$((prev-1)).0.0;; + *.0) prev=${latest%.0}; x=${prev##*.}; prev=${prev%.*}.$((x-1)).0;; + *) x=${prev##*.}; prev=${prev%.*}.$((x-1));; + esac + : ${prev:+git fetch --unshallow-exclude=$prev origin} + until git log -1 --oneline $prev; do + git fetch --deepen=100 + done + set +x + shell: bash + if: >- + ${{ + matrix.os != 'windows-latest' && + matrix.ruby == needs.ruby-versions.outputs.latest + }} + - id: build run: | rake build @@ -50,10 +73,10 @@ jobs: echo "pkg=${GITHUB_REPOSITORY#*/}-${RUNNING_OS%-*}" >> $GITHUB_OUTPUT env: RUNNING_OS: ${{matrix.os}} - shell: bash - if: ${{ matrix.os != 'windows-latest' }} + if: ${{ steps.fetch.outcome == 'success' }} + - name: Upload package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: pkg/*.gem name: ${{steps.build.outputs.pkg}} diff --git a/doc/optparse/option_params.rdoc b/doc/optparse/option_params.rdoc index 55f9b53..35db8b5 100644 --- a/doc/optparse/option_params.rdoc +++ b/doc/optparse/option_params.rdoc @@ -31,7 +31,7 @@ Contents: - {Long Names with Optional Arguments}[#label-Long+Names+with+Optional+Arguments] - {Long Names with Negation}[#label-Long+Names+with+Negation] - {Mixed Names}[#label-Mixed+Names] -- {Argument Styles}[#label-Argument+Styles] +- {Argument Strings}[#label-Argument+Strings] - {Argument Values}[#label-Argument+Values] - {Explicit Argument Values}[#label-Explicit+Argument+Values] - {Explicit Values in Array}[#label-Explicit+Values+in+Array] diff --git a/doc/optparse/ruby/argument_abbreviation.rb b/doc/optparse/ruby/argument_abbreviation.rb new file mode 100644 index 0000000..49007eb --- /dev/null +++ b/doc/optparse/ruby/argument_abbreviation.rb @@ -0,0 +1,9 @@ +require 'optparse' +parser = OptionParser.new +parser.on('-x', '--xxx=VALUE', %w[ABC def], 'Argument abbreviations') do |value| + p ['--xxx', value] +end +parser.on('-y', '--yyy=VALUE', {"abc"=>"XYZ", def: "FOO"}, 'Argument abbreviations') do |value| + p ['--yyy', value] +end +parser.parse! diff --git a/doc/optparse/tutorial.rdoc b/doc/optparse/tutorial.rdoc index b104379..6f56bbf 100644 --- a/doc/optparse/tutorial.rdoc +++ b/doc/optparse/tutorial.rdoc @@ -351,6 +351,29 @@ Executions: Omitting an optional argument does not raise an error. +==== Argument Abbreviations + +Specify an argument list as an Array or a Hash. + + :include: ruby/argument_abbreviation.rb + +When an argument is abbreviated, the expanded argument yielded. + +Executions: + + $ ruby argument_abbreviation.rb --help + Usage: argument_abbreviation [options] + Usage: argument_abbreviation [options] + -x, --xxx=VALUE Argument abbreviations + -y, --yyy=VALUE Argument abbreviations + $ ruby argument_abbreviation.rb --xxx A + ["--xxx", "ABC"] + $ ruby argument_abbreviation.rb --xxx c + argument_abbreviation.rb:9:in `
': invalid argument: --xxx c (OptionParser::InvalidArgument) + $ ruby argument_abbreviation.rb --yyy a --yyy d + ["--yyy", "XYZ"] + ["--yyy", "FOO"] + === Argument Values Permissible argument values may be restricted diff --git a/lib/optparse.rb b/lib/optparse.rb index 832b928..069c3e4 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -8,7 +8,6 @@ # See OptionParser for documentation. # - #-- # == Developer Documentation (not for RDoc output) # @@ -425,7 +424,8 @@ # If you have any questions, file a ticket at http://bugs.ruby-lang.org. # class OptionParser - OptionParser::Version = "0.4.0" + # The version string + OptionParser::Version = "0.5.0" # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze @@ -438,6 +438,8 @@ class OptionParser # and resolved against a list of acceptable values. # module Completion + # :nodoc: + def self.regexp(key, icase) Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase) end @@ -459,7 +461,7 @@ def self.candidate(key, icase = false, pat = nil, &block) candidates end - def candidate(key, icase = false, pat = nil) + def candidate(key, icase = false, pat = nil, &_) Completion.candidate(key, icase, pat, &method(:each)) end @@ -510,6 +512,8 @@ class OptionMap < Hash # RequiredArgument, etc. # class Switch + # :nodoc: + attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block # @@ -697,6 +701,11 @@ def pretty_print(q) # :nodoc: q.object_group(self) {pretty_print_contents(q)} end + def omitted_argument(val) # :nodoc: + val.pop if val.size == 3 and val.last.nil? + val + end + # # Switch that takes no arguments. # @@ -710,10 +719,10 @@ def parse(arg, argv) conv_arg(arg) end - def self.incompatible_argument_styles(*) + def self.incompatible_argument_styles(*) # :nodoc: end - def self.pattern + def self.pattern # :nodoc: Object end @@ -730,7 +739,7 @@ class RequiredArgument < self # # Raises an exception if argument is not present. # - def parse(arg, argv) + def parse(arg, argv, &_) unless arg raise MissingArgument if argv.empty? arg = argv.shift @@ -755,7 +764,7 @@ def parse(arg, argv, &error) if arg conv_arg(*parse_arg(arg, &error)) else - conv_arg(arg) + omitted_argument conv_arg(arg) end end @@ -774,13 +783,14 @@ class PlacedArgument < self # def parse(arg, argv, &error) if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0])) - return nil, block, nil + return nil, block end opt = (val = parse_arg(val, &error))[1] val = conv_arg(*val) if opt and !arg argv.shift else + omitted_argument val val[0] = nil end val @@ -798,6 +808,8 @@ def pretty_head # :nodoc: # matching pattern and converter pair. Also provides summary feature. # class List + # :nodoc: + # Map from acceptable argument types to pattern and converter pairs. attr_reader :atype @@ -837,7 +849,7 @@ def pretty_print(q) # :nodoc: def accept(t, pat = /.*/m, &block) if pat pat.respond_to?(:match) or - raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2)) + raise TypeError, "has no 'match'", ParseError.filter_backtrace(caller(2)) else pat = t if t.respond_to?(:match) end @@ -1033,11 +1045,31 @@ def compsys(to, name = File.basename($0)) # :nodoc: to << "#compdef #{name}\n" to << COMPSYS_HEADER visit(:compsys, {}, {}) {|o, d| - to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n] + to << %Q[ "#{o}[#{d.gsub(/[\\\"\[\]]/, '\\\\\&')}]" \\\n] } to << " '*:file:_files' && return 0\n" end + def help_exit + if STDOUT.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?}) + less = ENV["LESS"] + args = [{"LESS" => "#{!less || less.empty? ? '-' : less}Fe"}, pager, "w"] + print = proc do |f| + f.puts help + rescue Errno::EPIPE + # pager terminated + end + if Process.respond_to?(:fork) and false + IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call(STDOUT)} + # unreachable + end + IO.popen(*args, &print) + else + puts help + end + exit + end + # # Default options for ARGV, which never appear in option summary. # @@ -1049,8 +1081,7 @@ def compsys(to, name = File.basename($0)) # :nodoc: # Officious['help'] = proc do |parser| Switch::NoArgument.new do |arg| - puts parser.help - exit + parser.help_exit end end @@ -1129,6 +1160,10 @@ def self.inc(arg, default = nil) default.to_i + 1 end end + + # + # See self.inc + # def inc(*args) self.class.inc(*args) end @@ -1167,11 +1202,19 @@ def add_officious # :nodoc: def terminate(arg = nil) self.class.terminate(arg) end + # + # See #terminate. + # def self.terminate(arg = nil) throw :terminate, arg end @stack = [DefaultList] + # + # Returns the global top option list. + # + # Do not use directly. + # def self.top() DefaultList end # @@ -1192,9 +1235,9 @@ def self.accept(*args, &blk) top.accept(*args, &blk) end # # Directs to reject specified class argument. # - # +t+:: Argument class specifier, any object including Class. + # +type+:: Argument class specifier, any object including Class. # - # reject(t) + # reject(type) # def reject(*args, &blk) top.reject(*args, &blk) end # @@ -1284,10 +1327,24 @@ def ver end end + # + # Shows warning message with the program name + # + # +mesg+:: Message, defaulted to +$!+. + # + # See Kernel#warn. + # def warn(mesg = $!) super("#{program_name}: #{mesg}") end + # + # Shows message with the program name then aborts. + # + # +mesg+:: Message, defaulted to +$!+. + # + # See Kernel#abort. + # def abort(mesg = $!) super("#{program_name}: #{mesg}") end @@ -1309,6 +1366,9 @@ def base # # Pushes a new List. # + # If a block is given, yields +self+ and returns the result of the + # block, otherwise returns +self+. + # def new @stack.push(List.new) if block_given? @@ -1532,6 +1592,12 @@ def make_switch(opts, block = nil) nolong end + # ---- + # Option definition phase methods + # + # These methods are used to define options, or to construct an + # OptionParser instance in other words. + # :call-seq: # define(*params, &block) # @@ -1607,6 +1673,13 @@ def separator(string) top.append(string, nil, nil) end + # ---- + # Arguments parse phase methods + # + # These methods parse +argv+, convert, and store the results by + # calling handlers. As these methods do not modify +self+, +self+ + # can be frozen. + # # Parses command line arguments +argv+ in order. When a block is given, # each non-option argument is yielded. When optional +into+ keyword @@ -1616,21 +1689,21 @@ def separator(string) # # Returns the rest of +argv+ left unparsed. # - def order(*argv, into: nil, &nonopt) + def order(*argv, **keywords, &nonopt) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - order!(argv, into: into, &nonopt) + order!(argv, **keywords, &nonopt) end # # Same as #order, but removes switches destructively. # Non-option arguments remain in +argv+. # - def order!(argv = default_argv, into: nil, &nonopt) + def order!(argv = default_argv, into: nil, **keywords, &nonopt) setter = ->(name, val) {into[name.to_sym] = val} if into - parse_in_order(argv, setter, &nonopt) + parse_in_order(argv, setter, **keywords, &nonopt) end - def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: + def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc: opt, arg, val, rest = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { @@ -1641,19 +1714,24 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: opt, rest = $1, $2 opt.tr!('_', '-') begin - sw, = complete(:long, opt, true) - if require_exact && !sw.long.include?(arg) - throw :terminate, arg unless raise_unknown - raise InvalidOption, arg + if exact + sw, = search(:long, opt) + else + sw, = complete(:long, opt, true) end rescue ParseError throw :terminate, arg unless raise_unknown raise $!.set_option(arg, true) + else + unless sw + throw :terminate, arg unless raise_unknown + raise InvalidOption, arg + end end begin - opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)} - val = cb.call(val) if cb - setter.call(sw.switch_name, val) if setter + opt, cb, *val = sw.parse(rest, argv) {|*exc| raise(*exc)} + val = callback!(cb, 1, *val) if cb + callback!(setter, 2, sw.switch_name, *val) if setter rescue ParseError raise $!.set_option(arg, rest) end @@ -1671,7 +1749,7 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: val = arg.delete_prefix('-') has_arg = true rescue InvalidOption - raise if require_exact + raise if exact # if no short options match, try completion with long # options. sw, = complete(:long, opt) @@ -1683,7 +1761,7 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: raise $!.set_option(arg, true) end begin - opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} + opt, cb, *val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} rescue ParseError raise $!.set_option(arg, arg.length > 2) else @@ -1691,8 +1769,8 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: 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 + val = callback!(cb, 1, *val) if cb + callback!(setter, 2, sw.switch_name, *val) if setter rescue ParseError raise $!.set_option(arg, arg.length > 2) end @@ -1718,6 +1796,17 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: end private :parse_in_order + # Calls callback with _val_. + def callback!(cb, max_arity, *args) # :nodoc: + if (size = args.size) < max_arity and cb.to_proc.lambda? + (arity = cb.arity) < 0 and arity = (1-arity) + arity = max_arity if arity > max_arity + args[arity - 1] = nil if arity > size + end + cb.call(*args) + end + private :callback! + # # Parses command line arguments +argv+ in permutation mode and returns # list of non-option arguments. When optional +into+ keyword @@ -1725,18 +1814,18 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: # []= method (so it can be Hash, or OpenStruct, or other # similar object). # - def permute(*argv, into: nil) + def permute(*argv, **keywords) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - permute!(argv, into: into) + permute!(argv, **keywords) end # # Same as #permute, but removes switches destructively. # Non-option arguments remain in +argv+. # - def permute!(argv = default_argv, into: nil) + def permute!(argv = default_argv, **keywords) nonopts = [] - order!(argv, into: into, &nonopts.method(:<<)) + order!(argv, **keywords, &nonopts.method(:<<)) argv[0, 0] = nonopts argv end @@ -1748,20 +1837,20 @@ def permute!(argv = default_argv, into: nil) # values are stored there via []= method (so it can be Hash, # or OpenStruct, or other similar object). # - def parse(*argv, into: nil) + def parse(*argv, **keywords) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - parse!(argv, into: into) + parse!(argv, **keywords) end # # Same as #parse, but removes switches destructively. # Non-option arguments remain in +argv+. # - def parse!(argv = default_argv, into: nil) + def parse!(argv = default_argv, **keywords) if ENV.include?('POSIXLY_CORRECT') - order!(argv, into: into) + order!(argv, **keywords) else - permute!(argv, into: into) + permute!(argv, **keywords) end end @@ -1784,7 +1873,7 @@ def parse!(argv = default_argv, into: nil) # # params[:bar] = "x" # --bar x # # params[:zot] = "z" # --zot Z # - def getopts(*args, symbolize_names: false) + def getopts(*args, symbolize_names: false, **keywords) argv = Array === args.first ? args.shift : default_argv single_options, *long_options = *args @@ -1812,7 +1901,7 @@ def getopts(*args, symbolize_names: false) end end - parse_in_order(argv, result.method(:[]=)) + parse_in_order(argv, result.method(:[]=), **keywords) symbolize_names ? result.transform_keys(&:to_sym) : result end @@ -1881,6 +1970,9 @@ def additional_message(typ, opt) DidYouMean.formatter.message_for(all_candidates & checker.correct(opt)) end + # + # Return candidates for +word+. + # def candidate(word) list = [] case word @@ -1922,10 +2014,10 @@ def candidate(word) # The optional +into+ keyword argument works exactly like that accepted in # method #parse. # - def load(filename = nil, into: nil) + def load(filename = nil, **keywords) unless filename basename = File.basename($0, '.*') - return true if load(File.expand_path(basename, '~/.options'), into: into) rescue nil + return true if load(File.expand_path(basename, '~/.options'), **keywords) rescue nil basename << ".options" return [ # XDG @@ -1937,11 +2029,11 @@ def load(filename = nil, into: nil) '~/config/settings', ].any? {|dir| next if !dir or dir.empty? - load(File.expand_path(basename, dir), into: into) rescue nil + load(File.expand_path(basename, dir), **keywords) rescue nil } end begin - parse(*File.readlines(filename, chomp: true), into: into) + parse(*File.readlines(filename, chomp: true), **keywords) true rescue Errno::ENOENT, Errno::ENOTDIR false @@ -1954,10 +2046,10 @@ def load(filename = nil, into: nil) # # +env+ defaults to the basename of the program. # - def environment(env = File.basename($0, '.*')) + def environment(env = File.basename($0, '.*'), **keywords) env = ENV[env] || ENV[env.upcase] or return require 'shellwords' - parse(*Shellwords.shellwords(env)) + parse(*Shellwords.shellwords(env), **keywords) end # @@ -2123,6 +2215,7 @@ class ParseError < RuntimeError # Reason which caused the error. Reason = 'parse error' + # :nodoc: def initialize(*args, additional: nil) @additional = additional @arg0, = args @@ -2273,19 +2366,19 @@ def options # Parses +self+ destructively in order and returns +self+ containing the # rest arguments left unparsed. # - def order!(&blk) options.order!(self, &blk) end + def order!(**keywords, &blk) options.order!(self, **keywords, &blk) end # # Parses +self+ destructively in permutation mode and returns +self+ # containing the rest arguments left unparsed. # - def permute!() options.permute!(self) end + def permute!(**keywords) options.permute!(self, **keywords) end # # Parses +self+ destructively and returns +self+ containing the # rest arguments left unparsed. # - def parse!() options.parse!(self) end + def parse!(**keywords) options.parse!(self, **keywords) end # # Substitution of getopts is possible as follows. Also see @@ -2298,8 +2391,8 @@ def parse!() options.parse!(self) end # rescue OptionParser::ParseError # end # - def getopts(*args, symbolize_names: false) - options.getopts(self, *args, symbolize_names: symbolize_names) + def getopts(*args, symbolize_names: false, **keywords) + options.getopts(self, *args, symbolize_names: symbolize_names, **keywords) end # @@ -2309,7 +2402,8 @@ def self.extend_object(obj) super obj.instance_eval {@optparse = nil} end - def initialize(*args) + + def initialize(*args) # :nodoc: super @optparse = nil end diff --git a/lib/optparse/ac.rb b/lib/optparse/ac.rb index 0953725..23fc740 100644 --- a/lib/optparse/ac.rb +++ b/lib/optparse/ac.rb @@ -1,7 +1,11 @@ # frozen_string_literal: false require_relative '../optparse' +# +# autoconf-like options. +# class OptionParser::AC < OptionParser + # :stopdoc: private def _check_ac_args(name, block) @@ -14,6 +18,7 @@ def _check_ac_args(name, block) end ARG_CONV = proc {|val| val.nil? ? true : val} + private_constant :ARG_CONV def _ac_arg_enable(prefix, name, help_string, block) _check_ac_args(name, block) @@ -29,16 +34,27 @@ def _ac_arg_enable(prefix, name, help_string, block) enable end + # :startdoc: + public + # Define --enable / --disable style option + # + # Appears as --enable-name in help message. def ac_arg_enable(name, help_string, &block) _ac_arg_enable("enable", name, help_string, block) end + # Define --enable / --disable style option + # + # Appears as --disable-name in help message. def ac_arg_disable(name, help_string, &block) _ac_arg_enable("disable", name, help_string, block) end + # Define --with / --without style option + # + # Appears as --with-name in help message. def ac_arg_with(name, help_string, &block) _check_ac_args(name, block) diff --git a/lib/optparse/kwargs.rb b/lib/optparse/kwargs.rb index 992aca4..59a2f61 100644 --- a/lib/optparse/kwargs.rb +++ b/lib/optparse/kwargs.rb @@ -7,12 +7,17 @@ class OptionParser # # :include: ../../doc/optparse/creates_option.rdoc # - def define_by_keywords(options, meth, **opts) - meth.parameters.each do |type, name| + # Defines options which set in to _options_ for keyword parameters + # of _method_. + # + # Parameters for each keywords are given as elements of _params_. + # + def define_by_keywords(options, method, **params) + method.parameters.each do |type, name| case type when :key, :keyreq op, cl = *(type == :key ? %w"[ ]" : ["", ""]) - define("--#{name}=#{op}#{name.upcase}#{cl}", *opts[name]) do |o| + define("--#{name}=#{op}#{name.upcase}#{cl}", *params[name]) do |o| options[name] = o end end diff --git a/lib/optparse/version.rb b/lib/optparse/version.rb index b869d8f..b5ed695 100644 --- a/lib/optparse/version.rb +++ b/lib/optparse/version.rb @@ -2,6 +2,11 @@ # OptionParser internal utility class << OptionParser + # + # Shows version string in packages if Version is defined. + # + # +pkgs+:: package list + # def show_version(*pkgs) progname = ARGV.options.program_name result = false @@ -47,6 +52,8 @@ def show_version(*pkgs) result end + # :stopdoc: + def each_const(path, base = ::Object) path.split(/::|\//).inject(base) do |klass, name| raise NameError, path unless Module === klass @@ -68,4 +75,6 @@ def search_const(klass, name) end end end + + # :startdoc: end diff --git a/optparse.gemspec b/optparse.gemspec index a4287dd..1aa54aa 100644 --- a/optparse.gemspec +++ b/optparse.gemspec @@ -22,7 +22,8 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage - spec.files = Dir["{doc,lib,misc}/**/*"] + %w[README.md ChangeLog COPYING] + spec.files = Dir["{doc,lib,misc}/**/{*,.document}"] + + %w[README.md ChangeLog COPYING .document .rdoc_options] spec.rdoc_options = ["--main=README.md", "--op=rdoc", "--page-dir=doc"] spec.bindir = "exe" spec.executables = [] diff --git a/rakelib/changelogs.rake b/rakelib/changelogs.rake index df72f9d..5e00c98 100644 --- a/rakelib/changelogs.rake +++ b/rakelib/changelogs.rake @@ -3,7 +3,10 @@ task "build" => "changelogs" changelog = proc do |output, ver = nil, prev = nil| ver &&= Gem::Version.new(ver) range = [[prev], [ver, "HEAD"]].map {|ver, branch| ver ? "v#{ver.to_s}" : branch}.compact.join("..") - IO.popen(%W[git log --format=fuller --topo-order --no-merges #{range}]) do |log| + cmd = %W[git log --date=iso --format=fuller --topo-order --no-merges + --invert-grep --fixed-strings --grep=#{'[ci skip]'} + #{range} --] + IO.popen(cmd) do |log| line = log.gets FileUtils.mkpath(File.dirname(output)) File.open(output, "wb") do |f| @@ -18,10 +21,13 @@ changelog = proc do |output, ver = nil, prev = nil| end tags = IO.popen(%w[git tag -l v[0-9]*]).grep(/v(.*)/) {$1} -tags.sort_by! {|tag| tag.scan(/\d+/).map(&:to_i)} -tags.inject(nil) do |prev, tag| - task("logs/ChangeLog-#{tag}") {|t| changelog[t.name, tag, prev]} - tag +unless tags.empty? + tags.sort_by! {|tag| tag.scan(/\d+/).map(&:to_i)} + tags.pop if IO.popen(%W[git log --format=%H v#{tags.last}..HEAD --], &:read).empty? + tags.inject(nil) do |prev, tag| + task("logs/ChangeLog-#{tag}") {|t| changelog[t.name, tag, prev]} + tag + end end desc "Make ChangeLog" diff --git a/test/optparse/test_acceptable.rb b/test/optparse/test_acceptable.rb index 12f5322..c7ea215 100644 --- a/test/optparse/test_acceptable.rb +++ b/test/optparse/test_acceptable.rb @@ -8,6 +8,7 @@ def setup @opt.def_option("--integer VAL", Integer) { |v| @integer = v } @opt.def_option("--float VAL", Float) { |v| @float = v } @opt.def_option("--numeric VAL", Numeric) { |v| @numeric = v } + @opt.def_option("--array VAL", Array) { |v| @array = v } @opt.def_option("--decimal-integer VAL", OptionParser::DecimalInteger) { |i| @decimal_integer = i } @@ -195,4 +196,8 @@ def test_decimal_numeric end end + def test_array + assert_equal(%w"", no_error {@opt.parse!(%w"--array a,b,c")}) + assert_equal(%w"a b c", @array) + end end diff --git a/test/optparse/test_optarg.rb b/test/optparse/test_optarg.rb index 81127a8..f944605 100644 --- a/test/optparse/test_optarg.rb +++ b/test/optparse/test_optarg.rb @@ -9,6 +9,8 @@ def setup @opt.def_option("--regexp[=REGEXP]", Regexp) {|x| @reopt = x} @opt.def_option "--with_underscore[=VAL]" do |x| @flag = x end @opt.def_option "--with-hyphen[=VAL]" do |x| @flag = x end + @opt.def_option("--fallback[=VAL]") do |x = "fallback"| @flag = x end + @opt.def_option("--lambda[=VAL]", &->(x) {@flag = x}) @reopt = nil end @@ -57,4 +59,18 @@ def test_hyphenize assert_equal(%w"", no_error {@opt.parse!(%w"--with_hyphen=foo4")}) assert_equal("foo4", @flag) end + + def test_default_argument + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback=val1")}) + assert_equal("val1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback")}) + assert_equal("fallback", @flag) + end + + def test_lambda + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda=lambda1")}) + assert_equal("lambda1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda")}) + assert_equal(nil, @flag) + end end diff --git a/test/optparse/test_optparse.rb b/test/optparse/test_optparse.rb index bfa705a..8d09e0f 100644 --- a/test/optparse/test_optparse.rb +++ b/test/optparse/test_optparse.rb @@ -73,10 +73,17 @@ def test_into @opt.def_option "-p", "--port=PORT", "port", Integer @opt.def_option "-v", "--verbose" do @verbose = true end @opt.def_option "-q", "--quiet" do @quiet = true end + @opt.def_option "-o", "--option [OPT]" do |opt| @option = opt end result = {} @opt.parse %w(--host localhost --port 8000 -v), into: result assert_equal({host: "localhost", port: 8000, verbose: true}, result) assert_equal(true, @verbose) + result = {} + @opt.parse %w(--option -q), into: result + assert_equal({quiet: true, option: nil}, result) + result = {} + @opt.parse %w(--option OPTION -v), into: result + assert_equal({verbose: true, option: "OPTION"}, result) end def test_require_exact @@ -88,9 +95,9 @@ def test_require_exact end @opt.require_exact = true - %w(--zrs -F -Ffoo).each do |arg| + [%w(--zrs foo), %w(--zrs=foo), %w(-F foo), %w(-Ffoo)].each do |args| result = {} - @opt.parse([arg, 'foo'], into: result) + @opt.parse(args, into: result) assert_equal({zrs: 'foo'}, result) end @@ -99,6 +106,43 @@ def test_require_exact assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zrs foo))} assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zr foo))} assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-z foo))} + + @opt.def_option('-f', '--[no-]foo', 'foo') {|arg| @foo = arg} + @opt.parse(%w[-f]) + assert_equal(true, @foo) + @opt.parse(%w[--foo]) + assert_equal(true, @foo) + @opt.parse(%w[--no-foo]) + assert_equal(false, @foo) + end + + def test_exact_option + @opt.def_option('-F', '--zrs=IRS', 'zrs') + %w(--zrs --zr --z -zfoo -z -F -Ffoo).each do |arg| + result = {} + @opt.parse([arg, 'foo'], into: result) + assert_equal({zrs: 'foo'}, result) + end + + [%w(--zrs foo), %w(--zrs=foo), %w(-F foo), %w(-Ffoo)].each do |args| + result = {} + @opt.parse(args, into: result, exact: true) + assert_equal({zrs: 'foo'}, result) + end + + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(--zr foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(--z foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zrs foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-zr foo), exact: true)} + assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-z foo), exact: true)} + + @opt.def_option('-f', '--[no-]foo', 'foo') {|arg| @foo = arg} + @opt.parse(%w[-f], exact: true) + assert_equal(true, @foo) + @opt.parse(%w[--foo], exact: true) + assert_equal(true, @foo) + @opt.parse(%w[--no-foo], exact: true) + assert_equal(false, @foo) end def test_raise_unknown @@ -120,4 +164,44 @@ def test_nonopt_pattern e = assert_raise(OptionParser::InvalidOption) {@opt.parse(%w(-t))} assert_equal(["-t"], e.args) end + + def test_help_pager + require 'tmpdir' + Dir.mktmpdir do |dir| + File.open(File.join(dir, "options.rb"), "w") do |f| + f.puts "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + stdout = STDOUT.dup + def stdout.tty?; true; end + Object.__send__(:remove_const, :STDOUT) + STDOUT = stdout + ARGV.options do |opt| + end; + 100.times {|i| f.puts " opt.on('--opt-#{i}') {}"} + f.puts "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + opt.parse! + end + end; + end + + optparse = $".find {|path| path.end_with?("/optparse.rb")} + args = ["-r#{optparse}", "options.rb", "--help"] + cmd = File.join(dir, "pager.cmd") + if RbConfig::CONFIG["EXECUTABLE_EXTS"]&.include?(".cmd") + command = "@echo off" + else # if File.executable?("/bin/sh") + # TruffleRuby just calls `posix_spawnp` and no fallback to `/bin/sh`. + command = "#!/bin/sh\n" + end + + [ + [{"RUBY_PAGER"=>cmd, "PAGER"=>"echo ng"}, "Executing RUBY_PAGER"], + [{"RUBY_PAGER"=>nil, "PAGER"=>cmd}, "Executing PAGER"], + ].each do |env, expected| + File.write(cmd, "#{command}\n" "echo #{expected}\n", perm: 0o700) + assert_in_out_err([env, *args], "", [expected], chdir: dir) + end + end + end end diff --git a/test/optparse/test_placearg.rb b/test/optparse/test_placearg.rb index ed0e4d3..a8a11e6 100644 --- a/test/optparse/test_placearg.rb +++ b/test/optparse/test_placearg.rb @@ -13,6 +13,8 @@ def setup @reopt = nil @opt.def_option "--with_underscore=VAL" do |x| @flag = x end @opt.def_option "--with-hyphen=VAL" do |x| @flag = x end + @opt.def_option("--fallback [VAL]") do |x = "fallback"| @flag = x end + @opt.def_option("--lambda [VAL]", &->(x) {@flag = x}) end def test_short @@ -73,4 +75,22 @@ def test_conv assert_equal(%w"te.rb", no_error('[ruby-dev:38333]') {@opt.parse!(%w"-T1 te.rb")}) assert_equal(1, @topt) end + + def test_default_argument + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback=val1")}) + assert_equal("val1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback val2")}) + assert_equal("val2", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--fallback")}) + assert_equal("fallback", @flag) + end + + def test_lambda + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda=lambda1")}) + assert_equal("lambda1", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda lambda2")}) + assert_equal("lambda2", @flag) + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda")}) + assert_equal(nil, @flag) + end end diff --git a/test/optparse/test_reqarg.rb b/test/optparse/test_reqarg.rb index d5686d1..31d4fef 100644 --- a/test/optparse/test_reqarg.rb +++ b/test/optparse/test_reqarg.rb @@ -6,6 +6,7 @@ def setup super @opt.def_option "--with_underscore=VAL" do |x| @flag = x end @opt.def_option "--with-hyphen=VAL" do |x| @flag = x end + @opt.def_option("--lambda=VAL", &->(x) {@flag = x}) end class Def1 < TestOptionParser @@ -81,6 +82,11 @@ def test_hyphenize assert_equal("foo4", @flag) end + def test_lambda + assert_equal(%w"", no_error {@opt.parse!(%w"--lambda=lambda1")}) + assert_equal("lambda1", @flag) + end + class TestOptionParser::WithPattern < TestOptionParser def test_pattern pat = num = nil