benry-cmdopt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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