benry-cmdopt 1.0.0 → 2.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.
data/lib/benry/cmdopt.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  ###
4
- ### $Release$
5
- ### $Copyright$
6
- ### $License$
5
+ ### $Release: 2.0.0 $
6
+ ### $Copyright: copyright(c) 2021 kwatch@gmail.com $
7
+ ### $License: MIT License $
7
8
  ###
8
9
 
9
10
  require 'date'
@@ -11,494 +12,644 @@ require 'set'
11
12
 
12
13
 
13
14
  module Benry
15
+ end
14
16
 
15
17
 
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
18
+ ##
19
+ ## Command option parser.
20
+ ##
21
+ ## See: https://github.com/kwatch/benry-ruby/tree/main/benry-cmdopt
22
+ ##
23
+ module Benry::CmdOpt
100
24
 
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
25
 
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
26
+ VERSION = '$Release: 2.0.0 $'.split()[1]
112
27
 
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
28
 
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
29
+ def self.new()
30
+ #; [!7kkqv] creates Facade object.
31
+ return Facade.new
32
+ end
33
+
34
+
35
+ class Facade
127
36
 
37
+ def initialize()
38
+ @schema = SCHEMA_CLASS.new
128
39
  end
129
40
 
41
+ attr_reader :schema
130
42
 
131
- class Schema
43
+ def add(key, optdef, desc, *rest, type: nil, rexp: nil, pattern: nil, enum: nil, range: nil, value: nil, detail: nil, tag: nil, &callback)
44
+ rexp ||= pattern # for backward compatibility
45
+ #; [!vmb3r] defines command option.
46
+ #; [!71cvg] type, rexp, enum, and range are can be passed as positional args as well as keyword args.
47
+ @schema.add(key, optdef, desc, *rest, type: type, rexp: rexp, enum: enum, range: range, value: value, detail: detail, tag: tag, &callback)
48
+ #; [!tu4k3] returns self.
49
+ self
50
+ end
132
51
 
133
- def initialize()
134
- @items = []
135
- end
52
+ def option_help(width_or_format=nil, all: false)
53
+ #; [!dm4p8] returns option help message.
54
+ return @schema.option_help(width_or_format, all: all)
55
+ end
136
56
 
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
57
+ #; [!s61vo] '#to_s' is an alias to '#option_help()'.
58
+ alias to_s option_help
59
+
60
+ def each_option_and_desc(all: false, &block)
61
+ #; [!wght5] returns enumerator object if block not given.
62
+ return @schema.each_option_and_desc(all: all) unless block_given?()
63
+ #; [!bw9qx] yields each option definition string and help message.
64
+ #; [!kunfw] yields all items (including hidden items) if `all: true` specified.
65
+ @schema.each_option_and_desc(all: all, &block)
66
+ self
67
+ end
68
+ alias each_option_help each_option_and_desc # for backward compatibility
69
+
70
+ def parse(argv, all: true, &error_handler)
71
+ #; [!7gc2m] parses command options.
72
+ #; [!no4xu] returns option values as dict.
73
+ #; [!areof] handles only OptionError when block given.
74
+ #; [!peuva] returns nil when OptionError handled.
75
+ #; [!za9at] parses options only before args when `all: false`.
76
+ parser = PARSER_CLASS.new(@schema)
77
+ return parser.parse(argv, all: all, &error_handler)
78
+ end
79
+
80
+ end
81
+
82
+
83
+ class Schema
84
+
85
+ def initialize()
86
+ @items = []
87
+ end
88
+
89
+ def dup()
90
+ #; [!lxb0o] copies self object.
91
+ other = self.class.new
92
+ other.instance_variable_set(:@items, @items.dup)
93
+ return other
94
+ end
95
+
96
+ def copy_from(other, except: [])
97
+ #; [!6six3] copy schema items from others.
98
+ #; [!vt88s] copy schema items except items specified by 'except:' kwarg.
99
+ except = [except].flatten()
100
+ other.each do |item|
101
+ @items << item unless except.include?(item.key)
180
102
  end
