From 99af0a050a8da2d5cdd5b024de096d576de69b3a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 8 Oct 2022 19:52:27 +0900 Subject: [PATCH 1/4] [WIP] subcommand parser --- lib/optparse.rb | 35 +++++++++++++++++++++++++++++++---- sample/optparse/subcommand.rb | 23 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100755 sample/optparse/subcommand.rb diff --git a/lib/optparse.rb b/lib/optparse.rb index 26683ef..f0e9db7 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -1149,6 +1149,7 @@ def initialize(banner = nil, width = 32, indent = ' ' * 4) @default_argv = ARGV @require_exact = false @raise_unknown = true + @subparsers = nil add_officious yield self if block_given? end @@ -1171,6 +1172,12 @@ def self.terminate(arg = nil) throw :terminate, arg end + def subparser(name, *rest, &block) + parser = self.class.new(*rest) + (@subparsers ||= CompletingHash.new)[name] = [parser, block] + parser + end + @stack = [DefaultList] def self.top() DefaultList end @@ -1626,12 +1633,18 @@ def order(*argv, into: nil, &nonopt) # Non-option arguments remain in +argv+. # def order!(argv = default_argv, into: nil, &nonopt) - setter = ->(name, val) {into[name.to_sym] = val} if into + setter = into.extend(SymSetter).method(:sym_set) if into parse_in_order(argv, setter, &nonopt) end + module SymSetter + def sym_set(name, val) + self[name.to_sym] = val + end + end + def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: - opt, arg, val, rest = nil + opt, arg, val, rest, sub = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { while arg = argv.shift @@ -1699,6 +1712,17 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: # non-option argument else + # sub-command + if (key, (sub, block) = @subparsers&.complete(arg)) + block.call if block + if setter + into = setter.receiver.class.new.extend(SymSetter) + setter.call(key, into) + setter = into.method(:sym_set) + end + return sub.parse_in_order(argv, setter, &nonopt) + end + catch(:prune) do visit(:each_option) do |sw0| sw = sw0 @@ -1716,7 +1740,7 @@ def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: argv end - private :parse_in_order + protected :parse_in_order # # Parses command line arguments +argv+ in permutation mode and returns @@ -1735,6 +1759,9 @@ def permute(*argv, into: nil) # Non-option arguments remain in +argv+. # def permute!(argv = default_argv, into: nil) + if @subparsers + raise "cannot parse in permutation mode with subparsers" + end nonopts = [] order!(argv, into: into, &nonopts.method(:<<)) argv[0, 0] = nonopts @@ -1758,7 +1785,7 @@ def parse(*argv, into: nil) # Non-option arguments remain in +argv+. # def parse!(argv = default_argv, into: nil) - if ENV.include?('POSIXLY_CORRECT') + if @subparsers or ENV.include?('POSIXLY_CORRECT') order!(argv, into: into) else permute!(argv, into: into) diff --git a/sample/optparse/subcommand.rb b/sample/optparse/subcommand.rb new file mode 100755 index 0000000..96bcbb7 --- /dev/null +++ b/sample/optparse/subcommand.rb @@ -0,0 +1,23 @@ +#! /usr/bin/ruby +# contributed by Minero Aoki. + +require 'optparse' + +opts = {} +parser = OptionParser.new +parser.on('-i') { opts["i"] = true } +parser.on('-o') { puts["o"] = true } + +parser.subparser('add') {opts[:add] = {}} + .on('-i') { opts[:add]["i"] = true } +parser.subparser('del') {opts[:del] = {}}.then do |sub| + sub.on('-i') { opts[:del]["i"] = true } +end +parser.subparser('list') {opts[:list] = {}}.then do |sub| + sub.on('-iN', Integer) {|i| opts[:list]["i"] = i } +end + +h = {} +p parser.parse!(ARGV, into: h) +p h +p opts From ce01af957dd1ac1841e3c2604346ce578ed39744 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 21 Dec 2022 13:22:33 +0900 Subject: [PATCH 2/4] `raise_unknown` option --- lib/optparse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/optparse.rb b/lib/optparse.rb index f0e9db7..b472025 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -1643,7 +1643,7 @@ def sym_set(name, val) end end - def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: + def parse_in_order(argv = default_argv, setter = nil, raise_unknown: self.raise_unknown, &nonopt) # :nodoc: opt, arg, val, rest, sub = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { From 6c388b3da270a1988d387ae4c5a014ce9d364a3b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 21 Dec 2022 13:22:55 +0900 Subject: [PATCH 3/4] fixup! [WIP] subcommand parser --- sample/optparse/subcommand.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/optparse/subcommand.rb b/sample/optparse/subcommand.rb index 96bcbb7..2fb8fda 100755 --- a/sample/optparse/subcommand.rb +++ b/sample/optparse/subcommand.rb @@ -6,7 +6,7 @@ opts = {} parser = OptionParser.new parser.on('-i') { opts["i"] = true } -parser.on('-o') { puts["o"] = true } +parser.on('-o') { opts["o"] = true } parser.subparser('add') {opts[:add] = {}} .on('-i') { opts[:add]["i"] = true } From 4b131cea9ed8de96d37106f6886629fae799f703 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 21 Dec 2022 13:23:29 +0900 Subject: [PATCH 4/4] Extract `OptionParser#parse_option` --- lib/optparse.rb | 168 ++++++++++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 76 deletions(-) diff --git a/lib/optparse.rb b/lib/optparse.rb index b472025..0c06f39 100644 --- a/lib/optparse.rb +++ b/lib/optparse.rb @@ -1643,93 +1643,109 @@ def sym_set(name, val) end end + def parse_option(arg, argv, setter = nil) + case arg + when /\A--([^=]*)(?:=(.*))?/m + 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 + end + rescue ParseError + throw :terminate, arg unless raise_unknown + raise $!.set_option(arg, true) + 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 + rescue ParseError + raise $!.set_option(arg, rest) + end + + when /\A-(.)((=).*|.+)?/m + 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.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) + eq ||= !rest + end + end + rescue ParseError + throw :terminate, arg unless raise_unknown + raise $!.set_option(arg, true) + 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 + rescue ParseError + raise $!.set_option(arg, arg.length > 2) + end + + else + return false + end + + true + end + def parse_in_order(argv = default_argv, setter = nil, raise_unknown: self.raise_unknown, &nonopt) # :nodoc: opt, arg, val, rest, sub = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { while arg = argv.shift - case arg - # long option - when /\A--([^=]*)(?:=(.*))?/m - 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 - end - rescue ParseError - throw :terminate, arg unless raise_unknown - raise $!.set_option(arg, true) + next if parse_option(arg, argv, setter) + + # sub-command + if (key, (sub, block) = @subparsers&.complete(arg)) + block.call if block + if setter + into = setter.receiver.class.new.extend(SymSetter) + setter.call(key, into) + subsetter = into.method(:sym_set) 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 - rescue ParseError - raise $!.set_option(arg, rest) - end - - # short option - when /\A-(.)((=).*|.+)?/m - 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.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) - eq ||= !rest - end + pp argv: argv + sub.parse_in_order(argv, subsetter, raise_unknown: true) do |a| + pp arg: arg, a: a, argv: argv + nonopt.call(a) unless parse_option(a, argv) end - rescue ParseError - throw :terminate, arg unless raise_unknown - raise $!.set_option(arg, true) - 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 - rescue ParseError - raise $!.set_option(arg, arg.length > 2) - end - - # non-option argument - else - # sub-command - if (key, (sub, block) = @subparsers&.complete(arg)) - block.call if block - if setter - into = setter.receiver.class.new.extend(SymSetter) - setter.call(key, into) - setter = into.method(:sym_set) - end - return sub.parse_in_order(argv, setter, &nonopt) + rescue InvalidOption => e + e.recover(argv) + arg = argv.shift + retry if parse_option(arg, argv, setter) + raise end + end - catch(:prune) do - visit(:each_option) do |sw0| - sw = sw0 - sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg) - end - nonopt.call(arg) + catch(:prune) do + visit(:each_option) do |sw0| + sw = sw0 + sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg) end + nonopt.call(arg) end end