optparse2 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b99d0120a660e1c0831e8bb7498a7628f2e5e77bef266540ffdb7022120190e0
4
- data.tar.gz: 7a9e744ba67a2f23e0a69fde19f30807867ac259902cbaeff477c5b64b3c3627
3
+ metadata.gz: 6f61087f5b177ee9ffd0a587eb64ba8ff08893468ac8e0920621ee77d7b4a1b6
4
+ data.tar.gz: e5608123d52c5ad10d460f11ceb67bc9e9b0d5b145a502f9db16255e8df9ca0d
5
5
  SHA512:
6
- metadata.gz: f08a3c93c23ca10eb994e63317726c2c2b13407e7b80bdf1cedd095c9066679ac1c8d552c3691f3b87e9596c6c147def64a605a29794388d29766da3a5ccb4e4
7
- data.tar.gz: 40f819a15e6f05be7dd1e0ce72ac264492405415386840fad30543ef277ff2e93b048e7963693ec682b82bc410e30406d5a85c08a551f2337bcc87d4f3c68684
6
+ metadata.gz: 1155a87469ff90eaf3734338e4b883f97077e85327b4099a83bf360dd5f63bc5f136da6c3dee8a4de1ce0c0d8f98c1f85ae180f845ed0615dee4aee98747b345
7
+ data.tar.gz: e7099e4a6c7e2c0021a35c3b8b95ecafd632ed63bf86f26e9aca2884c42bb749d3f03c1baab1ad3527a90fb4a5434c2540ca1ac6f1ed4ce3167e7fa4eeedcc14
@@ -0,0 +1,51 @@
1
+ class OptParse2
2
+ # A Context is in charge of all option parsing
3
+ class Context
4
+ class << self
5
+ attr_accessor :current
6
+
7
+ def with_context(...)
8
+ context = new(...)
9
+ old, self.current = current, context
10
+ yield context
11
+ ensure
12
+ self.current = old
13
+ end
14
+ end
15
+
16
+ attr_reader :non_options, :deferred_options, :already_parsed_options
17
+
18
+ def initialize(into:, nonopt:)
19
+ @into = into
20
+ @nonopt = nonopt
21
+
22
+ @already_parsed_options = {}
23
+ @non_options = []
24
+ @deferred_options = {}
25
+ end
26
+
27
+ def add_non_option(non_option)
28
+ @non_options << non_option
29
+ end
30
+
31
+ def handle_deferred!
32
+ @deferred_options.each do |key, deferred|
33
+ self[key] = deferred[:proc].call(deferred[:data]) if deferred[:proc]
34
+ end
35
+ end
36
+
37
+ def key?(key)
38
+ @already_parsed_options.key?(key)
39
+ end
40
+
41
+ # Directly assigns `value` to `key`, both in the internal list of parsed
42
+ # options, as well as in the `into:` option (if one is provided).
43
+ #
44
+ # Note that if `value` is `DONT_ASSIGN` nothing happens
45
+ def []=(key, value)
46
+ return if DONT_ASSIGN.equal?(value)
47
+ @already_parsed_options[key] = value
48
+ @into[key] = value if @into
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,171 @@
1
+ class OptParse2
2
+ ## Helpers is a mixin that contains methods to modify how the original `Switch` works
3
+
4
+ module Helpers
5
+ # Mark the switch as hidden (so it won't show up in the usage)
6
+ def set_hidden
7
+ def self.summarize(*) end
8
+ end
9
+
10
+ # The name of the switch; this is the key that's used when assigning options into an `into:`.
11
+ # It normally corresponds to the flag name (`--foo-bar` -> `foo-bar`), but can be overwritten
12
+ # as needed.
13
+ attr_writer :switch_name
14
+ def switch_name
15
+ defined?(@switch_name) ? @switch_name : super
16
+ end
17
+
18
+ def set_multiple(multiple)
19
+ old_block = @block
20
+ sw = switch_name.to_sym
21
+
22
+ case multiple
23
+ in :first!
24
+ @block = ->(arg, **nil) do
25
+ ctx = OptParse2::Context.current
26
+ if ctx.deferred_options.key? sw
27
+ OptParse2::DONT_ASSIGN
28
+ else
29
+ ctx.deferred_options[sw] = {}
30
+ old_block ? old_block.call(arg) : arg
31
+ end
32
+ end
33
+ in :first
34
+ @block = ->(arg, **nil) do
35
+ ctx = OptParse2::Context.current
36
+ if ctx.deferred_options.key? sw
37
+ OptParse2::DONT_ASSIGN
38
+ else
39
+ ctx.deferred_options[sw] = {
40
+ proc: old_block ? proc{ old_block.call(arg) } : proc { arg }
41
+ }
42
+ OptParse2::DONT_ASSIGN
43
+ end
44
+ end
45
+ in :last!, nil
46
+ # don't do anything, this is the default behaviour
47
+ in :last
48
+ @block = ->(arg, **nil) do
49
+ ctx = OptParse2::Context.current
50
+ ctx.deferred_options[sw] = {
51
+ proc: old_block ? proc { old_block.call(arg) } : proc { arg }
52
+ }
53
+
54
+ OptParse2::DONT_ASSIGN
55
+ end
56
+ in :raise
57
+ @block = ->(arg) do
58
+ ctx = OptParse2::Context.current
59
+ if ctx.already_parsed_options.key? sw or ctx.deferred_options.key? sw
60
+ raise OptParse2::ParseError, "encountered repeated option"
61
+ end
62
+
63
+ old_block ? old_block.call(arg) : arg
64
+ end
65
+
66
+ in :count
67
+ @block = ->(amnt, **nil) do
68
+ ctx = OptParse2::Context.current
69
+
70
+ ctx.deferred_options[sw] ||= {
71
+ proc: old_block ? proc { |data| old_block.call(data) } : proc { |data| data },
72
+ data: 0
73
+ }
74
+
75
+ if amnt == true || amnt.nil?
76
+ ctx.deferred_options[sw][:data] += 1
77
+ else
78
+ ctx.deferred_options[sw][:data] = amnt
79
+ end
80
+
81
+ OptParse2::DONT_ASSIGN
82
+ end
83
+
84
+ in :count!
85
+ @block = ->(amnt, *a, **k, &b) do
86
+ ctx = OptParse2::Context.current
87
+
88
+ ctx.deferred_options[sw] ||= { data: 0 }
89
+
90
+ if amnt == true || amnt.nil?
91
+ ctx.deferred_options[sw][:data] += 1
92
+ else
93
+ ctx.deferred_options[sw][:data] = amnt
94
+ end
95
+
96
+ new_amnt = old_block ? old_block.call(ctx.deferred_options[sw][:data], *a, **k, &b) : ctx.deferred_options[sw][:data]
97
+ ctx.deferred_options[sw][:data] = new_amnt unless OptParse2::DONT_ASSIGN.equal? new_amnt
98
+
99
+ new_amnt
100
+ end
101
+
102
+ in :collect | [:collect, _]
103
+ transform = Array(multiple)[1]
104
+ @block = ->(arg, **nil) do
105
+ ctx = OptParse2::Context.current
106
+ ctx.deferred_options[sw] ||= {
107
+ proc: proc { |data| old_block ? old_block.call(data) : data },
108
+ data: []
109
+ }
110
+
111
+ ctx.deferred_options[sw][:data] << (transform ? transform.(arg) : arg)
112
+ OptParse2::DONT_ASSIGN
113
+ end
114
+
115
+ else
116
+ raise ArgumentError, "invalid multiple type: #{multiple}", caller(2)
117
+ end
118
+ end
119
+
120
+ # Same as `switch_name`, except it also will set the block to just return the original switch
121
+ # name as a symbol. Useful for group switches which don't actually have blocks:
122
+ # op.on '--interactive', key: :mode
123
+ # op.on '--force', key: :mode
124
+ # instead of:
125
+ # op.on '--interactive', key: :mode do :interactive end
126
+ # op.on '--force', key: :mode do :force end
127
+ # without this method, passing `--interactive` would just set `:mode` to `true`.
128
+ #
129
+ # This only happens if no block exists, and the argument does not take an arg.
130
+ def set_switch_name_possibly_block_value(val)
131
+ if @block.nil? && @arg.nil?
132
+ old_switch_name = switch_name.to_sym
133
+ @block = proc { old_switch_name }
134
+ end
135
+
136
+ self.switch_name = val
137
+ end
138
+
139
+ # Default values of switches are used when the switch is never passed in.
140
+ # If the `value` that's provided doesn't respond to `.call`, it's converted to a proc.
141
+ # If `bypass` is truthy, then the default value is never passed to the block's proc (if any)
142
+ def set_default(value, description, bypass)
143
+ if @arg.nil? && value != true && !bypass
144
+ raise ArgumentError, "Cannot supply a non-true default value to a flag which takes no arguments", caller(4)
145
+ end
146
+
147
+ if defined? value.call
148
+ @default_proc = value
149
+ else
150
+ @default_proc = proc { |_switch_name| value }
151
+ end
152
+
153
+ @default_description = description
154
+ @default_bypass = bypass
155
+ end
156
+
157
+ # Calls the default proc to figure out what the default value is for this switch
158
+ def default_bypass? = @default_bypass
159
+ def default? = defined?(@default_proc)
160
+ def default = @default_proc&.call(switch_name)
161
+
162
+ def default_description = @default_description || default.inspect
163
+ def desc
164
+ return super unless defined? @default
165
+ x = super
166
+ x << '' if x.empty?
167
+ x[-1] += " [default: #{default_description}]"
168
+ x
169
+ end
170
+ end
171
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptParse2
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/optparse2.rb CHANGED
@@ -6,6 +6,8 @@ OptionParser2 = OptParse2 # Alias
6
6
 
