benry-cmdopt 1.1.0 → 2.0.1

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.1 $
6
+ ### $Copyright: copyright(c) 2021 kwatch@gmail.com $
7
+ ### $License: MIT License $
7
8
  ###
8
9
 
9
10
  require 'date'
@@ -11,516 +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.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 name") {|optdict, key, val|
74
- ## arr = optdict[key] || []
75
- ## arr << val
76
- ## arr
77
- ## }
78
- ##
79
- ## Hidden option:
80
- ## ### if help string is nil, that option is removed from help message.
81
- ## require 'benry/cmdopt'
82
- ## cmdopt = Benry::Cmdopt.new
83
- ## cmdopt.add(:verbose, '-v, --verbose', "verbose mode")
84
- ## cmdopt.add(:debug , '-d[<LEVEL>]' , nil, type: Integer) # hidden
85
- ## puts cmdopt.option_help()
86
- ## ### output ('-d' doesn't appear because help string is nil)
87
- ## # -v, --verbose : verbose mode
88
- ##
89
- ## Not supported:
90
- ## * default value
91
- ## * `--no-xxx` style option
92
- ## * bash/zsh completion
93
- ##
94
- module Cmdopt
95
-
96
-
97
- VERSION = '$Release: 1.1.0 $'.split()[1]
98
-
99
-
100
- def self.new
101
- #; [!7kkqv] creates Facade object.
102
- return Facade.new
103
- end
104
-
105
-
106
- class Facade
107
-
108
- def initialize
109
- @schema = SCHEMA_CLASS.new
110
- end
18
+ ##
19
+ ## Command option parser.
20
+ ##
21
+ ## See: https://github.com/kwatch/benry-ruby/tree/main/benry-cmdopt
22
+ ##
23
+ module Benry::CmdOpt
111
24
 
112
- def add(key, optdef, help, type: nil, pattern: nil, enum: nil, &callback)
113
- #; [!vmb3r] defines command option.
114
- @schema.add(key, optdef, help, type: type, pattern: pattern, enum: enum, &callback)
115
- #; [!tu4k3] returns self.
116
- self
117
- end
118
25
 
119
- def option_help(width_or_format=nil, all: false)
120
- #; [!dm4p8] returns option help message.
121
- return @schema.option_help(width_or_format, all: all)
122
- end
26
+ VERSION = '$Release: 2.0.1 $'.split()[1]
123
27
 
124
- def each_option_help(&block)
125
- #; [!bw9qx] yields each option definition string and help message.
126
- @schema.each_option_help(&block)
127
- self
128
- end
129
28
 
130
- def parse(argv, &error_handler)
131
- #; [!7gc2m] parses command options.
132
- #; [!no4xu] returns option values as dict.
133
- #; [!areof] handles only OptionError when block given.
134
- #; [!peuva] returns nil when OptionError handled.
135
- parser = PARSER_CLASS.new(@schema)
136
- return parser.parse(argv, &error_handler)
137
- end
29
+ def self.new()
30
+ #; [!7kkqv] creates Facade object.
31
+ return Facade.new
32
+ end
33
+
34
+
35
+ class Facade
138
36
 
37
+ def initialize()
38
+ @schema = SCHEMA_CLASS.new
139
39
  end
140
40
 
41
+ attr_reader :schema
141
42
 
142
- 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
143
51
 
144
- def initialize()
145
- @items = []
146
- 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
147
56
 