103
+ self
104
+ end
181
105
 
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
106
+ def add(key, optdef, desc, *rest, type: nil, rexp: nil, pattern: nil, enum: nil, range: nil, value: nil, detail: nil, tag: nil, &callback)
107
+ rexp ||= pattern # for backward compatibility
108
+ #; [!kuhf9] type, rexp, enum, and range are can be passed as positional args as well as keyword args.
109
+ rest.each do |x|
110
+ case x
111
+ when Class ; type ||= x
112
+ when Regexp ; rexp ||= x
113
+ when Array, Set ; enum ||= x
114
+ when Range ; range ||= x
190
115
  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
116
+ #; [!e3emy] raises error when positional arg is not one of class, regexp, array, nor range.
117
+ raise _error("#{x.inspect}: Expected one of class, regexp, array or range, but got #{x.class.name}.")
213
118
  end
214
- return buf.join()
215
119
  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"
120
+ #; [!rhhji] raises SchemaError when key is not a Symbol.
121
+ key.nil? || key.is_a?(Symbol) or
122
+ raise _error("add(#{key.inspect}, #{optdef.inspect}): The first arg should be a Symbol as an option key.")
123
+ #; [!vq6eq] raises SchemaError when help message is missing."
124
+ desc.nil? || desc.is_a?(String) or
125
+ raise _error("add(#{key.inspect}, #{optdef.inspect}): Help message required as 3rd argument.")
126
+ #; [!7hi2d] takes command option definition string.
127
+ short, long, param, required = parse_optdef(optdef)
128
+ #; [!p9924] option key is omittable only when long option specified.
129
+ #; [!jtp7z] raises SchemaError when key is nil and no long option.
130
+ key || long or
131
+ raise _error("add(#{key.inspect}, #{optdef.inspect}): Long option required when option key (1st arg) not specified.")
132
+ #; [!rpl98] when long option is 'foo-bar' then key name is ':foo_bar'.
133
+ key ||= long.gsub(/-/, '_').intern
134
+ #; [!97sn0] raises SchemaError when ',' is missing between short and long options.
135
+ if long.nil? && param =~ /\A--/
136
+ raise _error("add(#{key.inspect}, #{optdef.inspect}): Missing ',' between short option and long options.")
227
137
  end
228
- private :_default_format
138
+ #; [!yht0v] keeps command option definitions.
139
+ item = SchemaItem.new(key, optdef, desc, short, long, param, required,
140
+ type: type, rexp: rexp, enum: enum, range: range, value: value, detail: detail, tag: tag, &callback)
141
+ @items << item
142
+ item
143
+ end
229
144
 
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
145
+ def option_help(width_or_format=nil, all: false)
146
+ #; [!0aq0i] can take integer as width.
147
+ #; [!pcsah] can take format string.
148
+ #; [!dndpd] detects option width automatically when nothing specified.
149
+ case width_or_format
150
+ when nil ; format = _default_format()
151
+ when Integer; format = " %-#{width_or_format}s : %s"
152
+ when String ; format = width_or_format
153
+ else
154
+ raise ArgumentError.new("#{width_or_format.inspect}: Width (integer) or format (string) expected.")
155
+ end
156
+ #; [!v7z4x] skips option help if help message is not specified.
157
+ #; [!to1th] includes all option help when `all` is true.
158
+ #; [!a4qe4] option should not be hidden if description is empty string.
159
+ sb = []
160
+ width = nil; indent = nil
161
+ each_option_and_desc(all: all) do |opt, desc, detail|
162
+ sb << format % [opt, desc || ""] << "\n"
163
+ #; [!848rm] supports multi-lines help message.
164
+ if detail
165
+ width ||= (format % ['', '']).length
166
+ indent ||= ' ' * width
167
+ sb << detail.gsub(/^/, indent)
168
+ sb << "\n" unless detail.end_with?("\n")
234
169
  end
235
- #; [!zbxyv] returns self.
236
- self
237
170
  end
171
+ return sb.join()
172
+ end
238
173
 
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 }
174
+ #; [!rrapd] '#to_s' is an alias to '#option_help()'.
175
+ alias to_s option_help
176
+
177
+ def each_option_and_desc(all: false, &block)
178
+ #; [!03sux] returns enumerator object if block not given.
179
+ return to_enum(:each_option_and_desc, all: all) unless block_given?()
180
+ #; [!4b911] yields each optin definition str and help message.
181
+ @items.each do |item|
182
+ #; [!cl8zy] when 'all' flag is false, not yield hidden items.
183
+ #; [!tc4bk] when 'all' flag is true, yields even hidden items.
184
+ yield item.optdef, item.desc, item.detail if all || ! item.hidden?
243
185
  end