7
7
  require_relative "optparse2/version"
8
8
  require_relative "optparse2/fixes"
9
+ require_relative "optparse2/switch-helpers"
10
+ require_relative "optparse2/context"
9
11
 
10
12
  class OptParse2
11
13
  class << self
@@ -13,63 +15,31 @@ class OptParse2
13
15
  end
14
16
  self.pos_set_banner = true
15
17
 
16
- attr_reader :into
17
-
18
18
  def initialize(...)
19
19
  @defaults = Set[]
20
20
  @positional = []
21
21
  @required = Set[]
22
22
  @rest = nil
23
- @into = nil
24
23
  @group = nil
25
24
  self.pos_set_banner = OptParse2.pos_set_banner
26
25
  super
27
26
  end
28
27
 
29
- ## Helpers is a mixin that contains methods to modify how the original `Switch` works
30
- module Helpers
31
- def set_hidden
32
- def self.summarize(*) end
33
- end
34
-
35
- attr_writer :switch_name
36
- def switch_name; defined?(@switch_name) ? @switch_name : super end
37
-
38
- def set_switch_name_possibly_block_value(val)
39
- if @block.nil? && @arg.nil?
40
- q = switch_name.to_sym
41
- @block = proc { q }
42
- end
43
-
44
- self.switch_name = val
45
- end
46
-
47
-
48
- # requires `switch_name`, `desc` to work
49
- def set_default(value, description)
50
- if defined? value.call
51
- @default = value
52
- else
53
- @default = proc { value }
54
- end
55
-
56
- @default_description = description
57
- end
58
-
59
- def default = @default.call(switch_name)
60
- def default_description = @default_description || default.inspect
61
- def desc
62
- return super unless defined? @default
63
- x = super
64
- x << '' if x.empty?
65
- x[-1] += " [default: #{default_description}]"
66
- x
67
- end
68
- end
28
+ # A constant that, when returned, will not actually assign objects inside `into:`s.
29
+ DONT_ASSIGN = Object.new.freeze
69
30
 
