optparse2 0.5.3 → 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: 4b73eda58e3715e16d45898c5d4e22386a23ba2363d0708c02ea336aec51a631
4
- data.tar.gz: 777192092c696ec69bd6199fbcbd09819468dc0b06541268fa8d1a1ba4b4e389
3
+ metadata.gz: 6f61087f5b177ee9ffd0a587eb64ba8ff08893468ac8e0920621ee77d7b4a1b6
4
+ data.tar.gz: e5608123d52c5ad10d460f11ceb67bc9e9b0d5b145a502f9db16255e8df9ca0d
5
5
  SHA512:
6
- metadata.gz: c31c01a23b2d0b7020cc004d6a4dda0f24ed2dfe18464a5f8f71249502cedd425e4cb2bb341d4974e733b7e1e721b9f421e49ee022009780c3d512216df220fb
7
- data.tar.gz: e0d55f1dc1a4743a3b7d3e763272791d30895e89fcbc60ebab3554bce3f0fa1dc3dcbd0a40cef3248875d32b8d1c9da507c773e4e41e33c02065af0439e9676f
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.5.3"
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
@@ -18,54 +20,26 @@ class OptParse2
18
20
  @positional = []
19
21
  @required = Set[]
20
22
  @rest = nil
23
+ @group = nil
21
24
  self.pos_set_banner = OptParse2.pos_set_banner
22
25
  super
23
26
  end
24
27
 
25
- ## Helpers is a mixin that contains methods to modify how the original `Switch` works
26
- module Helpers
27
- def set_hidden
28
- def self.summarize(*) end
29
- end
30
-
31
- attr_writer :switch_name
32
- def switch_name; defined?(@switch_name) ? @switch_name : super end
33
-
34
- def set_switch_name_possibly_block_value(val)
35
- if @block.nil? && @arg.nil?
36
- q = switch_name.to_sym
37
- @block = proc { q }
38
- end
39
-
40
- self.switch_name = val
41
- end
42
-
43
-
44
- # requires `switch_name`, `desc` to work
45
- def set_default(value, description)
46
- if defined? value.call
47
- @default = value
48
- else
49
- @default = proc { value }
50
- end
51
-
52
- @default_description = description
53
- end
54
-
55
- def default = @default.call(switch_name)
56
- def default_description = @default_description || default.inspect
57
- def desc
58
- return super unless defined? @default
59
- x = super
60
- x << '' if x.empty?
61
- x[-1] += " [default: #{default_description}]"
62
- x
63
- end
64
- end
28
+ # A constant that, when returned, will not actually assign objects inside `into:`s.
29
+ DONT_ASSIGN = Object.new.freeze
65
30
 
66
31
  # Update `make_switch` to support OptParse2's keyword arguments
67
- def make_switch(opts, block, hidden: false, key: nil, default: nodefault=true, default_description: nil,
68
- 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
+ )
69
43
  sw, *rest = super(opts, block)
70
44
 
71
45
  sw.extend Helpers
@@ -73,6 +47,7 @@ class OptParse2
73
47
  sw.set_switch_name_possibly_block_value key
74
48
  end
75
49
  sw.set_hidden if hidden
50
+ sw.set_multiple multiple if multiple
76
51
 
77
52
  if (not_style = rest[2])
78
53
  not_style.extend Helpers
@@ -89,8 +64,10 @@ class OptParse2
89
64
 
90
65
  if nodefault && default_description != nil
91
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"
92
69
  elsif not nodefault
93
- sw.set_default(default, default_description)
70
+ sw.set_default(default, default_description, default_bypass)
94
71
  @defaults << sw
95
72
  end
96
73
 
@@ -102,26 +79,31 @@ class OptParse2
102
79
  on_tail("\n" + msg)
103
80
  end
104
81
 
105
- def order!(argv = default_argv, into: nil, **keywords, &nonopt)
106
- if into.nil? && !@defaults.empty?
107
- raise "cannot call `order!` without an `into:` if there are default values"
108
- end
109
-
110
- already_done = {}
111
- already_done.define_singleton_method(:[]=) do |key, value|
112
- super(key, value)
113
- into[key] = value
82
+ def group(name, default: nodefault=true)
83
+ old_group, @group = @group, name
84
+ yield
85
+ if !nodefault && !@defaults.any? { |x| x.switch_name.to_sym == name }
86
+ (orig_default = default; default = proc { orig_default }) unless default.respond_to?(:call)
87
+ @defaults << Struct.new(:switch_name, :default_){ def default = default_.() }.new(name, default) # TODO: This should probably be extracted out into a class lol
114
88
  end
89
+ ensure
90
+ @group = old_group
91
+ end
115
92
 
116
- non_options = []
93
+ alias _super_order! order!
117
94
 
118
- result = super(argv, into: already_done, **keywords, &non_options.method(:<<))
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?
119
99
 
120
- 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] }
121
102
 
103
+ # Fetch all positional arguments using the same option parsing code
122
104
  old_raise, self.raise_unknown = self.raise_unknown, false
123
105
  begin
124
- super(argv2, into: already_done, **keywords)
106
+ p _super_order!(argv, into: context, **keywords)
125
107
  rescue OptParse::InvalidArgument => err
126
108
  err.args[0] = @positional[err.args[0][/\d+/].to_i].name
127
109
  raise
@@ -129,33 +111,74 @@ class OptParse2
129
111
  self.raise_unknown = old_raise
130
112
  end
131
113
 
132
- 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
133
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)
134
122
  if @rest
135
- if argv2.length < @rest.fetch(:required, 0)
136
- 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)
137
125
  end
138
126
 
139
- argv2 = @rest[:block] ? @rest[:block].call(argv2) : argv2
140
- into[@rest[:key]] = argv2.dup if @rest[:key]
141
- argv2.clear
142
- elsif !argv2.empty? && self.raise_unknown && !@positional.empty?
143
- raise ParseError, "got unexpected positional argument: #{argv2.first}", caller(1)
144
- else
145
- 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)
146
132
  end
133
+ end
134
+
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)
147
139
 
148
- @defaults.each do |sw|
149
- key = sw.switch_name.to_sym
150
- next if already_done.key? key
151
- into[key] = sw.default()
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
152
146
  end
147
+ end
153
148
 
149
+ private def ensure_all_required_arguments_were_supplied!(context)
154
150
  @required.each do |key|
155
- 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
156
152
  end
153
+ end
154
+
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!
157
168
 
158
- argv2
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
159
182
  end
160
183
 
161
184
  module Positional
@@ -236,10 +259,56 @@ class OptParse2
236
259
  title += " (#{required} arg minimum)" if required > 0
237
260
 
238
261
  on sprintf "%s%-*s %s", summary_indent, summary_width, title, description.first
239
- description[1..].each do |descr|
262
+ description[1..]&.each do |descr|
240
263
  on sprintf "%s%-*s %s", summary_indent, summary_width, '', descr
241
264
  end
242
265
 
243
266
  @rest = { name:, key:, required: required || 0, block: }
244
267
  end
245
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
data/publish CHANGED
@@ -78,4 +78,7 @@ update_version unless $version.nil?
78
78
  ####################################################################################################
79
79
 
80
80
  system('gem', 'build', exception: true)
81
- system('gem', 'push', "#$gem-#$version.gem", exception: true) unless $dry
81
+ unless $dry
82
+ system('gem', 'push', "#$gem-#$version.gem", exception: true)
83
+ system('gem', 'install', $gem, exception: true)
84
+ 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.5.3
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