benry-cmdopt 1.1.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,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.0 $'.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