148
- def add(key, optdef, help, type: nil, pattern: nil, enum: nil, &callback)
149
- #; [!rhhji] raises SchemaError when key is not a Symbol.
150
- key.nil? || key.is_a?(Symbol) or
151
- raise error("add(#{key.inspect}): 1st arg should be a Symbol as an option key.")
152
- #; [!vq6eq] raises SchemaError when help message is missing."
153
- help.nil? || help.is_a?(String) or
154
- raise error("add(#{key.inspect}, #{optdef.inspect}): help message required as 3rd argument.")
155
- #; [!7hi2d] takes command option definition string.
156
- short, long, param, optional = parse_optdef(optdef)
157
- #; [!p9924] option key is omittable only when long option specified.
158
- #; [!jtp7z] raises SchemaError when key is nil and no long option.
159
- key || long or
160
- raise error("add(#{key.inspect}, #{optdef.inspect}): long option required when option key (1st arg) not specified.")
161
- key ||= long.gsub(/-/, '_').intern
162
- #; [!97sn0] raises SchemaError when ',' is missing between short and long options.
163
- if long.nil? && param =~ /\A--/
164
- raise error("add(#{key.inspect}, #{optdef.inspect}): missing ',' between short option and long options.")
165
- end
166
- #; [!7xmr5] raises SchemaError when type is not registered.
167
- #; [!s2aaj] raises SchemaError when option has no params but type specified.
168
- if type
169
- PARAM_TYPES.key?(type) or
170
- raise error("#{type.inspect}: unregistered type.")
171
- param or
172
- raise error("#{type.inspect}: type specified in spite of option has no params.")
173
- end
174
- #; [!bi2fh] raises SchemaError when pattern is not a regexp.
175
- #; [!01fmt] raises SchmeaError when option has no params but pattern specified.
176
- if pattern
177
- pattern.is_a?(Regexp) or
178
- raise error("#{pattern.inspect}: regexp expected.")
179
- param or
180
- raise error("#{pattern.inspect}: pattern specified in spite of option has no params.")
181
- end
182
- #; [!melyd] raises SchmeaError when enum is not a Array nor Set.
183
- #; [!xqed8] raises SchemaError when enum specified for no param option.
184
- if enum
185
- enum.is_a?(Array) || enum.is_a?(Set) or
186
- raise error("#{enum.inspect}: array or set expected.")
187
- param or
188
- raise error("#{enum.inspect}: enum specified in spite of option has no params.")
189
- end
190
- #; [!yht0v] keeps command option definitions.
191
- item = SchemaItem.new(key, optdef, short, long, param, help,
192
- optional: optional, type: type, pattern: pattern, enum: enum, &callback)
193
- @items << item
194
- 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)
195
102
  end
103
+ self
104
+ end
196
105
 
197
- def option_help(width_or_format=nil, all: false)
198
- #; [!0aq0i] can take integer as width.
199
- #; [!pcsah] can take format string.
200
- #; [!dndpd] detects option width automatically when nothing specified.
201
- case width_or_format
202
- when nil ; format = _default_format()
203
- when Integer; format = " %-#{width_or_format}s : %s"
204
- 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
205
115
  else
206
- raise ArgumentError.new("#{width_or_format.inspect}: width (integer) or format (string) expected.")
207
- end
208
- #; [!v7z4x] skips option help if help message is not specified.
209
- #; [!to1th] includes all option help when `all` is true.
210
- buf = []
211
- width = nil
212
- each_option_help do |opt, help|
213
- #buf << format % [opt, help] << "\n" if help || all
214
- if help
215
- #; [!848rm] supports multi-lines help message.
216
- n = 0
217
- help.each_line do |line|
218
- if (n += 1) == 1
219
- buf << format % [opt, line.chomp] << "\n"
220
- else
221
- width ||= (format % ['', '']).length
222
- buf << (' ' * width) << line.chomp << "\n"
223
- end
224
- end
225
- elsif all
226
- buf << format % [opt, ''] << "\n"
227
- 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}.")
228
118
  end
229
- return buf.join()
230
119
  end
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.")
137
+ end
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
231
144
 
232
- def each_option_help(&block)
233
- #; [!4b911] yields each optin definition str and help message.
234
- @items.each do |item|
235
- 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")
236
169
  end
237
- #; [!zbxyv] returns self.
238
- self
239
170
  end
171
+ return sb.join()
172
+ end
240
173
 
241
- def find_short_option(short)
242
- #; [!b4js1] returns option definition matched to short name.
243
- #; [!s4d1y] returns nil when nothing found.
244
- 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?
245
185
  end
186
+ #; [!zbxyv] returns self.
187
+ self
188
+ end
189
+ alias each_option_help each_option_and_desc # for backward compatibility
246
190
 