70
31
  # Update `make_switch` to support OptParse2's keyword arguments
71
- def make_switch(opts, block, hidden: false, key: @group, default: nodefault=true, default_description: nil,
72
- required: false)
32
+ def make_switch(
33
+ opts,
34
+ block,
35
+ hidden: false,
36
+ key: @group,
37
+ default: nodefault=true,
38
+ default_bypass: false,
39
+ default_description: nil,
40
+ required: false,
41
+ multiple: nil
42
+ )
73
43
  sw, *rest = super(opts, block)
74
44
 
75
45
  sw.extend Helpers
@@ -77,6 +47,7 @@ class OptParse2
77
47
  sw.set_switch_name_possibly_block_value key
78
48
  end
79
49
  sw.set_hidden if hidden
50
+ sw.set_multiple multiple if multiple
80
51
 
81
52
  if (not_style = rest[2])
82
53
  not_style.extend Helpers
@@ -93,8 +64,10 @@ class OptParse2
93
64
 
94
65
  if nodefault && default_description != nil
95
66
  raise ArgumentError, "default: not supplied, but default_description: given"
67
+ elsif nodefault && default_bypass
68
+ raise ArgumentError, "default: not supplied, but default_bypass: given"
96
69
  elsif not nodefault
97
- sw.set_default(default, default_description)
70
+ sw.set_default(default, default_description, default_bypass)
98
71
  @defaults << sw
99
72
  end
100
73
 
@@ -117,31 +90,20 @@ class OptParse2
117
90
  @group = old_group
118
91
  end
119
92
 
