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.
@@ -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