247
- def find_long_option(long)
248
- #; [!atmf9] returns option definition matched to long name.
249
- #; [!6haoo] returns nil when nothing found.
250
- return @items.find {|item| item.long == long }
251
- end
191
+ def each(&block) # :nodoc:
192
+ #; [!y4k1c] yields each option item.
193
+ @items.each(&block)
194
+ end
252
195
 
253
- private
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
254
202
 
255
- def error(msg)
256
- return SchemaError.new(msg)
257
- end
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
258
208
 
259
- def parse_optdef(optdef)
260
- #; [!qw0ac] parses command option definition string.
261
- #; [!ae733] parses command option definition which has a required param.
262
- #; [!4h05c] parses command option definition which has an optional param.
263
- #; [!b7jo3] raises SchemaError when command option definition is invalid.
264
- case optdef
265
- when /\A[ \t]*-(\w),[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
266
- short, long, param1, param2 = $1, $2, $3, $4
267
- when /\A[ \t]*-(\w)(?:[ \t]+(\S+)|\[(\S+)\])?\z/
268
- short, long, param1, param2 = $1, nil, $2, $3
269
- when /\A[ \t]*--(\w[-\w]*)(?:=(\S*?)|\[=(\S*?)\])?\z/
270
- short, long, param1, param2 = nil, $1, $2, $3
271
- when /(--\w[-\w])*[ \t]+(\S+)/
272
- raise error("#{optdef}: invalid option definition (use '#{$1}=#{$2}' instead of '#{$1} #{$2}').")
273
- else
274
- raise error("#{optdef}: invalid option definition.")
275
- end
276
- return short, long, param1 || param2, !!param2
277
- end
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
278
216
 
279
- def _default_format(min_width=nil, max_width=35)
280
- #; [!bmr7d] changes min_with according to options.
281
- min_width ||= _preferred_option_width()
282
- #; [!hr45y] detects preffered option width.
283
- w = 0
284
- each_option_help do |opt, help|
285
- w = opt.length if w < opt.length
286
- end
287
- w = min_width if w < min_width
288
- w = max_width if w > max_width
289
- #; [!kkh9t] returns format string.
290
- return " %-#{w}s : %s"
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
230
+
231
+ def _error(msg)
232
+ return SchemaError.new(msg)
233
+ end
234
+
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.")
291
251
  end
252
+ required = param1 ? true : param2 ? false : nil
253
+ return short, long, (param1 || param2), required
254
+ end
292
255
 
293
- def _preferred_option_width()
294
- #; [!kl91t] shorten option help min width when only single options which take no arg.
295
- #; [!0koqb] widen option help min width when any option takes an arg.
296
- #; [!kl91t] widen option help min width when long option exists.
297
- long_p = @items.any? {|x| x.help && x.long && x.param }
298
- short_p = @items.all? {|x| x.help && !x.long && !x.param }
299
- return short_p ? 8 : long_p ? 20 : 14
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
300
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
301
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
302
277
  end
303
278
 
279
+ end
304
280
 
305
- class SchemaItem # avoid Struct
306
281
 
307
- def initialize(key, optdef, short, long, param, help, optional: nil, type: nil, pattern: nil, enum: nil, &callback)
308
- @key = key unless key.nil?
309
- @optdef = optdef unless optdef.nil?
310
- @short = short unless short.nil?
311
- @long = long unless long.nil?
312
- @param = param unless param.nil?
313
- @help = help unless help.nil?
314
- @optional = optional unless optional.nil?
315
- @type = type unless type.nil?
316
- @pattern = pattern unless pattern.nil?
317
- @enum = enum unless enum.nil?
318
- @callback = callback unless callback.nil?
319
- 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
320
305
 
321
- attr_reader :key, :optdef, :short, :long, :param, :help, :optional, :type, :pattern, :enum, :callback
322
- alias optional_param? optional
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
323
309
 
324
- def validate_and_convert(val, optdict)
325
- #; [!h0s0o] raises RuntimeError when value not matched to pattern.
326
- if @pattern && val != true
327
- val =~ @pattern or
328
- raise "pattern unmatched."
329
- end
330
- #; [!j4fuz] calls type-specific callback when type specified.
331
- if @type && val != true
332
- proc_ = PARAM_TYPES[@type]
333
- val = proc_.call(val)
334
- end
335
- #; [!5jrdf] raises RuntimeError when value not in enum.
336
- if @enum && val != true
337
- @enum.include?(val) or
338
- raise "expected one of #{@enum.join('/')}."
339
- end
340
- #; [!jn9z3] calls callback when callback specified.
341
- #; [!iqalh] calls callback with different number of args according to arity.
342
- if @callback
343
- n_args = @callback.arity
344
- val = n_args == 1 ? @callback.call(val) \
345
- : @callback.call(optdict, @key, val)
346
- end
347
- #; [!x066l] returns new value.
348
- return val
349
- 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
350
316
 
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
351
324
  end
352
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
353
332
 
354
- PARAM_TYPES = {
355
- String => proc {|val|
356
- val
357
- },
358
- Integer => proc {|val|
359
- #; [!6t8cs] converts value into integer.
360
- #; [!nzwc9] raises error when failed to convert value into integer.
361
- val =~ /\A[-+]?\d+\z/ or
362
- raise "integer expected."
363
- val.to_i
364
- },
365
- Float => proc {|val|
366
- #; [!gggy6] converts value into float.
367
- #; [!t4elj] raises error when faield to convert value into float.
368
- val =~ /\A[-+]?(\d+\.\d*|\.\d+)\z/ or
369
- raise "float expected."
370
- val.to_f
371
- },
372
- TrueClass => proc {|val|
373
- #; [!47kx4] converts 'true'/'on'/'yes' into true.
374
- #; [!3n810] converts 'false'/'off'/'no' into false.
375
- #; [!h8ayh] raises error when failed to convert value into true nor false.
376
- case val
377
- when /\A(?:true|on|yes)\z/i
378
- true
379
- when /\A(?:false|off|no)\z/i
380
- false
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
372
+ end
373
+
374
+ private
375
+
376
+ def _error(msg)
377
+ return SchemaError.new(msg)
378
+ end
379
+
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.
381
424
  else
382
- 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.")
383
431
  end
384
- },
385
- Date => proc {|val|
386
- #; [!sru5j] converts 'YYYY-MM-DD' into date object.
387
- #; [!h9q9y] raises error when failed to convert into date object.
388
- #; [!i4ui8] raises error when specified date not exist.
389
- val =~ /\A(\d\d\d\d)-(\d\d?)-(\d\d?)\z/ or
390
- raise "invalid date format (ex: '2000-01-01')"
391
- begin
392
- Date.new($1.to_i, $2.to_i, $3.to_i)
393
- rescue ArgumentError => ex
394
- 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.")
395
453
  end
396
- },
397
- }
454
+ end
455
+ end
398
456
 
457
+ end
399
458
 
400
- class Parser
401
459
 
402
- def initialize(schema)
403
- @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."
404
499
  end
500
+ },
501
+ }
405
502
 