186
+ #; [!zbxyv] returns self.
187
+ self
188
+ end
189
+ alias each_option_help each_option_and_desc # for backward compatibility
244
190
 
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
191
+ def each(&block) # :nodoc:
192
+ #; [!y4k1c] yields each option item.
193
+ @items.each(&block)
194
+ end
195
+
196
+ def empty?(all: true)
197
+ #; [!um8am] returns false if any item exists, else returns true.
198
+ #; [!icvm1] ignores hidden items if 'all: false' kwarg specified.
199
+ @items.each {|item| return false if all || ! item.hidden? }
200
+ return true
201
+ end
202
+
203
+ def get(key)
204
+ #; [!3wjfp] finds option item object by key.
205
+ #; [!0spll] returns nil if key not found.
206
+ return @items.find {|item| item.key == key }
207
+ end
208
+
209
+ def delete(key)
210
+ #; [!l86rb] deletes option item corresponding to key.
211
+ #; [!rq0aa] returns deleted item.
212
+ item = get(key)
213
+ @items.delete_if {|item| item.key == key }
214
+ return item
215
+ end
216
+
217
+ def find_short_option(short)
218
+ #; [!b4js1] returns option definition matched to short name.
219
+ #; [!s4d1y] returns nil when nothing found.
220
+ return @items.find {|item| item.short == short }
221
+ end
222
+
223
+ def find_long_option(long)
224
+ #; [!atmf9] returns option definition matched to long name.
225
+ #; [!6haoo] returns nil when nothing found.
226
+ return @items.find {|item| item.long == long }
227
+ end
228
+
229
+ private
250
230
 
251
- private
231
+ def _error(msg)
232
+ return SchemaError.new(msg)
233
+ end
252
234
 
