parse-argv 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/.yardopts +5 -0
- data/LICENSE +28 -0
- data/ReadMe.md +101 -0
- data/examples/ReadMe.md +29 -0
- data/examples/check.rb +109 -0
- data/examples/conversion.rb +47 -0
- data/examples/multi.rb +79 -0
- data/examples/simple.rb +37 -0
- data/lib/parse-argv/conversion.rb +398 -0
- data/lib/parse-argv/version.rb +8 -0
- data/lib/parse-argv.rb +900 -0
- data/lib/parse_argv.rb +3 -0
- data/syntax.md +158 -0
- metadata +70 -0
@@ -0,0 +1,398 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module ParseArgv
|
6
|
+
#
|
7
|
+
# The Conversion module provides an interface to convert String arguments to
|
8
|
+
# different types and is used by the {Result::Value#as} method.
|
9
|
+
#
|
10
|
+
# Besides the build-in defined types custom conversion functions can be
|
11
|
+
# defined with {Conversion.define}.
|
12
|
+
#
|
13
|
+
# In general a conversion function is a Proc which gets called with a String
|
14
|
+
# argument, an error handler and - optional - with function-specific
|
15
|
+
# arguments and options.
|
16
|
+
#
|
17
|
+
# The conversion function should convert the given String argument and return
|
18
|
+
# the result. When it's impossible to convert the argument the error handler
|
19
|
+
# have to be called with a descriptive error message.
|
20
|
+
#
|
21
|
+
# Here is an example for a conversion function of even Integer:
|
22
|
+
#
|
23
|
+
# ```
|
24
|
+
# ParseArgv::Conversion.define(:even_number) do |arg, &err|
|
25
|
+
# /\A-?\d+/.match?(arg) or err['argument have to be an even integer']
|
26
|
+
# result = arg.to_i
|
27
|
+
# result.even? ? result : err['not an even integer']
|
28
|
+
# end
|
29
|
+
# ```
|
30
|
+
#
|
31
|
+
# ## Build-In Conversion Functions
|
32
|
+
#
|
33
|
+
# <table>
|
34
|
+
# <thead><th>Name</th><th>Alias</th><th>Description</th></thead>
|
35
|
+
# <tbody>
|
36
|
+
# <tr>
|
37
|
+
# <td>:integer</td><td>Integer</td>
|
38
|
+
# <td>
|
39
|
+
# convert to <code>Integer</code>; it allows additional checks like
|
40
|
+
# :positive, :negative, :nonzero
|
41
|
+
# </td>
|
42
|
+
# </tr>
|
43
|
+
# <tr>
|
44
|
+
# <td>:float</td><td>Float</td>
|
45
|
+
# <td>
|
46
|
+
# convert to <code>Float</code>; it allows additional checks like
|
47
|
+
# :positive, :negative, :nonzero
|
48
|
+
# </td>
|
49
|
+
# </tr>
|
50
|
+
# <tr>
|
51
|
+
# <td>:number</td><td>Numeric</td>
|
52
|
+
# <td>
|
53
|
+
# convert to <code>Float</code> or <code>Integer</code>; it
|
54
|
+
# allows additional checks like :positive, :negative, :nonzero
|
55
|
+
# </td>
|
56
|
+
# </tr>
|
57
|
+
# <tr>
|
58
|
+
# <td>:byte</td><td></td>
|
59
|
+
# <td>
|
60
|
+
# convert to <code>Integer</code>; argument can have suffix
|
61
|
+
# <code>k</code>ilo, <code>M</code>ega, <code>G</code>iga,
|
62
|
+
# <code>T</code>era, <code>P</code>eta, <code>E</code>xa,
|
63
|
+
# <code>Z</code>etta and <code>Y</code>otta ('0.5M' == 524288)
|
64
|
+
# </td>
|
65
|
+
# </tr>
|
66
|
+
# <tr>
|
67
|
+
# <td>:string</td><td>String</td>
|
68
|
+
# <td>passes a non-empty string argument</td>
|
69
|
+
# </tr>
|
70
|
+
# <tr>
|
71
|
+
# <td>:file_name</td><td></td>
|
72
|
+
# <td>convert to file name; uses <code>File#expand_path</code></td>
|
73
|
+
# </tr>
|
74
|
+
# <tr>
|
75
|
+
# <td>:regexp</td><td>Regexp</td>
|
76
|
+
# <td>convert to a <code>Regexp</code></td>
|
77
|
+
# </tr>
|
78
|
+
# <tr>
|
79
|
+
# <td>:array</td><td>Array</td>
|
80
|
+
# <td>convert to a <code>Array<String></code></td>
|
81
|
+
# </tr>
|
82
|
+
# <tr>
|
83
|
+
# <td>:date</td><td>Date</td>
|
84
|
+
# <td>
|
85
|
+
# convert to a <code>Date</code>; accepts optional a <code>Date</code>
|
86
|
+
# or <code>Time</code> as :reference option
|
87
|
+
# </td>
|
88
|
+
# </tr>
|
89
|
+
# <tr>
|
90
|
+
# <td>:time</td><td>Time</td>
|
91
|
+
# <td>
|
92
|
+
# convert to a <code>Time</code>>; accepts optional a <code>Date</code>
|
93
|
+
# or <code>Time</code> as :reference option
|
94
|
+
# </td>
|
95
|
+
# </tr>
|
96
|
+
# <tr>
|
97
|
+
# <td>:file</td><td>File</td>
|
98
|
+
# <td>
|
99
|
+
# convert to a file name; checks if the file exists; allows additional
|
100
|
+
# checks like :blockdev, :chardev, :grpowned, :owned, :readable,
|
101
|
+
# :readable_real, :setgid, :setuid, :size, :socket, :sticky, :symlink
|
102
|
+
# :world_readable, :world_writeable, :writable, :writable_real, :zero
|
103
|
+
# </td>
|
104
|
+
# </tr>
|
105
|
+
# <tr>
|
106
|
+
# <td>:directory</td><td>Dir</td>
|
107
|
+
# <td>
|
108
|
+
# convert to a directory name; checks if the directory exists; allows
|
109
|
+
# additional check like the :file conversion (above)
|
110
|
+
# </td>
|
111
|
+
# </tr>
|
112
|
+
# <tr>
|
113
|
+
# <td>:file_content</td><td></td>
|
114
|
+
# <td>
|
115
|
+
# expects a file name and returns the file content
|
116
|
+
# </td>
|
117
|
+
# </tr>
|
118
|
+
# </tbody></table>
|
119
|
+
#
|
120
|
+
module Conversion
|
121
|
+
class << self
|
122
|
+
#
|
123
|
+
# Get a conversion function.
|
124
|
+
#
|
125
|
+
# The requested +type+ specifies the type of returned conversion
|
126
|
+
# function:
|
127
|
+
#
|
128
|
+
# - Symbol: a defined function; see {.define}
|
129
|
+
# - Class: function associated to the given Class
|
130
|
+
# - Enumerable<String>: function to pass only given strings
|
131
|
+
# - Array(type): function returning converted elements of argument
|
132
|
+
# array
|
133
|
+
# - Regexp: function which passes matching arguments
|
134
|
+
#
|
135
|
+
# @param type [Symbol, Class, Enumerable<String>, Array(type), Regexp]
|
136
|
+
# @return [#call] conversion function
|
137
|
+
#
|
138
|
+
# @example type is a Symbol
|
139
|
+
# ParseArgv::Conversion[:integer]
|
140
|
+
# # => Proc which converts an argument into an Integer
|
141
|
+
# ParseArgv::Conversion[:integer].call('42')
|
142
|
+
# # => 42
|
143
|
+
#
|
144
|
+
# @example type is a Class
|
145
|
+
# ParseArgv::Conversion[Time]
|
146
|
+
# # => Proc which converts an argument to a Time object
|
147
|
+
# ParseArgv::Conversion[Time].call('2022-01-02 12:13 CET')
|
148
|
+
# # => "2022-01-02 12:13:00 +0100"
|
149
|
+
#
|
150
|
+
# @example type is a Array<String>
|
151
|
+
# ParseArgv::Conversion[%w[foo bar baz]]
|
152
|
+
# # => Proc which allows only an argument 'foo', 'bar', or 'baz'
|
153
|
+
# ParseArgv::Conversion[%w[foo bar baz]].call('bar')
|
154
|
+
# # => "bar"
|
155
|
+
#
|
156
|
+
# @example type is a Array(type)
|
157
|
+
# ParseArgv::Conversion[[:number]]
|
158
|
+
# # => Proc which converts an argument array to numbers
|
159
|
+
# ParseArgv::Conversion[[:number]].call('42, 21.84')
|
160
|
+
# # => [42, 21.84]
|
161
|
+
#
|
162
|
+
# @example type is a Regexp
|
163
|
+
# Conversion[/\Ate+st\z/]
|
164
|
+
# # => Proc which allows only an argument matching the Regexp
|
165
|
+
# Conversion[/\Ate+st\z/].call('teeeeeeest')
|
166
|
+
# # => "teeeeeeest"
|
167
|
+
#
|
168
|
+
def [](type)
|
169
|
+
return regexp_match(type) if type.is_a?(Regexp)
|
170
|
+
if type.is_a?(Array) && type.size == 1
|
171
|
+
return array_of(Conversion[type.first])
|
172
|
+
end
|
173
|
+
return enum_type(type) if type.is_a?(Enumerable)
|
174
|
+
(@ll[type] || @ll[type.to_sym]) or
|
175
|
+
raise(UnknownAttributeConverterError, type)
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# Define a conversion function or an alias between two conversion
|
180
|
+
# functions.
|
181
|
+
#
|
182
|
+
# @overload define(name, &block)
|
183
|
+
# Define the conversion function for specified +name+.
|
184
|
+
# @param name [Symbol] conversion function name
|
185
|
+
# @param block [Proc] conversion function
|
186
|
+
#
|
187
|
+
# @example define the type +:odd_number+
|
188
|
+
# ParseArgv::Conversion.define(:odd_number) do |arg, &err|
|
189
|
+
# result = ParseArgv::Conversion[:number].call(arg, &err)
|
190
|
+
# result.odd? ? result : err['argument must be an odd number']
|
191
|
+
# end
|
192
|
+
#
|
193
|
+
# @overload define(new_name, old_name)
|
194
|
+
# Creates an alias between two conversion functions.
|
195
|
+
# @param new_name [Symbol] new name for the handler
|
196
|
+
# @param old_name [Symbol] name of existing handler
|
197
|
+
#
|
198
|
+
# @example define the alias +:odd+ for the existing type +:odd_number+
|
199
|
+
# ParseArgv::Conversion.define(:odd, :odd_number)
|
200
|
+
#
|
201
|
+
# @return [Conversion] itself
|
202
|
+
#
|
203
|
+
def define(name, old_name = nil, &block)
|
204
|
+
@ll[name] = old_name.nil? ? block : self[old_name]
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def regexp_match(regexp)
|
211
|
+
proc do |arg, *args, &err|
|
212
|
+
if args.include?(:match)
|
213
|
+
match = regexp.match(arg) and next match
|
214
|
+
else
|
215
|
+
regexp.match?(arg) and next arg
|
216
|
+
end
|
217
|
+
err["argument must match #{regexp}"]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def enum_type(enum)
|
222
|
+
set = Set.new(enum) { |e| e.to_s.strip }
|
223
|
+
proc do |arg, &err|
|
224
|
+
next arg if set.include?(arg)
|
225
|
+
allowed = set.map { |s| "`#{s}`" }.join(', ')
|
226
|
+
err["argument must be one of [#{allowed}]"]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def array_of(type)
|
231
|
+
proc do |arg, *args, **opts, &err|
|
232
|
+
Conversion[:array]
|
233
|
+
.call(arg, &err)
|
234
|
+
.map! { |a| type.call(a, *args, **opts, &err) }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
@ll = {}
|
240
|
+
|
241
|
+
define(:integer) do |arg, type = nil, &err|
|
242
|
+
/\A-?\d+/.match?(arg) or err['argument must be an integer']
|
243
|
+
arg = arg.to_i
|
244
|
+
case type
|
245
|
+
when :positive
|
246
|
+
arg.positive? or err['argument must be a positive integer']
|
247
|
+
when :negative
|
248
|
+
arg.negative? or err['argument must be a negative integer']
|
249
|
+
when :nonzero
|
250
|
+
arg.nonzero? or err['argument must be a nonzero integer']
|
251
|
+
end
|
252
|
+
arg
|
253
|
+
end
|
254
|
+
define(Integer, :integer)
|
255
|
+
|
256
|
+
define(:float) do |arg, type = nil, &err|
|
257
|
+
/\A[+\-]?\d*\.?\d+(?:[Ee][+\-]?\d+)?/.match?(arg) or
|
258
|
+
err['argument must be a float number']
|
259
|
+
arg = arg.to_f
|
260
|
+
case type
|
261
|
+
when :positive
|
262
|
+
arg.positive? or err['argument must be a positive float number']
|
263
|
+
when :negative
|
264
|
+
arg.negative? or err['argument must be a negative float number']
|
265
|
+
when :nonzero
|
266
|
+
arg.nonzero? or err['argument must be a nonzero float number']
|
267
|
+
end
|
268
|
+
arg
|
269
|
+
end
|
270
|
+
define(Float, :float)
|
271
|
+
|
272
|
+
define(:number) do |arg, type = nil, &err|
|
273
|
+
/\A[\+\-]?\d*\.?\d+(?:[Ee][\+\-]?\d+)?/.match?(arg) or
|
274
|
+
err['argument must be a number']
|
275
|
+
arg = arg.to_f
|
276
|
+
argi = arg.to_i
|
277
|
+
arg = argi if argi == arg
|
278
|
+
case type
|
279
|
+
when :positive
|
280
|
+
arg.positive? or err['argument must be a positive number']
|
281
|
+
when :negative
|
282
|
+
arg.negative? or err['argument must be a negative number']
|
283
|
+
when :nonzero
|
284
|
+
arg.nonzero? or err['argument must be a nonzero number']
|
285
|
+
end
|
286
|
+
arg
|
287
|
+
end
|
288
|
+
define(Numeric, :number)
|
289
|
+
|
290
|
+
define(:byte) do |arg, base: 1024, &err|
|
291
|
+
match = /\A(\d*\.?\d+(?:[Ee][\+\-]?\d+)?)([kmgtpezyKMGTPEZY]?)/.match(arg)
|
292
|
+
match or err['argument must be a byte number']
|
293
|
+
(match[1].to_f * (base**' kmgtpezy'.index(match[2].downcase))).to_i
|
294
|
+
end
|
295
|
+
|
296
|
+
define(:string) do |arg, &err|
|
297
|
+
arg.empty? ? err['argument must be not empty'] : arg
|
298
|
+
end
|
299
|
+
define(String, :string)
|
300
|
+
|
301
|
+
define(:file_name) do |arg, rel: nil, &err|
|
302
|
+
File.expand_path(Conversion[:string].call(arg, &err), rel)
|
303
|
+
end
|
304
|
+
|
305
|
+
define(:regexp) do |arg, &err|
|
306
|
+
Regexp.new(
|
307
|
+
Conversion[:string].call(
|
308
|
+
arg.delete_prefix('/').delete_suffix('/'),
|
309
|
+
&err
|
310
|
+
)
|
311
|
+
)
|
312
|
+
rescue RegexpError => e
|
313
|
+
err["invalid regular expression; #{e}"]
|
314
|
+
end
|
315
|
+
define(Regexp, :regexp)
|
316
|
+
|
317
|
+
define(:array) do |arg, &err|
|
318
|
+
arg = arg[1..-2] if arg[0] == '[' && arg[-1] == ']'
|
319
|
+
arg = arg.split(',').map!(&:strip)
|
320
|
+
arg.uniq!
|
321
|
+
arg.delete('')
|
322
|
+
arg.empty? ? err['argument can not be empty'] : arg
|
323
|
+
end
|
324
|
+
define(Array, :array)
|
325
|
+
|
326
|
+
define(:date) do |arg, reference: nil, &err|
|
327
|
+
defined?(::Date) || require('date')
|
328
|
+
ret = Date._parse(arg)
|
329
|
+
err['argument must be a date'] if ret.empty?
|
330
|
+
reference ||= Date.today
|
331
|
+
Date.new(
|
332
|
+
ret[:year] || reference.year,
|
333
|
+
ret[:mon] || reference.mon,
|
334
|
+
ret[:mday] || reference.mday
|
335
|
+
)
|
336
|
+
rescue Date::Error
|
337
|
+
err['argument must be a date']
|
338
|
+
end
|
339
|
+
defined?(::Date) && define(Date, :date)
|
340
|
+
|
341
|
+
define(:time) do |arg, reference: nil, &err|
|
342
|
+
defined?(::Date) || require('date')
|
343
|
+
ret = Date._parse(arg)
|
344
|
+
err['argument must be a time'] if ret.empty?
|
345
|
+
reference ||= Date.today
|
346
|
+
Time.new(
|
347
|
+
ret[:year] || reference.year,
|
348
|
+
ret[:mon] || reference.month,
|
349
|
+
ret[:mday] || reference.mday,
|
350
|
+
ret[:hour] || 0,
|
351
|
+
ret[:min] || 0,
|
352
|
+
ret[:sec] || 0,
|
353
|
+
ret[:offset]
|
354
|
+
)
|
355
|
+
rescue Date::Error
|
356
|
+
err['argument must be a time']
|
357
|
+
end
|
358
|
+
define(Time, :time)
|
359
|
+
|
360
|
+
define(:file) do |arg, *args, **opts, &err|
|
361
|
+
fname = Conversion[:file_name].call(arg, **opts, &err)
|
362
|
+
stat = File.stat(fname)
|
363
|
+
stat.file? or err['argument must be a file']
|
364
|
+
args.each do |att|
|
365
|
+
name = "#{att}?"
|
366
|
+
stat.respond_to?(name) or next
|
367
|
+
stat.public_send(name) or err["file is not #{att}"]
|
368
|
+
end
|
369
|
+
fname
|
370
|
+
rescue Errno::ENOENT
|
371
|
+
err['file does not exist']
|
372
|
+
end
|
373
|
+
define(File, :file)
|
374
|
+
|
375
|
+
define(:file_content) do |arg, **opts, &err|
|
376
|
+
next $stdin.read if arg == '-'
|
377
|
+
fname = Conversion[:file].call(arg, :readable, **opts, &err)
|
378
|
+
File.read(fname)
|
379
|
+
rescue SystemCallError
|
380
|
+
err['file is not readable']
|
381
|
+
end
|
382
|
+
|
383
|
+
define(:directory) do |arg, *args, **opts, &err|
|
384
|
+
fname = Conversion[:file_name].call(arg, **opts, &err)
|
385
|
+
stat = File.stat(fname)
|
386
|
+
stat.directory? or err['argument must be a directory']
|
387
|
+
args.each do |att|
|
388
|
+
name = "#{att}?"
|
389
|
+
stat.respond_to?(name) or next
|
390
|
+
stat.public_send(name) or err["directory is not #{att}"]
|
391
|
+
end
|
392
|
+
fname
|
393
|
+
rescue Errno::ENOENT
|
394
|
+
err['directory does not exist']
|
395
|
+
end
|
396
|
+
define(Dir, :directory)
|
397
|
+
end
|
398
|
+
end
|