benry-cmdopt 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +34 -1
- data/MIT-LICENSE +21 -0
- data/README.md +570 -62
- data/Rakefile.rb +6 -87
- data/benry-cmdopt.gemspec +23 -21
- data/doc/benry-cmdopt.html +650 -0
- data/doc/css/style.css +160 -0
- data/lib/benry/cmdopt.rb +568 -417
- data/task/common-task.rb +138 -0
- data/task/package-task.rb +72 -0
- data/task/readme-task.rb +125 -0
- data/task/test-task.rb +81 -0
- data/test/cmdopt_test.rb +1372 -681
- metadata +22 -28
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
case
|
187
|
-
when
|
188
|
-
when
|
189
|
-
when
|
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
|
-
|
192
|
-
|
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
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
231
|
+
def _error(msg)
|
232
|
+
return SchemaError.new(msg)
|
233
|
+
end
|
252
234
|
|
253
|
-
|
254
|
-
|
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
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
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
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
#; [!
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
when
|
356
|
-
|
357
|
-
|
358
|
-
|
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
|
-
|
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
|
-
|
364
|
-
|
365
|
-
#; [!
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
-
|
381
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
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
|
-
|
410
|
-
|
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
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
val
|
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
|
-
|
607
|
+
raise "** internal error"
|
431
608
|
end
|
432
|
-
#; [!
|
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
|
-
|
438
|
-
|
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
|
-
|
461
|
-
|
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
|
-
|
481
|
-
|
482
|
-
|
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
|
-
|
489
|
-
SCHEMA_CLASS = Schema
|
490
|
-
PARSER_CLASS = Parser
|
634
|
+
end
|
491
635
|
|
492
636
|
|
493
|
-
|
494
|
-
|
637
|
+
OPTIONS_CLASS = Hash
|
638
|
+
SCHEMA_CLASS = Schema
|
639
|
+
PARSER_CLASS = Parser
|
495
640
|
|
496
641
|
|
497
|
-
|
498
|
-
|
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
|