parse-argv 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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&lt;String&gt;</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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ParseArgv
4
+ #
5
+ # current version
6
+ #
7
+ VERSION = '0.1.0'
8
+ end