120
- def order!(argv = default_argv, into: nil, **keywords, &nonopt)
121
- if into.nil? && !@defaults.empty?
122
- raise "cannot call `order!` without an `into:` if there are default values"
123
- end
124
-
125
- already_done = {}
126
- already_done.define_singleton_method(:[]=) do |key, value|
127
- super(key, value)
128
- into[key] = value
129
- end
130
-
131
- # Really this shouldn't be on the class and should probably be passed to the block of each
132
- # parameter as requested, but that requires a _significant_ amount of tinkering with `optparse`'s
133
- # internals, which is not really in scope.
134
- @into = already_done
93
+ alias _super_order! order!
135
94
 
136
- non_options = []
95
+ # Parses all positional arguments from `argv` into `into`. Replaces `argv` with non-positional
96
+ # arguments when it's done.
97
+ private def parse_positional_arguments!(argv, context, keywords)
98
+ return if argv.empty? || @positional.empty?
137
99
 
138
- result = super(argv, into: already_done, **keywords, &non_options.method(:<<))
139
-
140
- argv2 = (non_options + result).each_with_index.flat_map { ["--*-positional-#{_2}", _1] }
100
+ # Prepend argument number to the argument array
101
+ argv.replace argv.each_with_index.flat_map { |value, idx| ["--*-positional-#{idx}", value] }
141
102
 
103
+ # Fetch all positional arguments using the same option parsing code
142
104
  old_raise, self.raise_unknown = self.raise_unknown, false
143
105
  begin
144
- super(argv2, into: already_done, **keywords)
106
+ p _super_order!(argv, into: context, **keywords)
145
107
  rescue OptParse::InvalidArgument => err
146
108
  err.args[0] = @positional[err.args[0][/\d+/].to_i].name
147
109
  raise
@@ -149,35 +111,74 @@ class OptParse2
149
111
  self.raise_unknown = old_raise
150
112
  end
151
113
 
152
- argv2 = argv2.each_slice(2).map { _2 }
114
+ # Delete any non-matching arguments. TODO: Can this be the return value of `_super_order!` ?
115
+ argv.replace argv.each_slice(2).map { |_flag_name, value| value }
116
+ end
153
117
 
118
+ # If a "rest" parameter was given, populates it. Also raises a ParseError exception for unexepcted
119
+ # positionals if no `.rest` parameter is present, `self.raise_unknown` is set, and at least one `.pos`
120
+ # positional argument was supplied
121
+ private def parse_rest_argument!(argv, context)
154
122
  if @rest
155
- if argv2.length < @rest.fetch(:required, 0)
156
- raise ParseError, "at least #{@rest[:required]} trailing arguments required (only got #{argv2.length})", caller(1)
123
+ if argv.length < @rest.fetch(:required, 0)
124
+ raise ParseError, "at least #{@rest[:required]} trailing arguments required (only got #{argv.length})", caller(1)
157
125
  end
158
126
 
159
- argv2 = @rest[:block] ? @rest[:block].call(argv2) : argv2
160
- into[@rest[:key]] = argv2.dup if @rest[:key]
161
- argv2.clear
162
- elsif !argv2.empty? && self.raise_unknown && !@positional.empty?
163
- raise ParseError, "got unexpected positional argument: #{argv2.first}", caller(1)
164
- else
165
- argv2.each(&nonopt)
127
+ argv = @rest[:block] ? @rest[:block].call(argv) : argv
128
+ context[@rest[:key]] = argv.dup if @rest[:key]
129
+ argv.clear
130
+ elsif !argv.empty? && self.raise_unknown && !@positional.empty?
131
+ raise ParseError, "got unexpected positional argument: #{argv.first}", caller(1)
166
132
  end
133
+ end
167
134
 
168
- @defaults.each do |sw|
169
- key = sw.switch_name.to_sym
170
- next if already_done.key? key
171
- into[key] = sw.default()
135
+ # Goes thru every default option, and calls
136
+ private def assign_defaults!(context)
137
+ visit :each_option do |sw|
138
+ next if !sw.default? || context.key?(key = sw.switch_name.to_sym)
139
+
140
+ if sw.default_bypass?
141
+ context[key] = sw.default
142
+ else
143
+ flag = sw.short.first || sw.long.first || raise("<INTERNAL ERROR: CAN THIS EVER HAPPEN?>")
144
+ _super_order! [flag, sw.default], into: context
145
+ end
172
146
  end
147
+ end
173
148
 
149
+ private def ensure_all_required_arguments_were_supplied!(context)
174
150
  @required.each do |key|