406
- def parse(argv, &error_handler)
407
- optdict = new_options_dict()
408
- while !argv.empty? && argv[0] =~ /\A-/
409
- optstr = argv.shift
410
- #; [!y04um] skips rest options when '--' found in argv.
411
- if optstr == '--'
412
- break
413
- elsif optstr =~ /\A--/
414
- #; [!uh7j8] parses long options.
415
- parse_long_option(optstr, optdict, argv)
416
- else
417
- #; [!nwnjc] parses short options.
418
- parse_short_options(optstr, optdict, argv)
419
- 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) }
420
534
  end
421
- #; [!3wmsy] returns command option values as a dict.
422
- return optdict
423
- rescue OptionError => ex
424
- #; [!qpuxh] handles only OptionError when block given.
425
- raise unless block_given?()
426
- yield ex
427
- #; [!dhpw1] returns nil when OptionError handled.
428
- nil
429
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
430
549
 
431
- def error(msg)
432
- 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"
433
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
434
582
 
435
- protected
436
-
437
- def parse_long_option(optstr, optdict, _argv)
438
- #; [!3i994] raises OptionError when invalid long option format.
439
- optstr =~ /\A--(\w[-\w]*)(?:=(.*))?\z/ or
440
- raise error("#{optstr}: invalid long option.")
441
- name = $1; val = $2
442
- #; [!er7h4] raises OptionError when unknown long option.
443
- item = @schema.find_long_option(name) or
444
- raise error("#{optstr}: unknown long option.")
445
- #; [!2jd9w] raises OptionError when no arguments specified for arg required long option.
446
- #; [!qyq8n] raises optionError when an argument specified for no arg long option.
447
- if item.optional_param?
448
- # do nothing
449
- elsif item.param
450
- 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
451
606
  else