253
- def error(msg)
254
- return SchemaError.new(msg)
235
+ def parse_optdef(optdef)
236
+ #; [!qw0ac] parses command option definition string.
237
+ #; [!ae733] parses command option definition which has a required param.
238
+ #; [!4h05c] parses command option definition which has an optional param.
239
+ #; [!b7jo3] raises SchemaError when command option definition is invalid.
240
+ case optdef
241
+ when /\A[ \t]*-(\w),[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
242
+ short, long, param1, param2 = $1, $2, $3, $4
243
+ when /\A[ \t]*-(\w)(?:[ \t]+(\S+)|\[(\S+)\])?\z/
244
+ short, long, param1, param2 = $1, nil, $2, $3
245
+ when /\A[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
246
+ short, long, param1, param2 = nil, $1, $2, $3
247
+ when /(--\w[-\w])*[ \t]+(\S+)/
248
+ raise _error("#{optdef}: Invalid option definition (use '#{$1}=#{$2}' instead of '#{$1} #{$2}').")
249
+ else
250
+ raise _error("#{optdef}: Invalid option definition.")
255
251
  end
252
+ required = param1 ? true : param2 ? false : nil
253
+ return short, long, (param1 || param2), required
254
+ end
256
255
 
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
256
+ def _default_format(min_width=nil, max_width=35)
257
+ #; [!bmr7d] changes min_with according to options.
258
+ min_width ||= _preferred_option_width()
259
+ #; [!hr45y] detects preffered option width.
260
+ w = 0
261
+ each_option_help do |opt, _|
262
+ w = opt.length if w < opt.length
275
263
  end
264
+ w = min_width if w < min_width
265
+ w = max_width if w > max_width
266
+ #; [!kkh9t] returns format string.
267
+ return " %-#{w}s : %s"
268
+ end
276
269
 
270
+ def _preferred_option_width()
271
+ #; [!kl91t] shorten option help min width when only single options which take no arg.
272
+ #; [!0koqb] widen option help min width when any option takes an arg.
273
+ #; [!kl91t] widen option help min width when long option exists.
274
+ long_p = @items.any? {|x| x.desc && x.long && x.param }
275
+ short_p = @items.all? {|x| x.desc && !x.long && !x.param }
276
+ return short_p ? 8 : long_p ? 20 : 14
277
277
  end
278
278
 
279
+ end
279
280
 
280
- class SchemaItem # avoid Struct
281
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
282
+ class SchemaItem # avoid Struct
283
+
284
+ def initialize(key, optdef, desc, short, long, param, required, type: nil, rexp: nil, pattern: nil, enum: nil, range: nil, detail: nil, value: nil, tag: nil, &callback)
285
+ rexp ||= pattern # for backward compatibility
286
+ _init_validation(param, required, type, rexp, enum, range, value)
287
+ @key = key unless nil == key
288
+ @optdef = optdef unless nil == optdef
289
+ @desc = desc unless nil == desc
290
+ @short = short unless nil == short
291
+ @long = long unless nil == long
292
+ @param = param unless nil == param
293
+ @required = required unless nil == required
294
+ @type = type unless nil == type
295
+ @rexp = rexp unless nil == rexp
296
+ @enum = enum unless nil == enum
297
+ @range = range unless nil == range
298
+ @detail = detail unless nil == detail
299
+ @value = value unless nil == value
300
+ @tag = tag unless nil == tag
301
+ @callback = callback unless nil == callback
302
+ #; [!nn4cp] freezes enum object.
303
+ @enum.freeze() if @enum
304
+ end
295
305
 
296
- attr_reader :key, :optdef, :short, :long, :param, :help, :optional, :type, :pattern, :enum, :callback
306
+ attr_reader :key, :optdef, :desc, :short, :long, :param, :type, :rexp, :enum, :range, :detail, :value, :tag, :callback
307
+ alias pattern rexp # for backward compatibility
308
+ alias help desc # for backward compatibility
297
309
 
298
- def optional_param?
299
- @optional
300
- end
310
+ def required?()
311
+ #; [!svxny] returns nil if option takes no arguments.
312
+ #; [!uwbgc] returns false if argument is optional.
313
+ #; [!togcx] returns true if argument is required.
314
+ return ! @param ? nil : !! @required
315
+ end
301
316
 
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
317
+ def arg_requireness()
318
+ #; [!kmo28] returns :none if option takes no arguments.
319
+ #; [!owpba] returns :optional if argument is optional.
320
+ #; [!s8gxl] returns :required if argument is required.
321
+ return :none if ! @param
322
+ return :required if @required
323
+ return :optional
324
+ end
325
+
326
+ def hidden?()
327
+ #; [!h0uxs] returns true if desc is nil.
328
+ #; [!su00g] returns true if key starts with '_'.
329
+ #; [!28vzx] returns false if else.
330
+ return @desc == nil || @key.to_s.start_with?('_')
331
+ end
328
332
 
333
+ def validate_and_convert(val, optdict)
334
+ #; [!h0s0o] raises RuntimeError when value not matched to pattern.
335
+ if @rexp && val != true
336
+ val =~ @rexp or
337
+ raise "Pattern unmatched."
338
+ end
339
+ #; [!j4fuz] calls type-specific callback when type specified.
340
+ if @type && val != true
341
+ proc_ = PARAM_TYPES[@type]
342
+ val = proc_.call(val)
343
+ end
344
+ #; [!5jrdf] raises RuntimeError when value not in enum.
345
+ if @enum && val != true
346
+ @enum.include?(val) or
347
+ raise "Expected one of #{@enum.join('/')}."
348
+ end
349
+ #; [!5falp] raise RuntimeError when value not in range.
350
+ #; [!a0rej] supports endless range.
351
+ if @range && val != true
352
+ r = @range
353
+ r.begin == nil || r.begin <= val or (
354
+ raise "Positive value (>= 0) expected." if r.begin == 0
355
+ raise "Positive value (>= 1) expected." if r.begin == 1
356
+ raise "Too small (min: #{r.begin.inspect})"
357
+ )
358
+ r.end == nil || val <= r.end or
359
+ raise "Too large (max: #{r.end.inspect})"
360
+ end
361
+ #; [!jn9z3] calls callback when callback specified.
362
+ #; [!iqalh] calls callback with different number of args according to arity.
363
+ if @callback
364
+ n_args = @callback.arity
365
+ val = n_args == 1 ? @callback.call(val) \
366
+ : @callback.call(optdict, @key, val)
367
+ end
368
+ #; [!eafem] returns default value (if specified) instead of true value.
369
+ return @value if val == true && @value != nil
370
+ #; [!x066l] returns new value.
371
+ return val
329
372
  end
330
373
 
374
+ private
375
+
376
+ def _error(msg)
377
+ return SchemaError.new(msg)
378
+ end
331
379
 
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
380
+ def _init_validation(param, required, type, rexp, enum, range, value)
381
+ #; [!wy2iv] when 'type:' specified...
382
+ if type
383
+ #; [!7xmr5] raises SchemaError when type is not registered.
384
+ PARAM_TYPES.key?(type) or
385
+ raise _error("#{type.inspect}: Unregistered type.")
386
+ #; [!s2aaj] raises SchemaError when option has no params but type specified.
387
+ #; [!sz8x2] not raise error when no params but value specified.
388
+ #; [!70ogf] not raise error when no params but TrueClass specified.
389
+ param || value != nil || type == TrueClass or
390
+ raise _error("#{type.inspect}: Type specified in spite of option has no params.")
391
+ end
392
+ #; [!6y8s2] when 'rexp:' specified...
393
+ if rexp
394
+ #; [!bi2fh] raises SchemaError when pattern is not a regexp.
395
+ rexp.is_a?(Regexp) or
396
+ raise _error("#{rexp.inspect}: Regexp pattern expected.")
397
+ #; [!01fmt] raises SchmeaError when option has no params but pattern specified.
398
+ param or
399
+ raise _error("#{rexp.inspect}: Regexp pattern specified in spite of option has no params.")
400
+ end
401
+ #; [!5nrvq] when 'enum:' specified...
402
+ if enum
403
+ #; [!melyd] raises SchemaError when enum is not an Array nor Set.
404
+ enum.is_a?(Array) || enum.is_a?(Set) or
405
+ raise _error("#{enum.inspect}: Array or set expected.")
406
+ #; [!xqed8] raises SchemaError when enum specified for no param option.
407
+ param or
408
+ raise _error("#{enum.inspect}: Enum specified in spite of option has no params.")
409
+ #; [!zuthh] raises SchemaError when enum element value is not instance of type class.
410
+ enum.each do |x|
411
+ x.is_a?(type) or
412
+ raise _error("#{enum.inspect}: Enum element value should be instance of #{type.name}, but #{x.inspect} is not.")
413
+ end if type
414
+ end
415
+ #; [!hk4nw] when 'range:' specified...
416
+ if range
417
+ #; [!z20ky] raises SchemaError when range is not a Range object.
418
+ range.is_a?(Range) or
419
+ raise _error("#{range.inspect}: Range object expected.")
420
+ #; [!gp025] raises SchemaError when range specified with `type: TrueClass`.
421
+ if type == TrueClass
422
+ raise _error("#{range.inspect}: Range is not available with `type: TrueClass`.")
423
+ #; [!7njd5] range beginning/end value should be expected type.
359
424
  else
360
- raise "boolean expected."
425
+ #; [!uymig] range object can be endless.
426
+ type_ = type || String
427
+ ok1 = range.begin == nil || range.begin.is_a?(type_)
428
+ ok2 = range.end == nil || range.end.is_a?(type_)
429
+ ok1 && ok2 or
430
+ raise _error("#{range.inspect}: Range value should be #{type_.name}, but not.")
361
431
  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."
432
+ end
433
+ #; [!a0g52] when 'value:' specified...
434
+ if value != nil
435
+ #; [!435t6] raises SchemaError when 'value:' is specified on argument-required option.
436
+ ! required or
437
+ raise _error("#{value.inspect}: 'value:' is meaningless when option has required argument (hint: change to optional argument instead).")
438
+ if type == TrueClass
439
+ #; [!6vwqv] raises SchemaError when type is TrueClass but value is not true nor false.
440
+ value == true || value == false or
441
+ raise _error("#{value.inspect}: Value should be true or false when `type: TrueClass` specified.")
442
+ elsif type
443
+ #; [!c6i2o] raises SchemaError when value is not a kind of type.
444
+ value.is_a?(type) or
445
+ raise _error("Type mismatched between `type: #{type.name}` and `value: #{value.inspect}`.")
446
+ else
447
+ #; [!lnhp6] not raise error when type is not specified.
448
+ end
449
+ if enum
450
+ #; [!6xb8o] value should be included in enum values.
451
+ enum.include?(value) or
452
+ raise _error("#{value}: Value should be included in enum values, but not.")
373
453
  end
374
- },
375
- }
454
+ end
455
+ end
376
456
 