175
- raise ParseError, "required option '#{key}' not provided" unless already_done.key? key.to_sym
151
+ raise ParseError, "required option '#{key}' not provided" unless context.key? key.to_sym
176
152
  end
153
+ end
177
154
 
178
- argv2
179
- ensure
180
- @into = nil # make sure we unset it when returning
155
+ def order!(argv = default_argv, into: nil, **keywords, &nonopt)
156
+ Context.with_context into:, nonopt: do |context|
157
+
158
+ # Parse all normal options in the command line
159
+ non_options = []
160
+ trailing_options = super(argv, into: context, **keywords, &non_options.method(:<<))
161
+ not_matched_options = non_options + trailing_options
162
+
163
+ # Now parse positional arguments and the "rest" argument
164
+ parse_positional_arguments!(not_matched_options, context, keywords)
165
+ parse_rest_argument!(not_matched_options, context)
166
+
167
+ context.handle_deferred!
168
+
169
+ # Now handle defaults---anything with a default that hasn't been assigned so far is set
170
+ assign_defaults!(context)
171
+
172
+ # Now that all arguments are parsed, and the defaults have been handled, check to make sure
173
+ # that all required arguments are handled.
174
+ ensure_all_required_arguments_were_supplied!(context)
175
+
176
+ # For each non-option argument, call the `nonopt` block
177
+ not_matched_options.each(&nonopt)
178
+
179
+ # Replace the original argv with the resulting options
180
+ argv.replace not_matched_options
181
+ end
181
182
  end
182
183
 
183
184
  module Positional
@@ -258,10 +259,56 @@ class OptParse2
258
259
  title += " (#{required} arg minimum)" if required > 0
259
260
 
260
261
  on sprintf "%s%-*s %s", summary_indent, summary_width, title, description.first
261
- description[1..].each do |descr|
262
+ description[1..]&.each do |descr|
262
263
  on sprintf "%s%-*s %s", summary_indent, summary_width, '', descr
263
264
  end
264
265
 
265
266
  @rest = { name:, key:, required: required || 0, block: }
266
267
  end
267
268
  end
269
+
270
+ __END__
271
+ OptParse2.new do |op|
272
+ op.on '--foo=FOO', multiple: :first! do puts "FOO: #{it}"; it end
273
+ op.on '--bar=BAR' do puts "BAR: #{it}"; it end
274
+
275
+ op.on '-v', '--verbose[=X]', Integer, multiple: :count
276
+
277
+ # op.on '-v', '--verbose[=X]', Integer, multiple: :count! do
278
+ # puts "verbose is: #{it}"
279
+ # it
280
+ # end
281
+
282
+ op.on '-aF', Integer, multiple: [:collect, :succ.to_proc] do |x|
283
+ p x
284
+ end
285
+
286
+ op.parse! %w[ -vvv --foo=abc --bar=123 --foo=xyz -a3 -a4 -a5 ], into: opts={}
287
+ p opts
288
+ end
289
+
290
+ # OptParse.new do |op|
291
+ # op.on '--foo=bar', /(.)(.)/ do |x| p x end
292
+ # op.parse! %w[ --foo=34 ]
293
+ # end
294
+ __END__
295
+ OptionParser2.new do |op|
296
+ op.on '-e', default: true
297
+ op.on '--bar1=ALL', default: 'hello', &:upcase
298
+ op.on '--doit=WHAT', /(.)(.)/, key: :A, default: 'xu' do
299
+ p [_1, _2, 'both!']
300
+ end
301
+
302
+ op.pos 'foo', required: true do 3 end
303
+ op.pos 'bar', required: true
304
+ op.pos 'baz', required: false
305
+ op.pos 'quux', required: false
306
+ # op.rest 'files'
307
+
308
+
309
+ rest = op.order!(argv = %w[ --doit 3q a b ] , into: opts={})
310
+
311
+ puts "rest=#{rest}"
312
+ puts "argv=#{argv}"
313
+ puts "opts=#{opts}"
314
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optparse2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SamW
@@ -19,8 +19,10 @@ files:
19
19
  - README.md
20
20
  - Rakefile
21
21
  - lib/optparse2.rb
22
+ - lib/optparse2/context.rb
22
23
  - lib/optparse2/fixes.rb
23
24
  - lib/optparse2/pathname.rb
25
+ - lib/optparse2/switch-helpers.rb
24
26
  - lib/optparse2/version.rb
25
27
  - publish
26
28
  - sig/optparse2.rbs