parse-argv 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/.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
|