457
+ end
377
458
 
378
- class Parser
379
459
 
380
- def initialize(schema)
381
- @schema = schema
460
+ PARAM_TYPES = {
461
+ String => proc {|val|
462
+ val
463
+ },
464
+ Integer => proc {|val|
465
+ #; [!6t8cs] converts value into integer.
466
+ #; [!nzwc9] raises error when failed to convert value into integer.
467
+ val =~ /\A[-+]?\d+\z/ or
468
+ raise "Integer expected."
469
+ val.to_i
470
+ },
471
+ Float => proc {|val|
472
+ #; [!gggy6] converts value into float.
473
+ #; [!t4elj] raises error when faield to convert value into float.
474
+ val =~ /\A[-+]?(\d+\.\d*|\.\d+)\z/ or
475
+ raise "Float expected."
476
+ val.to_f
477
+ },
478
+ TrueClass => proc {|val|
479
+ #; [!47kx4] converts 'true'/'on'/'yes' into true.
480
+ #; [!3n810] converts 'false'/'off'/'no' into false.
481
+ #; [!h8ayh] raises error when failed to convert value into true nor false.
482
+ case val
483
+ when /\A(?:true|on|yes)\z/i ; true
484
+ when /\A(?:false|off|no)\z/i ; false
485
+ else
486
+ raise "Boolean expected."
487
+ end
488
+ },
489
+ Date => proc {|val|
490
+ #; [!sru5j] converts 'YYYY-MM-DD' into date object.
491
+ #; [!h9q9y] raises error when failed to convert into date object.
492
+ #; [!i4ui8] raises error when specified date not exist.
493
+ val =~ /\A(\d\d\d\d)-(\d\d?)-(\d\d?)\z/ or
494
+ raise "Invalid date format (ex: '2000-01-01')"
495
+ begin
496
+ Date.new($1.to_i, $2.to_i, $3.to_i)
497
+ rescue ArgumentError => ex
498
+ raise "Date not exist."
382
499
  end
500
+ },
501
+ }
383
502
 
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
503
+
504
+ class Parser
505
+
506
+ def initialize(schema)
507
+ @schema = schema
508
+ end
509
+
510
+ def parse(argv, all: true, &error_handler)
511
+ optdict = new_options_dict()
512
+ index = 0
513
+ while index < argv.length
514
+ #; [!5s5b6] treats '-' as an argument, not an option.
515
+ if argv[index] =~ /\A-/ && argv[index] != "-"
516
+ optstr = argv.delete_at(index)
517
+ #; [!q8356] parses options even after arguments when `all: true`.
518
+ elsif all
519
+ index += 1
520
+ next
521
+ #; [!ryra3] doesn't parse options after arguments when `all: false`.
522
+ else
523
+ break
524
+ end
525
+ #; [!y04um] skips rest options when '--' found in argv.
526
+ if optstr == '--'
527
+ break
528
+ elsif optstr =~ /\A--/
529
+ #; [!uh7j8] parses long options.
530
+ parse_long_option(optstr, optdict)
531
+ else
532
+ #; [!nwnjc] parses short options.
533
+ parse_short_options(optstr, optdict) { argv.delete_at(index) }
398
534
  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