452
- val.nil? or raise error("#{optstr}: unexpected argument.")
607
+ raise "** internal error"
453
608
  end
454
- #; [!o596x] validates argument value.
455
- val ||= true
609
+ #; [!yu0kc] validates short option argument.
456
610
  begin
457
611
  val = item.validate_and_convert(val, optdict)
458
612
  rescue RuntimeError => ex
459
- raise error("#{optstr}: #{ex.message}")
460
- end
461
- optdict[item.key] = val
462
- end
463
-
464
- def parse_short_options(optstr, optdict, argv)
465
- n = optstr.length
466
- i = 0
467
- while (i += 1) < n
468
- char = optstr[i]
469
- #; [!4eh49] raises OptionError when unknown short option specified.
470
- item = @schema.find_short_option(char) or
471
- raise error("-#{char}: unknown option.")
472
- #
473
- if !item.param
474
- val = true
475
- elsif !item.optional_param?
476
- #; [!utdbf] raises OptionError when argument required but not specified.
477
- #; [!f63hf] short option arg can be specified without space separator.
478
- val = i+1 < n ? optstr[(i+1)..-1] : argv.shift or
479
- raise error("-#{char}: argument required.")
480
- i = n
613
+ if val == true
614
+ raise _error("-#{char}: #{ex.message}")
481
615
  else
482
- #; [!yjq6b] optional arg should be specified without space separator.
483
- #; [!wape4] otpional arg can be omit.
484
- val = i+1 < n ? optstr[(i+1)..-1] : true
485
- i = n
616
+ sp = item.required? ? ' ' : ''
617
+ raise _error("-#{char}#{sp}#{val}: #{ex.message}")
486
618
  end
487
- #; [!yu0kc] validates short option argument.
488
- begin
489
- val = item.validate_and_convert(val, optdict)
490
- rescue RuntimeError => ex
491
- if val == true
492
- raise error("-#{char}: #{ex.message}")
493
- else
494
- s = item.optional_param? ? '' : ' '
495
- raise error("-#{char}#{s}#{val}: #{ex.message}")
496
- end
497
- end
498
- optdict[item.key] = val
499
619
  end
620
+ optdict[item.key] = val
500
621
  end
622
+ end
501
623
 
502
- def new_options_dict()
503
- #; [!vm6h0] returns new hash object.
504
- return OPTIONS_CLASS.new
505
- end
506
-
624
+ def new_options_dict()
625
+ #; [!vm6h0] returns new hash object.
626
+ return OPTIONS_CLASS.new
507
627
  end
508
628
 
629
+ def handle_unknown_long_option(optstr, name, val)
630
+ #; [!0q78a] raises OptionError.
631
+ raise _error("#{optstr}: Unknown long option.")
632
+ end
509
633
 
510
- OPTIONS_CLASS = Hash
511
- SCHEMA_CLASS = Schema
512
- PARSER_CLASS = Parser
634
+ end
513
635
 
514
636
 
515
- class SchemaError < StandardError
516
- end
637
+ OPTIONS_CLASS = Hash
638
+ SCHEMA_CLASS = Schema
639
+ PARSER_CLASS = Parser
517
640
 
518
641
 
519
- class OptionError < StandardError
520
- end
642
+ class SchemaError < StandardError
643
+ end
521
644
 
522
645
 
646
+ class OptionError < StandardError
523
647
  end
524
648
 
525
649
 
526
650
  end
651
+
652
+
653
+ module Benry
654
+ Cmdopt = CmdOpt # for backawrd compatibility
655
+ end