benry-cmdopt 1.0.0 → 2.0.0

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