535
  end
536
+ #; [!3wmsy] returns command option values as a dict.
537
+ return optdict
538
+ rescue OptionError => ex
539
+ #; [!qpuxh] handles only OptionError when block given.
540
+ raise unless block_given?()
541
+ yield ex
542
+ #; [!dhpw1] returns nil when OptionError handled.
543
+ nil
544
+ end
545
+
546
+ def _error(msg)
547
+ return OptionError.new(msg)
548
+ end
408
549
 
409
- def error(msg)
410
- return OptionError.new(msg)
550
+ protected
551
+
552
+ def parse_long_option(optstr, optdict)
553
+ #; [!3i994] raises OptionError when invalid long option format.
554
+ optstr =~ /\A--(\w[-\w]*)(?:=(.*))?\z/ or
555
+ raise _error("#{optstr}: Invalid long option.")
556
+ name = $1; val = $2
557
+ #; [!1ab42] invokes error handler method when unknown long option.
558
+ #; [!er7h4] default behavior is to raise OptionError when unknown long option.
559
+ item = @schema.find_long_option(name) or
560
+ return handle_unknown_long_option(optstr, name, val)
561
+ #; [!2jd9w] raises OptionError when no arguments specified for arg required long option.
562
+ #; [!qyq8n] raises optionError when an argument specified for no arg long option.
563
+ case item.arg_requireness()
564
+ when :none # no arguments
565
+ val == nil or raise _error("#{optstr}: Unexpected argument.")
566
+ when :required # argument required
567
+ val or raise _error("#{optstr}: Argument required.")
568
+ when :optional # optonal argument
569
+ # do nothing
570
+ else
571
+ raise "** internal error"
411
572
  end
