aargs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|