benry-cmdopt 1.0.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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51be94488014a32dabfc2f524a850fa657a11c4c1d9d9b92761d00e57e24969e
4
+ data.tar.gz: d2078058fec806f194f0bfd1d5d6705a78430c7989d7eb9ddccadea2daa3311b
5
+ SHA512:
6
+ metadata.gz: 41e0e1a914cb6022f31244d6770067bd38636f27c0ce9e30dbcd37a731bc8d3f485329e0c0b49ff3aa322250f6d475ee0c28032a1053132e0f8c2bf061b2aa7c
7
+ data.tar.gz: e5e3922ccf2d06e9b56997cc07e7616dfa905fedb1974953f7bc9138d7b888cab6d5b5c8025000cd574b8b608ead71ef772571fa71486c5467294d20376bdb9e
@@ -0,0 +1,9 @@
1
+ =======
2
+ CHANGES
3
+ =======
4
+
5
+
6
+ Release 1.0.0 (2021-01-17)
7
+ --------------------------
8
+
9
+ * First public release
@@ -0,0 +1,163 @@
1
+ Benry::Cmdopt README
2
+ ====================
3
+
4
+ ($Release: 1.0.0 $)
5
+
6
+ Benry::Cmdopt is a command option parser library, like `optparse.rb`.
7
+
8
+ Compared to `optparse.rb`:
9
+
10
+ * Easy to use, easy to extend, easy to understand.
11
+ * Not add `-h` nor `--help` automatically.
12
+ * Not add `-v` nor `--version` automatically.
13
+ * Not regard `-x` as short cut of `--xxx`.
14
+ (`optparser.rb` regards `-x` as short cut of `--xxx` automatically.)
15
+ * Provides very simple feature to build custom help message.
16
+ * Separates command option schema class from parser class.
17
+
18
+ (Benry::Cmdopt requires Ruby >= 2.3)
19
+
20
+
21
+ Usage
22
+ =====
23
+
24
+
25
+ Define, parse, and print help
26
+ -----------------------------
27
+
28
+ ```ruby
29
+ require 'benry/cmdopt'
30
+
31
+ ## define
32
+ cmdopt = Benry::Cmdopt.new
33
+ cmdopt.add(:help , "-h, --help" , "print help message")
34
+ cmdopt.add(:version, " --version", "print version")
35
+
36
+ ## parse with error handling
37
+ options = cmdopt.parse(ARGV) do |err|
38
+ $stderr.puts "ERROR: #{err.message}"
39
+ exit(1)
40
+ end
41
+ p options # ex: {:help => true, :version => true}
42
+ p ARGV # options are removed from ARGV
43
+
44
+ ## help
45
+ if options[:help]
46
+ puts "Usage: foobar [<options>] [<args>...]"
47
+ puts ""
48
+ puts "Options:"
49
+ puts cmdopt.build_option_help()
50
+ ## or
51
+ #format = " %-20s : %s"
52
+ #cmdopt.each_option_help {|opt, help| puts format % [opt, help] }
53
+ end
54
+ ```
55
+
56
+
57
+ Command option parameter
58
+ ------------------------
59
+
60
+ ```ruby
61
+ ## required parameter
62
+ cmdopt.add(:file, "-f, --file=<FILE>", "filename")
63
+ cmdopt.add(:file, " --file=<FILE>", "filename")
64
+ cmdopt.add(:file, "-f <FILE>" , "filename")
65
+
66
+ ## optional parameter
67
+ cmdopt.add(:file, "-f, --file[=<FILE>]", "filename")
68
+ cmdopt.add(:file, " --file[=<FILE>]", "filename")
69
+ cmdopt.add(:file, "-f[<FILE>]" , "filename")
70
+ ```
71
+
72
+
73
+ Argument varidation
74
+ -------------------
75
+
76
+ ```ruby
77
+ ## type
78
+ cmdopt.add(:indent , "-i <N>", "indent width", type: Integer)
79
+ ## pattern
80
+ cmdopt.add(:indent , "-i <N>", "indent width", pattern: /\A\d+\z/)
81
+ ## enum
82
+ cmdopt.add(:indent , "-i <N>", "indent width", enum: [2, 4, 8])
83
+ ## callback
84
+ cmdopt.add(:indent , "-i <N>", "indent width") {|val|
85
+ val =~ /\A\d+\z/ or
86
+ raise "integer expected." # raise without exception class.
87
+ val.to_i # convert argument value.
88
+ }
89
+ ```
90
+
91
+
92
+ Available types
93
+ ---------------
94
+
95
+ * Integer (`/\A[-+]?\d+\z/`)
96
+ * Float (`/\A[-+]?(\d+\.\d*\|\.\d+)z/`)
97
+ * TrueClass (`/\A(true|on|yes|false|off|no)\z/`)
98
+ * Date (`/\A\d\d\d\d-\d\d?-\d\d?\z/`)
99
+
100
+
101
+ Multiple parameters
102
+ -------------------
103
+
104
+ ```ruby
105
+ cmdopt.add(:lib , "-I <NAME>", "library names") {|optdict, key, val|
106
+ arr = optdict[key] || []
107
+ arr << val
108
+ arr
109
+ }
110
+ ```
111
+
112
+
113
+ Not support
114
+ -----------
115
+
116
+ * default value
117
+ * `--no-xxx` style option
118
+
119
+
120
+ Internal classes
121
+ ================
122
+
123
+ * `Benry::Cmdopt::Schema` -- command option schema.
124
+ * `Benry::Cmdopt::Parser` -- command option parser.
125
+ * `Benry::Cmdopt::Facade` -- facade object including schema and parser.
126
+
127
+ ```ruby
128
+ require 'benry/cmdopt'
129
+
130
+ ## define schema
131
+ schema = Benry::Cmdopt::Schema.new
132
+ schema.add(:help , '-h, --help' , "show help message")
133
+ schema.add(:file , '-f, --file=<FILE>' , "filename")
134
+ schema.add(:indent, '-i, --indent[=<WIDTH>]', "enable indent", type: Integer)
135
+
136
+ ## parse options
137
+ parser = Benry::Cmdopt::Parser.new(schema)
138
+ argv = ['-hi2', '--file=blabla.txt', 'aaa', 'bbb']
139
+ opts = parser.parse(argv) do |err|
140
+ $stderr.puts "ERROR: #{err.message}"
141
+ exit 1
142
+ end
143
+ p opts #=> [:help=>true, :indent=>2, :file=>"blabla.txt"]
144
+ p argv #=> ["aaa", "bbb"]
145
+ ```
146
+
147
+ Notice that `Benry::Cmdopt.new()` returns facade object.
148
+
149
+ ```ruby
150
+ require 'benry/cmdopt'
151
+
152
+ cmdopt = Benry::Cmdopt.new() # new facade object
153
+ cmdopt.add(:help, '-h', "help message") # same as schema.add(...)
154
+ opts = cmdopt.parse(ARGV) # same as parser.parse(...)
155
+ ```
156
+
157
+
158
+ License and Copyright
159
+ =====================
160
+
161
+ $License: MIT License $
162
+
163
+ $Copyright: copyright(c) 2021 kuwata-lab.com all rights reserved $
@@ -0,0 +1,92 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ project = "benry-cmdopt"
5
+ release = ENV['RELEASE'] || "0.0.0"
6
+ copyright = "copyright(c) 2021 kuwata-lab.com all rights reserved"
7
+ license = "MIT License"
8
+
9
+ target_files = Dir[*%W[
10
+ README.md CHANGES.md MIT-LICENSE.txt Rakefile.rb
11
+ lib/**/*.rb
12
+ test/**/*_test.rb
13
+ #{project}.gemspec
14
+ ]]
15
+
16
+
17
+ require 'rake/clean'
18
+ CLEAN << "build"
19
+ CLOBBER << Dir.glob("#{project}-*.gem")
20
+
21
+
22
+ task :default => :help
23
+
24
+
25
+ desc "show help"
26
+ task :help do
27
+ puts "rake help # help"
28
+ puts "rake test # run test"
29
+ puts "rake package RELEASE=X.X.X # create gem file"
30
+ puts "rake publish RELEASE=X.X.X # upload gem file"
31
+ puts "rake clean # remove files"
32
+ end
33
+
34
+
35
+ desc "do test"
36
+ task :test do
37
+ sh "ruby", *Dir.glob("test/*.rb")
38
+ end
39
+
40
+
41
+ desc "create package"
42
+ task :package do
43
+ release != "0.0.0" or
44
+ raise "specify $RELEASE"
45
+ ## copy
46
+ dir = "build"
47
+ rm_rf dir if File.exist?(dir)
48
+ mkdir dir
49
+ target_files.each do |file|
50
+ dest = File.join(dir, File.dirname(file))
51
+ mkdir_p dest, :verbose=>false unless File.exist?(dest)
52
+ cp file, "#{dir}/#{file}"
53
+ end
54
+ ## edit
55
+ Dir.glob("#{dir}/**/*").each do |file|
56
+ next unless File.file?(file)
57
+ File.open(file, 'rb+') do |f|
58
+ s1 = f.read()
59
+ s2 = s1
60
+ s2 = s2.gsub(/\$Release[:].*?\$/, "$"+"Release: #{release} $")
61
+ s2 = s2.gsub(/\$Copyright[:].*?\$/, "$"+"Copyright: #{copyright} $")
62
+ s2 = s2.gsub(/\$License[:].*?\$/, "$"+"License: #{license} $")
63
+ #
64
+ if s1 != s2
65
+ f.rewind()
66
+ f.truncate(0)
67
+ f.write(s2)
68
+ end
69
+ end
70
+ end
71
+ ## build
72
+ chdir dir do
73
+ sh "gem build #{project}.gemspec"
74
+ end
75
+ mv "#{dir}/#{project}-#{release}.gem", "."
76
+ end
77
+
78
+
79
+ desc "upload gem file to rubygems.org"
80
+ task :publish do
81
+ release != "0.0.0" or
82
+ raise "specify $RELEASE"
83
+ #
84
+ gemfile = "#{project}-#{release}.gem"
85
+ print "** Are you sure to publish #{gemfile}? [y/N]: "
86
+ answer = $stdin.gets().strip()
87
+ if answer.downcase == "y"
88
+ sh "gem push #{gemfile}"
89
+ sh "git tag ruby-#{project}-#{release}"
90
+ sh "git push --tags"
91
+ end
92
+ end
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'benry-cmdopt'
5
+ spec.version = '$Release: 1.0.0 $'.split()[1]
6
+ spec.author = 'kwatch'
7
+ spec.email = 'kwatch@gmail.com'
8
+ spec.platform = Gem::Platform::RUBY
9
+ spec.homepage = 'https://github.com/kwatch/benry/tree/ruby/benry-cmdopt'
10
+ spec.summary = "Command option parser, like `optparse.rb`"
11
+ spec.description = <<-'END'
12
+ Command option parser, like `optparse.rb`.
13
+ END
14
+ spec.license = 'MIT'
15
+ spec.files = Dir[
16
+ 'README.md', 'CHANGES.md', 'MIT-LICENSE',
17
+ 'Rakefile.rb', 'benry-cmdopt.gemspec',
18
+ 'bin/*',
19
+ 'lib/**/*.rb',
20
+ 'test/**/*.rb',
21
+ ]
22
+ #spec.executables = ['benry-cmdopt']
23
+ spec.bindir = 'bin'
24
+ spec.require_path = 'lib'
25
+ spec.test_files = Dir['test/**/*_test.rb']
26
+ #spec.extra_rdoc_files = ['README.md', 'CHANGES.md']
27
+
28
+ spec.add_development_dependency 'minitest' , '~> 5.8'
29
+ spec.add_development_dependency 'minitest-ok' , '~> 0.3'
30
+ end
@@ -0,0 +1,504 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### $Release$
5
+ ### $Copyright$
6
+ ### $License$
7
+ ###
8
+
9
+ require 'date'
10
+ require 'set'
11
+
12
+
13
+ module Benry
14
+
15
+
16
+ ##
17
+ ## Command option parser.
18
+ ##
19
+ ## Usage:
20
+ ## ## define
21
+ ## cmdopt = Benry::Cmdopt.new
22
+ ## cmdopt.add(:help , "-h, --help" , "print help message")
23
+ ## cmdopt.add(:version, " --version", "print version")
24
+ ## ## parse
25
+ ## options = cmdopt.parse(ARGV) do |err|
26
+ ## $stderr.puts "ERROR: #{err.message}"
27
+ ## exit(1)
28
+ ## end
29
+ ## p options # ex: {:help => true, :version => true}
30
+ ## p ARGV # options are removed from ARGV
31
+ ## ## help
32
+ ## if options[:help]
33
+ ## puts "Usage: foobar [<options>] [<args>...]"
34
+ ## puts ""
35
+ ## puts "Options:"
36
+ ## puts cmdopt.build_option_help()
37
+ ## ## or
38
+ ## #format = " %-20s : %s"
39
+ ## #cmdopt.each_option_help {|opt, help| puts format % [opt, help] }
40
+ ## end
41
+ ##
42
+ ## Command option parameter:
43
+ ## ## required
44
+ ## cmdopt.add(:file, "-f, --file=<FILE>", "filename")
45
+ ## cmdopt.add(:file, " --file=<FILE>", "filename")
46
+ ## cmdopt.add(:file, "-f <FILE>" , "filename")
47
+ ## ## optional
48
+ ## cmdopt.add(:file, "-f, --file[=<FILE>]", "filename")
49
+ ## cmdopt.add(:file, " --file[=<FILE>]", "filename")
50
+ ## cmdopt.add(:file, "-f[<FILE>]" , "filename")
51
+ ##
52
+ ## Validation:
53
+ ## ## type
54
+ ## cmdopt.add(:indent , "-i <N>", "indent width", type: Integer)
55
+ ## ## pattern
56
+ ## cmdopt.add(:indent , "-i <N>", "indent width", pattern: /\A\d+\z/)
57
+ ## ## enum
58
+ ## cmdopt.add(:indent , "-i <N>", "indent width", enum: [2, 4, 8])
59
+ ## ## callback
60
+ ## cmdopt.add(:indent , "-i <N>", "indent width") {|val|
61
+ ## val =~ /\A\d+\z/ or
62
+ ## raise "integer expected." # raise without exception class.
63
+ ## val.to_i # convert argument value.
64
+ ## }
65
+ ##
66
+ ## Available types:
67
+ ## * Integer (`/\A[-+]?\d+\z/`)
68
+ ## * Float (`/\A[-+]?(\d+\.\d*\|\.\d+)z/`)
69
+ ## * TrueClass (`/\A(true|on|yes|false|off|no)\z/`)
70
+ ## * Date (`/\A\d\d\d\d-\d\d?-\d\d?\z/`)
71
+ ##
72
+ ## Multiple parameters:
73
+ ## cmdopt.add(:lib , "-I <NAME>", "library names") {|optdict, key, val|
74
+ ## arr = optdict[key] || []
75
+ ## arr << val
76
+ ## arr
77
+ ## }
78
+ ##
79
+ ## Not support:
80
+ ## * default value
81
+ ## * `--no-xxx` style option
82
+ ##
83
+ module Cmdopt
84
+
85
+
86
+ VERSION = '$Release: 1.0.0 $'.split()[1]
87
+
88
+
89
+ def self.new
90
+ #; [!7kkqv] creates Facade object.
91
+ return Facade.new
92
+ end
93
+
94
+
95
+ class Facade
96
+
97
+ def initialize
98
+ @schema = SCHEMA_CLASS.new
99
+ end
100
+
101
+ def add(key, optdef, help, type: nil, pattern: nil, enum: nil, &callback)
102
+ #; [!vmb3r] defines command option.
103
+ @schema.add(key, optdef, help, type: type, pattern: pattern, enum: enum, &callback)
104
+ #; [!tu4k3] returns self.
105
+ self
106
+ end
107
+
108
+ def build_option_help(width_or_format=nil, all: false)
109
+ #; [!dm4p8] returns option help message.
110
+ return @schema.build_option_help(width_or_format, all: all)
111
+ end
112
+
113
+ def each_option_help(&block)
114
+ #; [!bw9qx] yields each option definition string and help message.
115
+ @schema.each_option_help(&block)
116
+ self
117
+ end
118
+
119
+ def parse(argv, &error_handler)
120
+ #; [!7gc2m] parses command options.
121
+ #; [!no4xu] returns option values as dict.
122
+ #; [!areof] handles only OptionError when block given.
123
+ #; [!peuva] returns nil when OptionError handled.
124
+ parser = PARSER_CLASS.new(@schema)
125
+ return parser.parse(argv, &error_handler)
126
+ end
127
+
128
+ end
129
+
130
+
131
+ class Schema
132
+
133
+ def initialize()
134
+ @items = []
135
+ end
136
+
137
+ def add(key, optdef, help, type: nil, pattern: nil, enum: nil, &callback)
138
+ #; [!rhhji] raises SchemaError when key is not a Symbol.
139
+ key.nil? || key.is_a?(Symbol) or
140
+ raise error("add(#{key.inspect}): 1st arg should be a Symbol as an option key.")
141
+ #; [!vq6eq] raises SchemaError when help message is missing."
142
+ help.nil? || help.is_a?(String) or
143
+ raise error("add(#{key.inspect}, #{optdef.inspect}): help message required as 3rd argument.")
144
+ #; [!7hi2d] takes command option definition string.
145
+ short, long, param, optional = parse_optdef(optdef)
146
+ #; [!p9924] option key is omittable only when long option specified.
147
+ #; [!jtp7z] raises SchemaError when key is nil and no long option.
148
+ key || long or
149
+ raise error("add(#{key.inspect}, #{optdef.inspect}): long option required when option key (1st arg) not specified.")
150
+ key ||= long.gsub(/-/, '_').intern
151
+ #; [!7xmr5] raises SchemaError when type is not registered.
152
+ #; [!s2aaj] raises SchemaError when option has no params but type specified.
153
+ if type
154
+ PARAM_TYPES.key?(type) or
155
+ raise error("#{type.inspect}: unregistered type.")
156
+ param or
157
+ raise error("#{type.inspect}: type specified in spite of option has no params.")
158
+ end
159
+ #; [!bi2fh] raises SchemaError when pattern is not a regexp.
160
+ #; [!01fmt] raises SchmeaError when option has no params but pattern specified.
161
+ if pattern
162
+ pattern.is_a?(Regexp) or
163
+ raise error("#{pattern.inspect}: regexp expected.")
164
+ param or
165
+ raise error("#{pattern.inspect}: pattern specified in spite of option has no params.")
166
+ end
167
+ #; [!melyd] raises SchmeaError when enum is not a Array nor Set.
168
+ #; [!xqed8] raises SchemaError when enum specified for no param option.
169
+ if enum
170
+ enum.is_a?(Array) || enum.is_a?(Set) or
171
+ raise error("#{enum.inspect}: array or set expected.")
172
+ param or
173
+ raise error("#{enum.inspect}: enum specified in spite of option has no params.")
174
+ end
175
+ #; [!yht0v] keeps command option definitions.
176
+ item = SchemaItem.new(key, optdef, short, long, param, help,
177
+ optional: optional, type: type, pattern: pattern, enum: enum, &callback)
178
+ @items << item
179
+ item
180
+ end
181
+
182
+ def build_option_help(width_or_format=nil, all: false)
183
+ #; [!0aq0i] can take integer as width.
184
+ #; [!pcsah] can take format string.
185
+ #; [!dndpd] detects option width automatically when nothing specified.
186
+ case width_or_format
187
+ when nil ; format = _default_format()
188
+ when Integer; format = " %-#{width_or_format}s: %s"
189
+ when String ; format = width_or_format
190
+ else
191
+ raise ArgumentError.new("#{width_or_format.inspect}: width (integer) or format (string) expected.")
192
+ end
193
+ #; [!v7z4x] skips option help if help message is not specified.
194
+ #; [!to1th] includes all option help when `all` is true.
195
+ buf = []
196
+ width = nil
197
+ each_option_help do |opt, help|
198
+ #buf << format % [opt, help] << "\n" if help || all
199
+ if help
200
+ #; [!848rm] supports multi-lines help message.
201
+ n = 0
202
+ help.each_line do |line|
203
+ if (n += 1) == 1
204
+ buf << format % [opt, line.chomp] << "\n"
205
+ else
206
+ width ||= (format % ['', '']).length
207
+ buf << (' ' * width) << line.chomp << "\n"
208
+ end
209
+ end
210
+ elsif all
211
+ buf << format % [opt, ''] << "\n"
212
+ end
213
+ end
214
+ return buf.join()
215
+ end
216
+
217
+ def _default_format(min_width=20, max_width=35)
218
+ #; [!hr45y] detects preffered option width.
219
+ w = 0
220
+ each_option_help do |opt, help|
221
+ w = opt.length if w < opt.length
222
+ end
223
+ w = min_width if w < min_width
224
+ w = max_width if w > max_width
225
+ #; [!kkh9t] returns format string.
226
+ return " %-#{w}s : %s"
227
+ end
228
+ private :_default_format
229
+
230
+ def each_option_help(&block)
231
+ #; [!4b911] yields each optin definition str and help message.
232
+ @items.each do |item|
233
+ yield item.optdef, item.help
234
+ end
235
+ #; [!zbxyv] returns self.
236
+ self
237
+ end
238
+
239
+ def find_short_option(short)
240
+ #; [!b4js1] returns option definition matched to short name.
241
+ #; [!s4d1y] returns nil when nothing found.
242
+ return @items.find {|item| item.short == short }
243
+ end
244
+
245
+ def find_long_option(long)
246
+ #; [!atmf9] returns option definition matched to long name.
247
+ #; [!6haoo] returns nil when nothing found.
248
+ return @items.find {|item| item.long == long }
249
+ end
250
+
251
+ private
252
+
253
+ def error(msg)
254
+ return SchemaError.new(msg)
255
+ end
256
+
257
+ def parse_optdef(optdef)
258
+ #; [!qw0ac] parses command option definition string.
259
+ #; [!ae733] parses command option definition which has a required param.
260
+ #; [!4h05c] parses command option definition which has an optional param.
261
+ #; [!b7jo3] raises SchemaError when command option definition is invalid.
262
+ case optdef
263
+ when /\A[ \t]*-(\w),[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
264
+ short, long, param1, param2 = $1, $2, $3, $4
265
+ when /\A[ \t]*-(\w)(?:[ \t]+(\S+)|\[(\S+)\])?\z/
266
+ short, long, param1, param2 = $1, nil, $2, $3
267
+ when /\A[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
268
+ short, long, param1, param2 = nil, $1, $2, $3
269
+ when /(--\w[-\w])*[ \t]+(\S+)/
270
+ raise error("#{optdef}: invalid option definition (use '#{$1}=#{$2}' instead of '#{$1} #{$2}').")
271
+ else
272
+ raise error("#{optdef}: invalid option definition.")
273
+ end
274
+ return short, long, param1 || param2, !!param2
275
+ end
276
+
277
+ end
278
+
279
+
280
+ class SchemaItem # avoid Struct
281
+
282
+ def initialize(key, optdef, short, long, param, help, optional: nil, type: nil, pattern: nil, enum: nil, &callback)
283
+ @key = key
284
+ @optdef = optdef
285
+ @short = short
286
+ @long = long
287
+ @param = param
288
+ @help = help
289
+ @optional = optional
290
+ @type = type
291
+ @pattern = pattern
292
+ @enum = enum
293
+ @callback = callback
294
+ end
295
+
296
+ attr_reader :key, :optdef, :short, :long, :param, :help, :optional, :type, :pattern, :enum, :callback
297
+
298
+ def optional_param?
299
+ @optional
300
+ end
301
+
302
+ def validate_and_convert(val, optdict)
303
+ #; [!h0s0o] raises RuntimeError when value not matched to pattern.
304
+ if @pattern && val != true
305
+ val =~ @pattern or
306
+ raise "pattern unmatched."
307
+ end
308
+ #; [!j4fuz] calls type-specific callback when type specified.
309
+ if @type && val != true
310
+ proc_ = PARAM_TYPES[@type]
311
+ val = proc_.call(val)
312
+ end
313
+ #; [!5jrdf] raises RuntimeError when value not in enum.
314
+ if @enum && val != true
315
+ @enum.include?(val) or
316
+ raise "expected one of #{@enum.join('/')}."
317
+ end
318
+ #; [!jn9z3] calls callback when callback specified.
319
+ #; [!iqalh] calls callback with different number of args according to arity.
320
+ if @callback
321
+ n_args = @callback.arity
322
+ val = n_args == 1 ? @callback.call(val) \
323
+ : @callback.call(optdict, @key, val)
324
+ end
325
+ #; [!x066l] returns new value.
326
+ return val
327
+ end
328
+
329
+ end
330
+
331
+
332
+ PARAM_TYPES = {
333
+ String => proc {|val|
334
+ val
335
+ },
336
+ Integer => proc {|val|
337
+ #; [!6t8cs] converts value into integer.
338
+ #; [!nzwc9] raises error when failed to convert value into integer.
339
+ val =~ /\A[-+]?\d+\z/ or
340
+ raise "integer expected."
341
+ val.to_i
342
+ },
343
+ Float => proc {|val|
344
+ #; [!gggy6] converts value into float.
345
+ #; [!t4elj] raises error when faield to convert value into float.
346
+ val =~ /\A[-+]?(\d+\.\d*|\.\d+)\z/ or
347
+ raise "float expected."
348
+ val.to_f
349
+ },
350
+ TrueClass => proc {|val|
351
+ #; [!47kx4] converts 'true'/'on'/'yes' into true.
352
+ #; [!3n810] converts 'false'/'off'/'no' into false.
353
+ #; [!h8ayh] raises error when failed to convert value into true nor false.
354
+ case val
355
+ when /\A(?:true|on|yes)\z/i
356
+ true
357
+ when /\A(?:false|off|no)\z/i
358
+ false
359
+ else
360
+ raise "boolean expected."
361
+ end
362
+ },
363
+ Date => proc {|val|
364
+ #; [!sru5j] converts 'YYYY-MM-DD' into date object.
365
+ #; [!h9q9y] raises error when failed to convert into date object.
366
+ #; [!i4ui8] raises error when specified date not exist.
367
+ val =~ /\A(\d\d\d\d)-(\d\d?)-(\d\d?)\z/ or
368
+ raise "invalid date format (ex: '2000-01-01')"
369
+ begin
370
+ Date.new($1.to_i, $2.to_i, $3.to_i)
371
+ rescue ArgumentError => ex
372
+ raise "date not exist."
373
+ end
374
+ },
375
+ }
376
+
377
+
378
+ class Parser
379
+
380
+ def initialize(schema)
381
+ @schema = schema
382
+ end
383
+
384
+ def parse(argv, &error_handler)
385
+ optdict = new_options_dict()
386
+ while !argv.empty? && argv[0] =~ /\A-/
387
+ optstr = argv.shift
388
+ #; [!y04um] skips rest options when '--' found in argv.
389
+ if optstr == '--'
390
+ break
391
+ elsif optstr =~ /\A--/
392
+ #; [!uh7j8] parses long options.
393
+ parse_long_option(optstr, optdict, argv)
394
+ else
395
+ #; [!nwnjc] parses short options.
396
+ parse_short_options(optstr, optdict, argv)
397
+ end
398
+ end
399
+ #; [!3wmsy] returns command option values as a dict.
400
+ return optdict
401
+ rescue OptionError => ex
402
+ #; [!qpuxh] handles only OptionError when block given.
403
+ raise unless block_given?()
404
+ yield ex
405
+ #; [!dhpw1] returns nil when OptionError handled.
406
+ nil
407
+ end
408
+
409
+ def error(msg)
410
+ return OptionError.new(msg)
411
+ end
412
+
413
+ protected
414
+
415
+ def parse_long_option(optstr, optdict, _argv)
416
+ #; [!3i994] raises OptionError when invalid long option format.
417
+ optstr =~ /\A--(\w[-\w]*)(?:=(.*))?\z/ or
418
+ raise error("#{optstr}: invalid long option.")
419
+ name = $1; val = $2
420
+ #; [!er7h4] raises OptionError when unknown long option.
421
+ item = @schema.find_long_option(name) or
422
+ raise error("#{optstr}: unknown long option.")
423
+ #; [!2jd9w] raises OptionError when no arguments specified for arg required long option.
424
+ #; [!qyq8n] raises optionError when an argument specified for no arg long option.
425
+ if item.optional_param?
426
+ # do nothing
427
+ elsif item.param
428
+ val or raise error("#{optstr}: argument required.")
429
+ else
430
+ val.nil? or raise error("#{optstr}: unexpected argument.")
431
+ end
432
+ #; [!o596x] validates argument value.
433
+ val ||= true
434
+ begin
435
+ val = item.validate_and_convert(val, optdict)
436
+ rescue RuntimeError => ex
437
+ raise error("#{optstr}: #{ex.message}")
438
+ end
439
+ optdict[item.key] = val
440
+ end
441
+
442
+ def parse_short_options(optstr, optdict, argv)
443
+ n = optstr.length
444
+ i = 0
445
+ while (i += 1) < n
446
+ char = optstr[i]
447
+ #; [!4eh49] raises OptionError when unknown short option specified.
448
+ item = @schema.find_short_option(char) or
449
+ raise error("-#{char}: unknown option.")
450
+ #
451
+ if !item.param
452
+ val = true
453
+ elsif !item.optional_param?
454
+ #; [!utdbf] raises OptionError when argument required but not specified.
455
+ #; [!f63hf] short option arg can be specified without space separator.
456
+ val = i+1 < n ? optstr[(i+1)..-1] : argv.shift or
457
+ raise error("-#{char}: argument required.")
458
+ i = n
459
+ else
460
+ #; [!yjq6b] optional arg should be specified without space separator.
461
+ #; [!wape4] otpional arg can be omit.
462
+ val = i+1 < n ? optstr[(i+1)..-1] : true
463
+ i = n
464
+ end
465
+ #; [!yu0kc] validates short option argument.
466
+ begin
467
+ val = item.validate_and_convert(val, optdict)
468
+ rescue RuntimeError => ex
469
+ if val == true
470
+ raise error("-#{char}: #{ex.message}")
471
+ else
472
+ s = item.optional_param? ? '' : ' '
473
+ raise error("-#{char}#{s}#{val}: #{ex.message}")
474
+ end
475
+ end
476
+ optdict[item.key] = val
477
+ end
478
+ end
479
+
480
+ def new_options_dict()
481
+ #; [!vm6h0] returns new hash object.
482
+ return OPTIONS_CLASS.new
483
+ end
484
+
485
+ end
486
+
487
+
488
+ OPTIONS_CLASS = Hash
489
+ SCHEMA_CLASS = Schema
490
+ PARSER_CLASS = Parser
491
+
492
+
493
+ class SchemaError < StandardError
494
+ end
495
+
496
+
497
+ class OptionError < StandardError
498
+ end
499
+
500
+
501
+ end
502
+
503
+
504
+ end