573
+ #; [!o596x] validates argument value.
574
+ val ||= true
575
+ begin
576
+ val = item.validate_and_convert(val, optdict)
577
+ rescue RuntimeError => ex
578
+ raise _error("#{optstr}: #{ex.message}")
579
+ end
580
+ optdict[item.key] = val
581
+ end
412
582
 
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.")
583
+ def parse_short_options(optstr, optdict, &block)
584
+ n = optstr.length
585
+ i = 0
586
+ while (i += 1) < n
587
+ char = optstr[i]
588
+ #; [!4eh49] raises OptionError when unknown short option specified.
589
+ item = @schema.find_short_option(char) or
590
+ raise _error("-#{char}: Unknown option.")
591
+ #
592
+ case item.arg_requireness()
593
+ when :none # no arguments
594
+ val = true
595
+ when :required # argument required
596
+ #; [!utdbf] raises OptionError when argument required but not specified.
597
+ #; [!f63hf] short option arg can be specified without space separator.
598
+ val = i+1 < n ? optstr[(i+1)..-1] : yield or
599
+ raise _error("-#{char}: Argument required.")
600
+ i = n
601
+ when :optional # optonal argument
602
+ #; [!yjq6b] optional arg should be specified without space separator.
603
+ #; [!wape4] otpional arg can be omit.
604
+ val = i+1 < n ? optstr[(i+1)..-1] : true
605
+ i = n
429
606
  else
430
- val.nil? or raise error("#{optstr}: unexpected argument.")
607
+ raise "** internal error"
431
608
  end
432
- #; [!o596x] validates argument value.
433
- val ||= true
609
+ #; [!yu0kc] validates short option argument.
434
610
  begin
435
611
  val = item.validate_and_convert(val, optdict)
436
612
  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
613
+ if val == true
614
+ raise _error("-#{char}: #{ex.message}")
459
615
  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
616
+ sp = item.required? ? ' ' : ''
617
+ raise _error("-#{char}#{sp}#{val}: #{ex.message}")
464
618
  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
619
  end
620
+ optdict[item.key] = val
478
621
  end
622
+ end
479
623
 
480
- def new_options_dict()
481
- #; [!vm6h0] returns new hash object.
482
- return OPTIONS_CLASS.new
483
- end
484
-
624
+ def new_options_dict()
625
+ #; [!vm6h0] returns new hash object.
626
+ return OPTIONS_CLASS.new
485
627
  end
486
628
 
629
+ def handle_unknown_long_option(optstr, name, val)
630
+ #; [!0q78a] raises OptionError.
631
+ raise _error("#{optstr}: Unknown long option.")
632
+ end
487
633
 
488
- OPTIONS_CLASS = Hash
489
- SCHEMA_CLASS = Schema
490
- PARSER_CLASS = Parser
634
+ end
491
635
 
492
636
 
493
- class SchemaError < StandardError
494
- end
637
+ OPTIONS_CLASS = Hash
638
+ SCHEMA_CLASS = Schema
639
+ PARSER_CLASS = Parser
495
640
 
496
641
 
497
- class OptionError < StandardError
498
- end
642
+ class SchemaError < StandardError
643
+ end
499
644
 
500
645
 
646
+ class OptionError < StandardError
501
647
  end
502
648
 
503
649
 
504
650
  end
651
+
652
+
653
+ module Benry
654
+ Cmdopt = CmdOpt # for backawrd compatibility
655
+ end