aargs 0.1.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 +7 -0
- data/lib/aargs.rb +441 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 72b94ed35ff9c3b27edacb27589a1d9da3aa08aa3c710033deb76666f218ff46
|
4
|
+
data.tar.gz: 7f530a6f57da8117bd146dedcffea0a2b78fa4b02b20e6261f61707e1c123149
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '08fd7a05ad0570d93f3e7ddcaa66950b0e9b814c9b56ee6810e9685076254cf49331b3221d3e4b1f7bdb8f7cd2e1c995ebcdcc740c82860815d3161d8934ee86'
|
7
|
+
data.tar.gz: 2cdc298a10b4371c16419f152034217d1408965c081812c57395322d25606eef447596d79db5e3d4dc63fc32da735101be7b05520ea9b92c2b84fe6586684f96
|
data/lib/aargs.rb
ADDED
@@ -0,0 +1,441 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
# Basic aargs parser
|
6
|
+
class Aargs
|
7
|
+
def self.kebab(sym)
|
8
|
+
sym.to_s.gsub(/[^[:alnum:]]/, '-')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.underscore(src)
|
12
|
+
src.gsub(/[^[:alnum:]]/, '_').to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.flagify_arg(arg)
|
16
|
+
case arg
|
17
|
+
when Symbol
|
18
|
+
"--#{kebab(arg)}"
|
19
|
+
when Hash
|
20
|
+
arg.map(&method(:flagify_kwarg)).flatten
|
21
|
+
else
|
22
|
+
arg
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.flagify_kwarg(arg, value)
|
27
|
+
case value
|
28
|
+
when TrueClass
|
29
|
+
"--#{kebab(arg)}"
|
30
|
+
when FalseClass
|
31
|
+
"--no-#{kebab(arg)}"
|
32
|
+
when Array
|
33
|
+
value.map { |v| "--#{kebab(arg)}=#{v}" }
|
34
|
+
else
|
35
|
+
"--#{kebab(arg)}=#{value}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convert symbolic arguments and keyword-arguments into an equivalent `ARGV`. Non-symbol argments remain unchanged.
|
40
|
+
# Note that to generate a epilogue portion of an ARGV you need to pass keyword arguments as explicit hashes followed
|
41
|
+
# by non-hash, non-symbol values.
|
42
|
+
def self.to_argv(*args)
|
43
|
+
args.map(&method(:flagify_arg)).flatten
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.parse(args_or_argv, aliases: {}, flag_configs: {})
|
47
|
+
argv = to_argv(*args_or_argv)
|
48
|
+
|
49
|
+
literal_only = false
|
50
|
+
prologue = []
|
51
|
+
epilogue = []
|
52
|
+
flags = {}
|
53
|
+
last_sym = nil
|
54
|
+
last_sym_pending = nil
|
55
|
+
|
56
|
+
resolve = lambda do |src|
|
57
|
+
raise "Missing value after '#{last_sym_pending}'" if last_sym_pending
|
58
|
+
|
59
|
+
sym = underscore(src)
|
60
|
+
aliases[sym] || sym
|
61
|
+
end
|
62
|
+
|
63
|
+
argv.each do |arg|
|
64
|
+
if literal_only
|
65
|
+
epilogue << arg
|
66
|
+
next
|
67
|
+
end
|
68
|
+
case arg
|
69
|
+
when /^--$/
|
70
|
+
literal_only = true
|
71
|
+
last_sym = nil
|
72
|
+
when /^-([[:alnum:]])$/
|
73
|
+
last_sym = sym = resolve.call(Regexp.last_match(1))
|
74
|
+
case flags[sym]
|
75
|
+
when true
|
76
|
+
flags[sym] = 2
|
77
|
+
when Integer
|
78
|
+
flags[sym] += 1
|
79
|
+
when nil
|
80
|
+
flags[sym] = true
|
81
|
+
else
|
82
|
+
raise "Unexpected boolean '#{arg}' after set to value #{flags[sym].inspect}"
|
83
|
+
end
|
84
|
+
|
85
|
+
when /^--(?<no>no-)?(?<flag>[[:alnum:]-]+)(?:=(?<value>.*))?$/
|
86
|
+
flag = Regexp.last_match[:flag]
|
87
|
+
value = Regexp.last_match[:value]
|
88
|
+
no = Regexp.last_match[:no]
|
89
|
+
sym = resolve.call(flag)
|
90
|
+
boolean = boolean?(sym, flag_configs: flag_configs)
|
91
|
+
if no
|
92
|
+
raise "Unexpected value specified with no- prefix: #{arg}" unless value.nil?
|
93
|
+
|
94
|
+
flags[sym] = false
|
95
|
+
last_sym = nil
|
96
|
+
elsif value.nil?
|
97
|
+
last_sym = boolean ? nil : sym
|
98
|
+
case flags[sym]
|
99
|
+
when true
|
100
|
+
flags[sym] = 2
|
101
|
+
when Integer
|
102
|
+
flags[sym] += 1
|
103
|
+
when nil, false
|
104
|
+
flags[sym] = true
|
105
|
+
else
|
106
|
+
last_sym_pending = arg
|
107
|
+
end
|
108
|
+
else
|
109
|
+
raise "Unexpected value for #{inspect_flag(arg)}: #{value.inspect}" if boolean
|
110
|
+
|
111
|
+
last_sym = nil
|
112
|
+
case flags[sym]
|
113
|
+
when nil
|
114
|
+
flags[sym] = value
|
115
|
+
when Array
|
116
|
+
flags[sym] << value
|
117
|
+
else
|
118
|
+
flags[sym] = [flags[sym], value]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
else
|
123
|
+
if last_sym
|
124
|
+
case flags[last_sym]
|
125
|
+
when true
|
126
|
+
flags[last_sym] = arg
|
127
|
+
when Array
|
128
|
+
flags[last_sym] << arg
|
129
|
+
else
|
130
|
+
flags[last_sym] = [flags[last_sym], arg]
|
131
|
+
end
|
132
|
+
last_sym_pending = nil
|
133
|
+
elsif flags.empty?
|
134
|
+
prologue << arg
|
135
|
+
else # first non-switch after switches + values
|
136
|
+
literal_only = true
|
137
|
+
epilogue << arg
|
138
|
+
end
|
139
|
+
end
|
140
|
+
next if arg.nil?
|
141
|
+
end
|
142
|
+
raise "Missing value after '#{last_sym_pending}'" if last_sym_pending
|
143
|
+
|
144
|
+
result = {}
|
145
|
+
result[:prologue] = prologue unless prologue.empty?
|
146
|
+
result[:flags] = flags unless flags.empty?
|
147
|
+
result[:epilogue] = epilogue unless epilogue.empty?
|
148
|
+
result unless result.empty?
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return Hash
|
152
|
+
attr_reader :aliases
|
153
|
+
|
154
|
+
# @returns Array
|
155
|
+
attr_reader :required_prologue
|
156
|
+
attr_reader :optional_prologue
|
157
|
+
attr_reader :prologue_key
|
158
|
+
attr_reader :flag_configs
|
159
|
+
attr_reader :required_epilogue
|
160
|
+
attr_reader :optional_epilogue
|
161
|
+
attr_reader :epilogue_key
|
162
|
+
|
163
|
+
DEFAULT = Object.new
|
164
|
+
|
165
|
+
def initialize(
|
166
|
+
prologue: DEFAULT,
|
167
|
+
flag_config: DEFAULT,
|
168
|
+
flag_configs: nil,
|
169
|
+
epilogue: DEFAULT,
|
170
|
+
aliases: {},
|
171
|
+
program: nil)
|
172
|
+
@program = program || begin
|
173
|
+
%r{^(?:.*/)?(?<file>[^/]+):\d+:in} =~ caller.first
|
174
|
+
file
|
175
|
+
end
|
176
|
+
@aliases = aliases.freeze
|
177
|
+
prologue_set = prologue && prologue != DEFAULT
|
178
|
+
flag_configs_set = flag_configs && flag_configs != DEFAULT
|
179
|
+
epilogue_set = epilogue && epilogue != DEFAULT
|
180
|
+
prologue = epilogue_set || flag_configs_set ? false : true if prologue == DEFAULT
|
181
|
+
initialize_prologue(prologue)
|
182
|
+
flag_config = flag_configs_set ? false : true if flag_config == DEFAULT
|
183
|
+
@flag_configs = Hash.new(flag_config).merge(flag_configs || {}).freeze
|
184
|
+
epilogue = prologue_set || flag_configs_set ? false : true if epilogue == DEFAULT
|
185
|
+
initialize_epilogue(epilogue)
|
186
|
+
@valid = false
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def initialize_prologue(prologue)
|
192
|
+
@required_prologue = []
|
193
|
+
@optional_prologue = []
|
194
|
+
@prologue_key = :prologue if prologue == true
|
195
|
+
@prologue_key = false if prologue == false
|
196
|
+
return unless @prologue_key.nil?
|
197
|
+
|
198
|
+
Array(prologue).each do |key|
|
199
|
+
/^(?<key>[[:alnum:]-]*)(?<optional>\?)?$/ =~ key
|
200
|
+
key = key.to_sym
|
201
|
+
if optional
|
202
|
+
@optional_prologue << key if optional
|
203
|
+
else
|
204
|
+
raise 'required prologue cannot follow optional prologue' unless @optional_prologue.empty?
|
205
|
+
|
206
|
+
@required_prologue << key
|
207
|
+
end
|
208
|
+
end
|
209
|
+
@required_prologue.freeze
|
210
|
+
@optional_prologue.freeze
|
211
|
+
end
|
212
|
+
|
213
|
+
def initialize_epilogue(epilogue)
|
214
|
+
@required_epilogue = []
|
215
|
+
@optional_epilogue = []
|
216
|
+
@epilogue_key = :epilogue if epilogue == true
|
217
|
+
@epilogue_key = false if epilogue == false
|
218
|
+
@epilogue_key = epilogue if epilogue.is_a?(Symbol)
|
219
|
+
return unless @epilogue_key.nil?
|
220
|
+
|
221
|
+
Array(epilogue).each do |key|
|
222
|
+
/^(?<key>[[:alnum:]-]*)(?<optional>\?)?$/ =~ key
|
223
|
+
key = key.to_sym
|
224
|
+
if optional
|
225
|
+
@optional_epilogue << key if optional
|
226
|
+
else
|
227
|
+
raise 'required epilogue cannot follow optional epilogue' unless @optional_epilogue.empty?
|
228
|
+
|
229
|
+
@required_epilogue << key
|
230
|
+
end
|
231
|
+
end
|
232
|
+
@required_epilogue = @required_epilogue.freeze
|
233
|
+
@optional_epilogue = @optional_epilogue.freeze
|
234
|
+
end
|
235
|
+
|
236
|
+
public
|
237
|
+
|
238
|
+
def self.flag_config(sym, flag_configs:)
|
239
|
+
flag_config = flag_configs[sym]
|
240
|
+
case flag_config
|
241
|
+
when true
|
242
|
+
{ type: :anything }
|
243
|
+
when Symbol
|
244
|
+
{ type: flag_config }
|
245
|
+
when nil
|
246
|
+
nil
|
247
|
+
when String
|
248
|
+
{ help: flag_config }
|
249
|
+
else
|
250
|
+
flag_config
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def self.flag_type(sym, flag_configs:)
|
255
|
+
config = flag_config(sym, flag_configs: flag_configs)
|
256
|
+
config[:type] if config
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.boolean?(sym, flag_configs:)
|
260
|
+
flag_type(sym, flag_configs: flag_configs) == :boolean
|
261
|
+
end
|
262
|
+
|
263
|
+
def flag_config(sym)
|
264
|
+
Aargs.flag_config(sym, flag_configs: flag_configs)
|
265
|
+
end
|
266
|
+
|
267
|
+
def flag_type(sym)
|
268
|
+
Aargs.flag_type(sym, flag_configs: flag_configs)
|
269
|
+
end
|
270
|
+
|
271
|
+
def boolean?(sym)
|
272
|
+
Aargs.boolean?(sym, flag_configs: flag_configs)
|
273
|
+
end
|
274
|
+
|
275
|
+
def required?(sym)
|
276
|
+
[required_prologue, required_epilogue].map(&method(:Array)).flatten.member?(sym)
|
277
|
+
end
|
278
|
+
|
279
|
+
def optional?(sym)
|
280
|
+
[optional_prologue, optional_epilogue].map(&method(:Array)).flatten.member?(sym)
|
281
|
+
end
|
282
|
+
|
283
|
+
def splat?(sym)
|
284
|
+
[prologue_key, epilogue_key].member?(sym)
|
285
|
+
end
|
286
|
+
|
287
|
+
def inspect_flag(sym)
|
288
|
+
arg = Aargs.kebab(sym)
|
289
|
+
return "#{arg.upcase}" if required?(sym)
|
290
|
+
return "[#{arg.upcase}]" if optional?(sym)
|
291
|
+
return "[aargs]" if sym == :any_key
|
292
|
+
return "[#{arg.to_s.upcase} ... [#{arg.to_s.upcase}]]" if splat?(sym)
|
293
|
+
return "--[no-]#{arg}" if boolean?(sym)
|
294
|
+
|
295
|
+
"--#{arg}=VALUE"
|
296
|
+
end
|
297
|
+
|
298
|
+
def help
|
299
|
+
prologue_keys = [required_prologue, optional_prologue, prologue_key ? prologue_key : nil].map(&method(:Array)).flatten
|
300
|
+
epilogue_keys = [required_epilogue, optional_epilogue, epilogue_key ? epilogue_key : nil].map(&method(:Array)).flatten
|
301
|
+
flag_keys = flag_configs.keys
|
302
|
+
flag_keys << :any_key if flag_configs[:any_key]
|
303
|
+
all_flags = prologue_keys + (flag_keys - prologue_keys) + epilogue_keys
|
304
|
+
usage = "Usage: #{@program} #{all_flags.map(&method(:inspect_flag)).join(' ')}"
|
305
|
+
any_real_help = false
|
306
|
+
lines = all_flags.map do |flag|
|
307
|
+
config = flag_config(flag)
|
308
|
+
next unless config
|
309
|
+
|
310
|
+
real_help = config[:help]
|
311
|
+
any_real_help ||= real_help
|
312
|
+
flag_help = real_help || case config[:type]
|
313
|
+
when :boolean
|
314
|
+
'(switch)'
|
315
|
+
else
|
316
|
+
"(#{config[:type]})"
|
317
|
+
end
|
318
|
+
[inspect_flag(flag), flag_help] if flag_help
|
319
|
+
end.compact
|
320
|
+
return [usage] if lines.empty? || !any_real_help
|
321
|
+
|
322
|
+
width = lines.map(&:first).map(&:length).max
|
323
|
+
lines.map! { |(flag, help)| format(" %<flag>-#{width}s : %<help>s", flag: flag, help: help) }
|
324
|
+
[usage, nil] + lines
|
325
|
+
end
|
326
|
+
|
327
|
+
def valid?
|
328
|
+
@valid
|
329
|
+
end
|
330
|
+
|
331
|
+
def parse(*args)
|
332
|
+
raise 'Aargs are frozen once parsed' if @valid
|
333
|
+
|
334
|
+
@parsed = Aargs.parse(args, aliases: aliases, flag_configs: flag_configs) || {}
|
335
|
+
@values = @parsed[:flags] || {}
|
336
|
+
parsed_prologue = @parsed[:prologue] || []
|
337
|
+
|
338
|
+
validate_sufficient_prologue(parsed_prologue)
|
339
|
+
consumed_prologue = apply_prologue(parsed_prologue)
|
340
|
+
apply_epilogue(parsed_prologue, consumed_prologue)
|
341
|
+
@valid = true
|
342
|
+
self
|
343
|
+
end
|
344
|
+
|
345
|
+
# @return if the given key is a known flag that should appear as part of the object's API
|
346
|
+
def api_key?(key)
|
347
|
+
@values.member?(key) || @optional_prologue.member?(key) || @flag_configs.member?(key)
|
348
|
+
end
|
349
|
+
|
350
|
+
def respond_to_missing?(sym, *_)
|
351
|
+
/^(?<key>.*?)(?:(?<_boolean>\?))?$/ =~ sym
|
352
|
+
key = key.to_sym
|
353
|
+
# puts(sym: sym, key: key, values: @values)
|
354
|
+
return super unless api_key?(key)
|
355
|
+
|
356
|
+
true
|
357
|
+
end
|
358
|
+
|
359
|
+
def method_missing(sym, *_)
|
360
|
+
return super unless @parsed
|
361
|
+
|
362
|
+
/^(?<key>.*?)(?:(?<boolean>\?))?$/ =~ sym
|
363
|
+
key = key.to_sym
|
364
|
+
return super unless api_key?(key)
|
365
|
+
|
366
|
+
value = @values[key]
|
367
|
+
return !(!value) if boolean
|
368
|
+
|
369
|
+
value
|
370
|
+
end
|
371
|
+
|
372
|
+
private
|
373
|
+
|
374
|
+
# Validate that we have enough arguments given to satisfy our required prologue, taking into account any that were
|
375
|
+
# specified as flags.
|
376
|
+
def validate_sufficient_prologue(parsed_prologue)
|
377
|
+
return if prologue_key
|
378
|
+
|
379
|
+
pp(required_prologue: required_prologue, values: @values)
|
380
|
+
actual_required_prologue = required_prologue - @values.keys
|
381
|
+
return if actual_required_prologue.length <= parsed_prologue.length
|
382
|
+
|
383
|
+
missing_flags = actual_required_prologue.drop(parsed_prologue.length)
|
384
|
+
raise "Missing positional arguments: #{missing_flags.map(&method(:inspect_flag)).join(', ')}"
|
385
|
+
end
|
386
|
+
|
387
|
+
# Validate that we have enough arguments given to satisfy our required prologue, taking into account any that were
|
388
|
+
# specified as flags.
|
389
|
+
def validate_sufficient_epilogue(parsed_epilogue)
|
390
|
+
return if epilogue_key
|
391
|
+
|
392
|
+
actual_required_epilogue = required_epilogue - @values.keys
|
393
|
+
return if actual_required_epilogue.length <= parsed_epilogue.length
|
394
|
+
|
395
|
+
missing_flags = actual_required_epilogue.drop(parsed_epilogue.length)
|
396
|
+
raise "Missing positional arguments: #{missing_flags.map(&method(:inspect_flag)).join(', ')}"
|
397
|
+
end
|
398
|
+
|
399
|
+
# Reverse-merge prologue values into {@link @values}
|
400
|
+
# @return [Hash] the recognized prologue flags
|
401
|
+
def apply_prologue(parsed_prologue)
|
402
|
+
return @values[prologue_key] = parsed_prologue if prologue_key
|
403
|
+
|
404
|
+
# Remove any prologue keys whose values appeared as flags:
|
405
|
+
expected_prologue = (required_prologue + optional_prologue) - @values.keys
|
406
|
+
# Convert the prologue into a hash based on the prologue keys we're still waiting for:
|
407
|
+
consumed_prologue = expected_prologue.zip(parsed_prologue).reject do |_, v|
|
408
|
+
# Avoid nil values since they're never returned from {@link Aargs.parse}
|
409
|
+
v.nil?
|
410
|
+
end.to_h
|
411
|
+
@values = consumed_prologue.merge(@values)
|
412
|
+
consumed_prologue
|
413
|
+
end
|
414
|
+
|
415
|
+
# Any extra prologue values become the beginning of the epilogue.
|
416
|
+
# Reverse-merge epilogue values into {@link @values}
|
417
|
+
# @raise if there's an epilogue given but we don't expect one
|
418
|
+
# @see epilogue_key
|
419
|
+
def apply_epilogue(parsed_prologue, consumed_prologue)
|
420
|
+
parsed_epilogue = parsed_prologue.drop(consumed_prologue.length).concat(Array(@parsed[:epilogue]))
|
421
|
+
|
422
|
+
# TODO: allow ... after required/optional consumed
|
423
|
+
|
424
|
+
# Remove any epilogue keys whose values appeared as flags:
|
425
|
+
epilogue_keys = [required_epilogue, optional_epilogue].map(&method(:Array)).flatten
|
426
|
+
expected_epilogue = epilogue_keys - @values.keys
|
427
|
+
# Convert the epilogue into a hash based on the epilogue keys we're still waiting for:
|
428
|
+
consumed_epilogue = expected_epilogue.zip(parsed_epilogue).reject do |_, v|
|
429
|
+
# Avoid nil values since they're never returned from {@link Aargs.parse}
|
430
|
+
v.nil?
|
431
|
+
end.to_h
|
432
|
+
@values = consumed_epilogue.merge(@values)
|
433
|
+
|
434
|
+
epilogue = parsed_epilogue.drop(consumed_epilogue.length)
|
435
|
+
return if epilogue.empty?
|
436
|
+
raise "Unexpected epilogue: #{epilogue.inspect}" unless epilogue_key
|
437
|
+
|
438
|
+
@values[epilogue_key] = epilogue
|
439
|
+
nil
|
440
|
+
end
|
441
|
+
end
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aargs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua Pollak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |-
|
14
|
+
== `aargs` is a Better Ruby Options Parser
|
15
|
+
Provide a consistent shell and IRB experience for your users with `aargs`. “Do we have an accord?”
|
16
|
+
email: abottomlesspit@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/aargs.rb
|
22
|
+
homepage: https://rubygems.org/gems/aargs
|
23
|
+
licenses:
|
24
|
+
- MIT
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubygems_version: 3.0.3
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: "“…more what you’d call “guidelines” than actual rules.”"
|
45
|
+
test_files: []
|