benry-cmdapp 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +6 -0
- data/README.md +1693 -852
- data/benry-cmdapp.gemspec +3 -3
- data/doc/benry-cmdapp.html +1582 -906
- data/lib/benry/cmdapp.rb +1894 -1060
- data/test/app_test.rb +882 -1078
- data/test/config_test.rb +71 -0
- data/test/context_test.rb +382 -0
- data/test/func_test.rb +302 -82
- data/test/help_test.rb +1054 -553
- data/test/metadata_test.rb +191 -0
- data/test/misc_test.rb +175 -0
- data/test/registry_test.rb +402 -0
- data/test/run_all.rb +4 -3
- data/test/scope_test.rb +1210 -0
- data/test/shared.rb +112 -49
- data/test/util_test.rb +154 -99
- metadata +17 -9
- data/test/action_test.rb +0 -1038
- data/test/index_test.rb +0 -185
data/lib/benry/cmdapp.rb
CHANGED
@@ -2,1376 +2,2210 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
###
|
5
|
-
### $Release: 0.
|
5
|
+
### $Release: 1.0.0 $
|
6
6
|
### $Copyright: copyright(c) 2023 kwatch@gmail.com $
|
7
7
|
### $License: MIT License $
|
8
8
|
###
|
9
9
|
|
10
|
-
|
11
10
|
require 'benry/cmdopt'
|
12
11
|
|
13
12
|
|
14
13
|
module Benry::CmdApp
|
15
14
|
|
16
15
|
|
17
|
-
|
18
|
-
|
16
|
+
$VERBOSE_MODE = nil # true when global option '-v, --verbose' specified
|
17
|
+
$QUIET_MODE = nil # true when global option '-q, --quiet' specified
|
18
|
+
$COLOR_MODE = nil # true when global option '--color' specified
|
19
|
+
$DEBUG_MODE = nil # true when global option '--debug' specified
|
20
|
+
#$TRACE_MODE = nil # use `@config.trace_mode?` instead.
|
21
|
+
$DRYRUN_MODE = nil # true when global option '-X, --dryrun' specified
|
22
|
+
|
23
|
+
|
24
|
+
class BaseError < StandardError
|
25
|
+
def should_report_backtrace?()
|
26
|
+
#; [!oj9x3] returns true in base exception class to report backtrace.
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class DefinitionError < BaseError
|
32
|
+
end
|
19
33
|
|
34
|
+
class ExecutionError < BaseError
|
35
|
+
end
|
20
36
|
|
21
|
-
class
|
37
|
+
class ActionError < ExecutionError
|
38
|
+
end
|
22
39
|
|
23
|
-
class
|
24
|
-
|
25
|
-
|
26
|
-
|
40
|
+
class OptionError < ExecutionError
|
41
|
+
def should_report_backtrace?()
|
42
|
+
#; [!6qvnc] returns false in OptionError class because no need to report backtrace.
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
end
|
27
46
|
|
28
|
-
class
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
47
|
+
class CommandError < ExecutionError
|
48
|
+
def should_report_backtrace?()
|
49
|
+
#; [!o9xu2] returns false in ComamndError class because no need to report backtrace.
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
end
|
33
53
|
|
34
54
|
|
35
55
|
module Util
|
36
56
|
module_function
|
37
57
|
|
38
|
-
def
|
39
|
-
#; [!
|
40
|
-
#; [!
|
41
|
-
|
42
|
-
#; [!
|
43
|
-
|
44
|
-
|
45
|
-
|
58
|
+
def method2action(meth)
|
59
|
+
#; [!bt77a] converts method name (Symbol) to action name (String).
|
60
|
+
#; [!o5822] converts `:foo_` into `'foo'`.
|
61
|
+
#; [!msgjc] converts `:aa__bb____cc` into `'aa:bb:cc'`.
|
62
|
+
#; [!qmkfv] converts `:aa_bb_cc` into `'aa-bb-cc'`.
|
63
|
+
#; [!tvczb] converts `:_aa_bb:_cc_dd:_ee` into `'_aa-bb:_cc-dd:_ee'`.
|
64
|
+
s = meth.to_s # ex: :foo => "foo"
|
65
|
+
s = s.sub(/_+\z/, '') # ex: "foo_" => "foo"
|
66
|
+
s = s.gsub(/(__)+/, ':') # ex: "aa__bb__cc" => "aa:bb:cc"
|
67
|
+
s = s.gsub(/(?<=\w)_/, '-') # ex: '_aa_bb:_cc_dd' => '_aa-bb:_cc-dd'
|
68
|
+
return s
|
46
69
|
end
|
47
70
|
|
48
|
-
def
|
49
|
-
#; [!
|
50
|
-
#; [!
|
51
|
-
|
52
|
-
|
71
|
+
def method2help(obj, meth)
|
72
|
+
#; [!q3y3a] returns command argument string which represents method parameters.
|
73
|
+
#; [!r6u58] converts `.foo(x)` into `' <x>'`.
|
74
|
+
#; [!8be14] converts `.foo(x=0)` into `' [<x>]'`.
|
75
|
+
#; [!skofc] converts `.foo(*x)` into `' [<x>...]'`.
|
76
|
+
#; [!61xy6] converts `.foo(x, y=0, *z)` into `' <x> [<y> [<z>...]]'`.
|
77
|
+
#; [!0342t] ignores keyword parameters.
|
78
|
+
sb = []; n = 0
|
79
|
+
obj.method(meth).parameters.each do |kind, param|
|
80
|
+
case kind
|
81
|
+
when :req ; sb << " <#{param2arg(param)}>"
|
82
|
+
when :opt ; sb << " [<#{param2arg(param)}>" ; n += 1
|
83
|
+
when :rest ; sb << " [<#{param2arg(param)}>...]"
|
84
|
+
when :key
|
85
|
+
when :keyreq
|
86
|
+
when :keyrest
|
87
|
+
else
|
88
|
+
raise "** assertion failed: kind=#{kind.inspect}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
sb << ("]" * n) if n > 0
|
92
|
+
#; [!mbxy5] converts `.foo(x, *x_)` into `' <x>...'`.
|
93
|
+
#; [!mh9ni] converts `.foo(x, *x2)` into `' <x>...'`.
|
94
|
+
return sb.join().sub(/<([^>]+)> \[<\1[-_2]>\.\.\.\]/, "<\\1>...")
|
53
95
|
end
|
54
96
|
|
55
|
-
def
|
56
|
-
#; [!
|
57
|
-
|
58
|
-
#; [!
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
97
|
+
def param2arg(param)
|
98
|
+
#; [!ahvsn] converts parameter name (Symbol) into argument name (String).
|
99
|
+
#; [!27dpw] converts `:aa_or_bb_or_cc` into `'aa|bb|cc'`.
|
100
|
+
#; [!to41h] converts `:aa__bb__cc` into `'aa.bb.cc'`.
|
101
|
+
#; [!2ma08] converts `:aa_bb_cc` into `'aa-bb-cc'`.
|
102
|
+
s = param.to_s
|
103
|
+
s = s.gsub('_or_', '|') # ex: 'file_or_dir' => 'file|dir'
|
104
|
+
s = s.gsub('__' , '.') # ex: 'file__html' => 'file.html'
|
105
|
+
s = s.gsub('_' , '-') # ex: 'foo_bar_baz' => 'foo-bar-baz'
|
106
|
+
return s
|
63
107
|
end
|
64
108
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
109
|
+
def validate_args_and_kwargs(obj, meth, args, kwargs)
|
110
|
+
n_req = 0; n_opt = 0; rest_p = false; keyrest_p = false
|
111
|
+
kws = kwargs.dup
|
112
|
+
obj.method(meth).parameters.each do |kind, param|
|
113
|
+
case kind
|
114
|
+
when :req ; n_req += 1 # ex: f(x)
|
115
|
+
when :opt ; n_opt += 1 # ex: f(x=0)
|
116
|
+
when :rest ; rest_p = true # ex: f(*x)
|
117
|
+
when :key ; kws.delete(param) # ex: f(x: 0)
|
118
|
+
when :keyreq ; kws.delete(param) # ex: f(x:)
|
119
|
+
when :keyrest ; keyrest_p = true # ex: f(**x)
|
120
|
+
else
|
121
|
+
raise "** assertion failed: kind=#{kind.inspect}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
#; [!jalnr] returns error message if argument required but no args specified.
|
125
|
+
#; [!gv6ow] returns error message if too less arguments.
|
126
|
+
if args.length < n_req
|
127
|
+
return (args.length == 0) \
|
128
|
+
? "Argument required (but nothing specified)." \
|
129
|
+
: "Too less arguments (at least #{n_req} args)."
|
130
|
+
end
|
131
|
+
#; [!q5rp3] returns error message if argument specified but no args expected.
|
132
|
+
#; [!dewkt] returns error message if too much arguments specified.
|
133
|
+
if args.length > n_req + n_opt && ! rest_p
|
134
|
+
return (n_req + n_opt == 0) \
|
135
|
+
? "#{args[0].inspect}: Unexpected argument (expected no args)." \
|
136
|
+
: "Too much arguments (at most #{n_req + n_opt} args)."
|
137
|
+
end
|
138
|
+
#; [!u7wgm] returns error message if unknown keyword argument specified.
|
139
|
+
if ! kws.empty? && ! keyrest_p
|
140
|
+
return "#{kws.keys.first}: Unknown keyword argument."
|
141
|
+
end
|
142
|
+
#; [!2ep76] returns nil if no error found.
|
143
|
+
return nil
|
71
144
|
end
|
72
145
|
|
73
|
-
def
|
74
|
-
#; [!
|
146
|
+
def delete_escape_chars(str)
|
147
|
+
#; [!snl3e] removes escape chars from string.
|
75
148
|
return str.gsub(/\e\[.*?m/, '')
|
76
149
|
end
|
77
150
|
|
78
|
-
|
79
|
-
|
80
|
-
|
151
|
+
def color_mode?()
|
152
|
+
#; [!xyta1] returns value of $COLOR_MODE if it is not nil.
|
153
|
+
#; [!8xufh] returns value of $stdout.tty? if $COLOR_MODE is nil.
|
154
|
+
return $COLOR_MODE != nil ? $COLOR_MODE : $stdout.tty?
|
81
155
|
end
|
82
156
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
#; [!z1ygi] supports nested tag.
|
91
|
-
case tag
|
92
|
-
when nil ; return nil
|
93
|
-
when :important, "important" ; return true
|
94
|
-
when :unimportant, "unimportant" ; return false
|
95
|
-
when Array
|
96
|
-
return true if tag.include?(:important)
|
97
|
-
return false if tag.include?(:unimportant)
|
98
|
-
return nil
|
99
|
-
else
|
100
|
-
return nil
|
157
|
+
def method_override?(klass, meth) # :nodoc:
|
158
|
+
#; [!ldd1x] returns true if method defined in parent or ancestor classes.
|
159
|
+
klass.ancestors[1..-1].each do |cls|
|
160
|
+
if cls.method_defined?(meth) || cls.private_method_defined?(meth)
|
161
|
+
return true
|
162
|
+
end
|
163
|
+
break if cls.is_a?(Class)
|
101
164
|
end
|
165
|
+
#; [!bc65v] returns false if meethod not defined in parent nor ancestor classes.
|
166
|
+
return false
|
102
167
|
end
|
103
168
|
|
104
|
-
def
|
105
|
-
|
169
|
+
def name_should_be_a_string(name, kind, errcls)
|
170
|
+
#; [!9j4d0] do nothing if name is a string.
|
171
|
+
#; [!a2n8y] raises error if name is not a string.
|
172
|
+
name.is_a?(String) or
|
173
|
+
raise errcls.new("`#{name.inspect}`: #{kind} name should be a string, but got #{name.class.name} object.")
|
174
|
+
nil
|
106
175
|
end
|
107
176
|
|
108
|
-
|
109
|
-
return "\e[2m#{s}\e[0m"
|
110
|
-
end
|
177
|
+
end
|
111
178
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
leftside = !! $1
|
129
|
-
width = $2.to_i
|
130
|
-
n = width - name.length
|
131
|
-
n = 0 if n < 0
|
132
|
-
s = " " * n
|
133
|
-
#; [!7bl2b] considers minus sign in format.
|
134
|
-
return leftside ? (yield name) + s : s + (yield name)
|
135
|
-
else
|
136
|
-
return yield name
|
137
|
-
end
|
179
|
+
|
180
|
+
class OptionSchema < Benry::CmdOpt::Schema
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
class ActionOptionSchema < OptionSchema
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
class OptionParser < Benry::CmdOpt::Parser
|
189
|
+
|
190
|
+
def parse(args, all: true)
|
191
|
+
#; [!iaawe] raises OptionError if option error found.
|
192
|
+
return super
|
193
|
+
rescue Benry::CmdOpt::OptionError => exc
|
194
|
+
raise OptionError.new(exc.message)
|
138
195
|
end
|
139
196
|
|
140
197
|
end
|
141
198
|
|
142
199
|
|
143
|
-
|
200
|
+
ACTION_OPTION_SCHEMA_CLASS = ActionOptionSchema
|
201
|
+
ACTION_OPTION_PARSER_CLASS = OptionParser
|
202
|
+
ACTION_SHARED_OPTIONS = proc {|dummy_schema|
|
203
|
+
arr = []
|
204
|
+
arr << dummy_schema.add(:help, "-h, --help", "print help message", hidden: true)#.freeze
|
205
|
+
arr
|
206
|
+
}.call(OptionSchema.new)
|
144
207
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
def lookup_action(action_name)
|
152
|
-
name = action_name.to_s
|
153
|
-
#; [!tnwq0] supports alias name.
|
154
|
-
alias_obj = nil
|
155
|
-
if @aliases[name]
|
156
|
-
alias_obj = @aliases[name]
|
157
|
-
name = alias_obj.action_name
|
158
|
-
end
|
159
|
-
#; [!vivoa] returns action metadata object.
|
160
|
-
#; [!z15vu] returns ActionWithArgs object if alias has args and/or kwargs.
|
161
|
-
metadata = @actions[name]
|
162
|
-
if alias_obj && alias_obj.args && ! alias_obj.args.empty?
|
163
|
-
args = alias_obj.args.dup()
|
164
|
-
opts = metadata.parse_options(args)
|
165
|
-
return ActionWithArgs.new(metadata, args, opts)
|
166
|
-
else
|
167
|
-
return metadata
|
168
|
-
end
|
208
|
+
|
209
|
+
class OptionSet
|
210
|
+
|
211
|
+
def initialize(*items)
|
212
|
+
@items = items
|
169
213
|
end
|
170
214
|
|
171
|
-
def
|
172
|
-
#; [!
|
173
|
-
|
174
|
-
#; [!
|
175
|
-
|
176
|
-
#; [!arcia] action names are sorted.
|
177
|
-
metadatas = @actions.values()
|
178
|
-
metadatas = metadatas.reject {|ameta| ameta.hidden? } if ! all
|
179
|
-
pairs = metadatas.collect {|ameta|
|
180
|
-
[ameta.name, ameta.desc, ameta.important?]
|
181
|
-
}
|
182
|
-
pairs += @aliases.collect {|name, aliobj|
|
183
|
-
[name, aliobj.desc, aliobj.important?]
|
184
|
-
} if include_alias
|
185
|
-
pairs.sort_by {|name, _, _| name }.each(&block)
|
215
|
+
def copy_from(schema)
|
216
|
+
#; [!d9udc] copy option items from schema.
|
217
|
+
schema.each {|item| @items << item }
|
218
|
+
#; [!v1ok3] returns self.
|
219
|
+
self
|
186
220
|
end
|
187
221
|
|
188
|
-
def
|
189
|
-
|
222
|
+
def copy_into(schema)
|
223
|
+
#; [!n00r1] copy option items into schema.
|
224
|
+
@items.each {|item| schema.add_item(item) }
|
225
|
+
#; [!ynn1m] returns self.
|
226
|
+
self
|
190
227
|
end
|
191
228
|
|
192
|
-
def
|
193
|
-
|
194
|
-
|
229
|
+
def select(*keys)
|
230
|
+
#; [!mqkzf] creates new OptionSet object with filtered options.
|
231
|
+
items = @items.select {|item| keys.include?(item.key) }
|
232
|
+
return self.class.new(*items)
|
195
233
|
end
|
196
234
|
|
197
|
-
def
|
198
|
-
#; [!
|
199
|
-
|
200
|
-
|
201
|
-
raise ActionNotFoundError.new("delete_action(#{action_name.inspect}): Action not found.")
|
235
|
+
def exclude(*keys)
|
236
|
+
#; [!oey0q] creates new OptionSet object with remained options.
|
237
|
+
items = @items.select {|item| ! keys.include?(item.key) }
|
238
|
+
return self.class.new(*items)
|
202
239
|
end
|
203
240
|
|
204
|
-
|
205
|
-
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
class BaseMetadata
|
245
|
+
|
246
|
+
def initialize(name, desc, tag: nil, important: nil, hidden: nil)
|
247
|
+
@name = name
|
248
|
+
@desc = desc
|
249
|
+
@tag = tag if nil != tag
|
250
|
+
@important = important if nil != important
|
251
|
+
@hidden = hidden if nil != hidden
|
206
252
|
end
|
207
253
|
|
208
|
-
|
209
|
-
|
210
|
-
|
254
|
+
attr_reader :name, :desc, :tag, :important, :hidden
|
255
|
+
alias important? important
|
256
|
+
alias hidden? hidden
|
257
|
+
|
258
|
+
def alias?()
|
259
|
+
raise NotImplementedError.new("#{self.class.name}#alias?(): not implemented yet.")
|
211
260
|
end
|
212
261
|
|
213
|
-
|
214
|
-
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
class ActionMetadata < BaseMetadata
|
266
|
+
|
267
|
+
def initialize(name, desc, schema, klass, meth, usage: nil, detail: nil, description: nil, postamble: nil, tag: nil, important: nil, hidden: nil)
|
268
|
+
super(name, desc, tag: tag, important: important, hidden: hidden)
|
269
|
+
@schema = schema
|
270
|
+
@klass = klass
|
271
|
+
@meth = meth
|
272
|
+
@usage = usage if nil != usage
|
273
|
+
@detail = detail if nil != detail
|
274
|
+
@description = description if nil != description
|
275
|
+
@postamble = postamble if nil != postamble
|
215
276
|
end
|
216
277
|
|
217
|
-
|
218
|
-
|
219
|
-
|
278
|
+
attr_reader :schema, :klass, :meth, :usage, :detail, :description, :postamble
|
279
|
+
|
280
|
+
def hidden?()
|
281
|
+
#; [!stied] returns true/false if `hidden:` kwarg provided.
|
282
|
+
#; [!eumhz] returns true/false if method is private or not.
|
283
|
+
return @hidden if @hidden != nil
|
284
|
+
return ! @klass.method_defined?(@meth)
|
220
285
|
end
|
221
286
|
|
222
|
-
def
|
223
|
-
|
287
|
+
def option_empty?(all: false)
|
288
|
+
#; [!14xgg] returns true if the action has no options.
|
289
|
+
#; [!dbtht] returns false if the action has at least one option.
|
290
|
+
#; [!wa315] considers hidden options if `all: true` passed.
|
291
|
+
return @schema.empty?(all: all)
|
224
292
|
end
|
225
293
|
|
226
|
-
def
|
227
|
-
|
228
|
-
|
294
|
+
def option_help(format, all: false)
|
295
|
+
#; [!bpkwn] returns help message string of the action.
|
296
|
+
#; [!76hni] includes hidden options in help message if `all:` is truthy.
|
297
|
+
return @schema.option_help(format, all: all)
|
229
298
|
end
|
230
299
|
|
231
|
-
def
|
232
|
-
|
300
|
+
def parse_options(args)
|
301
|
+
#; [!gilca] returns parsed options.
|
302
|
+
#; [!v34yk] raises OptionError if option has error.
|
303
|
+
parser = ACTION_OPTION_PARSER_CLASS.new(@schema)
|
304
|
+
return parser.parse(args, all: true) # raises error if invalid option given
|
233
305
|
end
|
234
306
|
|
235
|
-
def
|
236
|
-
|
237
|
-
|
307
|
+
def alias?()
|
308
|
+
#; [!c1eq3] returns false which means that this is not an alias metadata.
|
309
|
+
return false
|
238
310
|
end
|
239
311
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
class AliasMetadata < BaseMetadata
|
316
|
+
|
317
|
+
def initialize(alias_name, action_name, args, tag: nil, important: nil, hidden: nil)
|
318
|
+
#; [!qtb61] sets description string automatically.
|
319
|
+
#; [!kgic6] includes args value into description if provided.
|
320
|
+
desc = _build_desc(action_name, args)
|
321
|
+
super(alias_name, desc, tag: tag, important: important, hidden: hidden)
|
322
|
+
@action = action_name
|
323
|
+
@args = args
|
245
324
|
end
|
246
325
|
|
247
|
-
|
248
|
-
|
326
|
+
attr_reader :action, :args
|
327
|
+
|
328
|
+
def _build_desc(action_name, args)
|
329
|
+
return args && ! args.empty? ? "alias for '#{action_name} #{args.join(' ')}'" \
|
330
|
+
: "alias for '#{action_name}'"
|
249
331
|
end
|
332
|
+
private :_build_desc
|
250
333
|
|
251
|
-
def
|
252
|
-
|
334
|
+
def alias?()
|
335
|
+
#; [!c798o] returns true which means that this is an alias metadata.
|
336
|
+
return true
|
253
337
|
end
|
254
338
|
|
255
|
-
def
|
256
|
-
|
339
|
+
def name_with_args()
|
340
|
+
#; [!6kjuv] returns alias name if no args.
|
341
|
+
return @name if ! @args || @args.empty?
|
342
|
+
#; [!d4xrb] returns alias name and args as combined.
|
343
|
+
return "#{@name} (with '#{@args.join(' ')}')"
|
257
344
|
end
|
258
345
|
|
259
346
|
end
|
260
347
|
|
261
348
|
|
262
|
-
|
349
|
+
module ClassMethodModule
|
263
350
|
|
351
|
+
def define_alias(alias_name, action_name, tag: nil, important: nil, hidden: nil)
|
352
|
+
return __define_alias(alias_name, action_name, tag: tag, important: important, hidden: hidden, alias_for_alias: false)
|
353
|
+
end
|
264
354
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
355
|
+
def define_alias!(alias_name, action_name, tag: nil, important: nil, hidden: nil) # :nodoc:
|
356
|
+
return __define_alias(alias_name, action_name, tag: tag, important: important, hidden: hidden, alias_for_alias: true)
|
357
|
+
end
|
358
|
+
|
359
|
+
def __define_alias(alias_name, action_name, tag: nil, important: nil, hidden: nil, alias_for_alias: false) # :nodoc:
|
360
|
+
#; [!zawcd] action arg can be a string or an array of string.
|
361
|
+
action_arg = action_name
|
362
|
+
if action_arg.is_a?(Array)
|
363
|
+
action_name, *args = action_arg
|
364
|
+
else
|
365
|
+
args = []
|
366
|
+
end
|
367
|
+
#; [!hqc27] raises DefinitionError if something error exists in alias or action.
|
368
|
+
errmsg = __validate_alias_and_action(alias_name, action_name, alias_for_alias)
|
369
|
+
errmsg == nil or
|
370
|
+
raise DefinitionError.new("define_alias(#{alias_name.inspect}, #{action_arg.inspect}): #{errmsg}")
|
371
|
+
#; [!oo91b] registers new metadata of alias.
|
372
|
+
alias_metadata = AliasMetadata.new(alias_name, action_name, args, tag: tag, important: important, hidden: hidden)
|
373
|
+
REGISTRY.metadata_add(alias_metadata)
|
374
|
+
#; [!wfbqu] returns alias metadata.
|
375
|
+
return alias_metadata
|
376
|
+
end
|
377
|
+
private :__define_alias
|
378
|
+
|
379
|
+
def __validate_alias_and_action(alias_name, action_name, alias_for_alias=false) # :nodoc:
|
380
|
+
#; [!2x1ew] returns error message if alias name is not a string.
|
381
|
+
#; [!galce] returns error message if action name is not a string.
|
382
|
+
if ! alias_name.is_a?(String)
|
383
|
+
return "Alias name should be a string, but got #{alias_name.class.name} object."
|
384
|
+
elsif ! action_name.is_a?(String)
|
385
|
+
return "Action name should be a string, but got #{action_name.class.name} object."
|
386
|
+
end
|
387
|
+
#; [!zh0a9] returns error message if other alias already exists.
|
388
|
+
#; [!ohow0] returns error message if other action exists with the same name as alias.
|
389
|
+
alias_md = REGISTRY.metadata_get(alias_name)
|
390
|
+
if alias_md == nil ; nil # ok: new alias should be not defined
|
391
|
+
elsif alias_md.alias? ; return "Alias '#{alias_name}' already defined."
|
392
|
+
else ; return "Can't define new alias '#{alias_name}' because already defined as an action."
|
393
|
+
end
|
394
|
+
#; [!r24qn] returns error message if action doesn't exist.
|
395
|
+
#; [!lxolh] returns error message if action is an alias name.
|
396
|
+
action_md = REGISTRY.metadata_get(action_name)
|
397
|
+
if action_md == nil ; return "Action '#{action_name}' not found."
|
398
|
+
elsif alias_for_alias ; nil # ok: alias for alias is allowed
|
399
|
+
elsif action_md.alias? ; return "'#{action_name}' should be an action, but is an alias."
|
400
|
+
else ; nil # ok: action should be defined
|
401
|
+
end
|
402
|
+
#; [!b6my2] returns nil if no errors found.
|
403
|
+
return nil
|
404
|
+
end
|
405
|
+
private :__validate_alias_and_action
|
406
|
+
|
407
|
+
def undef_alias(alias_name)
|
408
|
+
#; [!pk3ya] raises DefinitionError if alias name is not a string.
|
409
|
+
Util.name_should_be_a_string(alias_name, 'Alias', DefinitionError)
|
410
|
+
#; [!krdkt] raises DefinitionError if alias not exist.
|
411
|
+
#; [!juykx] raises DefinitionError if action specified instead of alias.
|
412
|
+
md = REGISTRY.metadata_get(alias_name)
|
413
|
+
errmsg = (
|
414
|
+
if md == nil ; "Alias not exist."
|
415
|
+
elsif md.alias? ; nil
|
416
|
+
else ; "Alias expected but action name specified."
|
417
|
+
end
|
418
|
+
)
|
419
|
+
errmsg == nil or
|
420
|
+
raise DefinitionError.new("undef_alias(#{alias_name.inspect}): #{errmsg}")
|
421
|
+
#; [!ocyso] deletes existing alias.
|
422
|
+
REGISTRY.metadata_del(alias_name)
|
423
|
+
nil
|
424
|
+
end
|
425
|
+
|
426
|
+
def undef_action(action_name)
|
427
|
+
#; [!bcyn3] raises DefinitionError if action name is not a string.
|
428
|
+
Util.name_should_be_a_string(action_name, 'Action', DefinitionError)
|
429
|
+
#; [!bvu95] raises error if action not exist.
|
430
|
+
#; [!717fw] raises error if alias specified instead of action.
|
431
|
+
md = REGISTRY.metadata_get(action_name)
|
432
|
+
errmsg = (
|
433
|
+
if md == nil ; "Action not exist."
|
434
|
+
elsif md.alias? ; "Action expected but alias name specified."
|
435
|
+
else ; nil
|
436
|
+
end
|
437
|
+
)
|
438
|
+
errmsg == nil or
|
439
|
+
raise DefinitionError.new("undef_action(#{action_name.inspect}): #{errmsg}")
|
440
|
+
#; [!01sx1] deletes existing action.
|
441
|
+
REGISTRY.metadata_del(action_name)
|
442
|
+
#; [!op8z5] deletes action method from action class.
|
443
|
+
md.klass.class_eval { remove_method(md.meth) }
|
444
|
+
nil
|
445
|
+
end
|
446
|
+
|
447
|
+
def define_abbrev(abbrev, prefix)
|
448
|
+
#; [!e1fob] raises DefinitionError if error found.
|
449
|
+
errmsg = __validate_abbrev(abbrev, prefix)
|
450
|
+
errmsg == nil or
|
451
|
+
raise DefinitionError.new(errmsg)
|
452
|
+
#; [!ed6hr] registers abbrev with prefix.
|
453
|
+
REGISTRY.abbrev_add(abbrev, prefix)
|
454
|
+
nil
|
455
|
+
end
|
456
|
+
|
457
|
+
def __validate_abbrev(abbrev, prefix, _registry: REGISTRY) # :nodoc:
|
458
|
+
#; [!qfzbp] abbrev should be a string.
|
459
|
+
abbrev.is_a?(String) or return "#{abbrev.inspect}: Abbreviation should be a string, but got #{abbrev.class.name} object."
|
460
|
+
#; [!f5isx] abbrev should end with ':'.
|
461
|
+
abbrev.end_with?(":") or return "'#{abbrev}': Abbreviation should end with ':'."
|
462
|
+
#; [!r673p] abbrev should not contain unexpected symbol.
|
463
|
+
abbrev =~ /\A\w[-\w]*:/ or return "'#{abbrev}': Invalid abbreviation."
|
464
|
+
#; [!dckvt] abbrev should not exist.
|
465
|
+
! _registry.abbrev_exist?(abbrev) or return "'#{abbrev}': Abbreviation is already defined."
|
466
|
+
#; [!5djjt] abbrev should not be the same name with existing prefix.
|
467
|
+
! _registry.category_exist?(abbrev) or return "'#{abbrev}': Abbreviation is not available because a prefix with the same name already exists."
|
468
|
+
#; [!mq4ki] prefix should be a string.
|
469
|
+
prefix.is_a?(String) or return "#{prefix.inspect}: Prefix should be a string, but got #{prefix.class.name} object."
|
470
|
+
#; [!a82z3] prefix should end with ':'.
|
471
|
+
prefix.end_with?(":") or return "'#{prefix}': Prefix should end with ':'."
|
472
|
+
#; [!eq5iu] prefix should exist.
|
473
|
+
_registry.category_exist?(prefix) or return "'#{prefix}': No such prefix."
|
474
|
+
#; [!jzkhc] returns nil if no error found.
|
475
|
+
return nil
|
476
|
+
end
|
477
|
+
private :__validate_abbrev
|
478
|
+
|
479
|
+
def current_app() # :nodoc:
|
480
|
+
#; [!xdjce] returns current application.
|
481
|
+
return @current_app
|
482
|
+
end
|
483
|
+
|
484
|
+
def _set_current_app(app) # :nodoc:
|
485
|
+
#; [!1yqwl] sets current application.
|
486
|
+
@current_app = app
|
487
|
+
nil
|
488
|
+
end
|
270
489
|
|
271
|
-
def self.delete_alias(alias_name)
|
272
|
-
#; [!9g0x9] deletes alias.
|
273
|
-
#; [!r49vi] raises error if alias not exist.
|
274
|
-
INDEX.delete_alias(alias_name)
|
275
490
|
end
|
491
|
+
extend ClassMethodModule
|
276
492
|
|
277
493
|
|
278
|
-
class
|
494
|
+
class ActionScope
|
279
495
|
|
280
|
-
def initialize(
|
281
|
-
@
|
282
|
-
@
|
283
|
-
@method = method
|
284
|
-
@schema = schema
|
285
|
-
@desc = desc
|
286
|
-
@detail = detail if detail != nil
|
287
|
-
@postamble = postamble if postamble != nil
|
288
|
-
@important = important if important != nil
|
289
|
-
@tag = tag if tag != nil
|
290
|
-
@hidden = hidden if hidden != nil
|
496
|
+
def initialize(config, context=nil)
|
497
|
+
@config = config
|
498
|
+
@__context__ = context || CONTEXT_CLASS.new(config)
|
291
499
|
end
|
292
500
|
|
293
|
-
|
501
|
+
def __clear_recursive_reference() # :nodoc:
|
502
|
+
#; [!i68z0] clears instance var which refers context object.
|
503
|
+
@__context__ = nil
|
504
|
+
nil
|
505
|
+
end
|
294
506
|
|
295
|
-
def
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
507
|
+
def inspect()
|
508
|
+
return super.split().first() + ">"
|
509
|
+
end
|
510
|
+
|
511
|
+
def self.inherited(subclass)
|
512
|
+
subclass.class_eval do
|
513
|
+
@__actiondef__ = nil
|
514
|
+
@__prefixdef__ = nil
|
515
|
+
#; [!8cck9] sets Proc object to `@action` in subclass.
|
516
|
+
@action = lambda do |desc, usage: nil, detail: nil, description: nil, postamble: nil, tag: nil, important: nil, hidden: nil|
|
517
|
+
#; [!r07i7] `@action.()` raises DefinitionError if called consectively.
|
518
|
+
@__actiondef__ == nil or
|
519
|
+
raise DefinitionError.new("`@action.()` called without method definition (please define method for this action).")
|
520
|
+
schema = _new_option_schema()
|
521
|
+
#; [!34psw] `@action.()` stores arguments into `@__actiondef__`.
|
522
|
+
kws = {usage: usage, detail: detail, description: description, postamble: postamble, tag: tag, important: important, hidden: hidden}
|
523
|
+
@__actiondef__ = [desc, schema, kws]
|
524
|
+
end
|
525
|
+
#; [!en6n0] sets Proc object to `@option` in subclass.
|
526
|
+
@option = lambda do |key, optstr, desc,
|
527
|
+
type: nil, rexp: nil, pattern: nil, enum: nil,
|
528
|
+
range: nil, value: nil, detail: nil,
|
529
|
+
tag: nil, important: nil, hidden: nil, &callback|
|
530
|
+
#; [!68hf8] raises DefinitionError if `@option.()` called without `@action.()`.
|
531
|
+
@__actiondef__ != nil or
|
532
|
+
raise DefinitionError.new("`@option.()` called without `@action.()`.")
|
533
|
+
#; [!2p98r] `@option.()` stores arguments into option schema object.
|
534
|
+
schema = @__actiondef__[1]
|
535
|
+
schema.add(key, optstr, desc,
|
536
|
+
type: type, rexp: rexp, pattern: pattern, enum: enum,
|
537
|
+
range: range, value: value, detail: detail,
|
538
|
+
tag: tag, important: important, hidden: hidden, &callback)
|
539
|
+
end
|
540
|
+
#; [!aiwns] `@copy_options.()` copies options from other action.
|
541
|
+
@copy_options = lambda do |action_name, except: []|
|
542
|
+
#; [!bfxye] `@copy_options.()` tries to find an action with current prefix.
|
543
|
+
metadata = nil
|
544
|
+
curr_prefix = current_prefix()
|
545
|
+
if curr_prefix
|
546
|
+
metadata = REGISTRY.metadata_get(curr_prefix + action_name)
|
547
|
+
end
|
548
|
+
#; [!mhhn2] `@copy_options.()` raises DefinitionError when action not found.
|
549
|
+
metadata ||= REGISTRY.metadata_get(action_name)
|
550
|
+
metadata != nil or
|
551
|
+
raise DefinitionError.new("@copy_options.(#{action_name.inspect}): Action not found.")
|
552
|
+
#; [!0slo8] raises DefinitionError if `@copy_options.()` called without `@action.()`.
|
553
|
+
@__actiondef__ != nil or
|
554
|
+
raise DefinitionError.new("@copy_options.(#{action_name.inspect}): Called without `@action.()`.")
|
555
|
+
#; [!0qz0q] `@copy_options.()` stores arguments into option schema object.
|
556
|
+
#; [!dezh1] `@copy_options.()` ignores help option automatically.
|
557
|
+
schema = @__actiondef__[1]
|
558
|
+
except = except.is_a?(Array) ? except : (except == nil ? [] : [except])
|
559
|
+
schema.copy_from(metadata.schema, except: [:help] + except)
|
560
|
+
end
|
561
|
+
#; [!7g5ug] sets Proc object to `@optionset` in subclass.
|
562
|
+
@optionset = lambda do |*optionsets|
|
563
|
+
#; [!o27kt] raises DefinitionError if `@optionset.()` called without `@action.()`.
|
564
|
+
@__actiondef__ != nil or
|
565
|
+
raise DefinitionError.new("`@optionset.()` called without `@action.()`.")
|
566
|
+
#; [!ky6sg] copies option items from optionset into schema object.
|
567
|
+
schema = @__actiondef__[1]
|
568
|
+
optionsets.each {|optset| optset.copy_into(schema) }
|
569
|
+
end
|
570
|
+
end
|
571
|
+
nil
|
572
|
+
end
|
573
|
+
|
574
|
+
def self._new_option_schema() # :nodoc:
|
575
|
+
#; [!zuxmj] creates new option schema object.
|
576
|
+
schema = ACTION_OPTION_SCHEMA_CLASS.new()
|
577
|
+
#; [!rruxi] adds '-h, --help' option as hidden automatically.
|
578
|
+
ACTION_SHARED_OPTIONS.each {|item| schema.add_item(item) }
|
579
|
+
return schema
|
580
|
+
end
|
581
|
+
private_class_method :_new_option_schema
|
582
|
+
|
583
|
+
def self.method_added(method_symbol)
|
584
|
+
#; [!6frgx] do nothing if `@action.()` is not called.
|
585
|
+
return false if @__actiondef__ == nil
|
586
|
+
#; [!e3yjo] clears `@__actiondef__`.
|
587
|
+
meth = method_symbol
|
588
|
+
desc, schema, kws = @__actiondef__
|
589
|
+
@__actiondef__ = nil
|
590
|
+
#; [!jq4ex] raises DefinitionError if option defined but corresponding keyword arg is missing.
|
591
|
+
errmsg = __validate_kwargs(method_symbol, schema)
|
592
|
+
errmsg == nil or
|
593
|
+
raise DefinitionError,
|
594
|
+
"def #{method_symbol}(): #{errmsg}"
|
595
|
+
#; [!ejdlo] converts method name to action name.
|
596
|
+
action = Util.method2action(meth) # ex: :a__b_c => "a:b-c"
|
597
|
+
#; [!w9qat] when `category()` called before defining action method...
|
598
|
+
alias_p = false
|
599
|
+
if @__prefixdef__
|
600
|
+
prefix, prefix_action, alias_target = @__prefixdef__
|
601
|
+
#; [!3pl1r] renames method name to new name with prefix.
|
602
|
+
meth = "#{prefix.gsub(':', '__')}#{meth}".intern
|
603
|
+
alias_method(meth, method_symbol)
|
604
|
+
remove_method(method_symbol)
|
605
|
+
#; [!mil2g] when action name matched to 'action:' kwarg of `category()`...
|
606
|
+
if action == prefix_action
|
607
|
+
#; [!hztpp] uses pefix name as action name.
|
608
|
+
action = prefix.chomp(':')
|
609
|
+
#; [!cydex] clears `action:` kwarg.
|
610
|
+
@__prefixdef__[1] = nil
|
611
|
+
#; [!8xsnw] when action name matched to `alias_for:` kwarg of `category()`...
|
612
|
+
elsif action == alias_target
|
613
|
+
#; [!iguvp] adds prefix name to action name.
|
614
|
+
action = prefix + action
|
615
|
+
alias_p = true
|
616
|
+
#; [!wmevh] else...
|
617
|
+
else
|
618
|
+
#; [!9cyc2] adds prefix name to action name.
|
619
|
+
action = prefix + action
|
620
|
+
end
|
621
|
+
#; [!y8lh0] else...
|
622
|
+
else
|
623
|
+
#; [!0ki5g] not add prefix to action name.
|
624
|
+
prefix = alias_target = nil
|
625
|
+
end
|
626
|
+
#; [!dad1q] raises DefinitionError if action with same name already defined.
|
627
|
+
#; [!ur8lp] raises DefinitionError if method already defined in parent or ancestor class.
|
628
|
+
#; [!dj0ql] method override check is done with new method name (= prefixed name).
|
629
|
+
(errmsg = __validate_action_method(action, meth, method_symbol)) == nil or
|
630
|
+
raise DefinitionError.new("def #{method_symbol}(): #{errmsg}")
|
631
|
+
#; [!7fnh4] registers action metadata.
|
632
|
+
action_metadata = ActionMetadata.new(action, desc, schema, self, meth, **kws)
|
633
|
+
REGISTRY.metadata_add(action_metadata)
|
634
|
+
#; [!lyn0z] registers alias metadata if necessary.
|
635
|
+
if alias_p
|
636
|
+
prefix != nil or raise "** assertion failed: ailas_target=#{alias_target.inspect}"
|
637
|
+
alias_metadata = AliasMetadata.new(prefix.chomp(':'), action, nil)
|
638
|
+
REGISTRY.metadata_add(alias_metadata)
|
639
|
+
#; [!4402s] clears `alias_for:` kwarg.
|
640
|
+
@__prefixdef__[2] = nil
|
641
|
+
end
|
642
|
+
#; [!u0td6] registers prefix of action if not registered yet.
|
643
|
+
REGISTRY.category_add_via_action(action)
|
644
|
+
#
|
645
|
+
return true # for testing purpose
|
646
|
+
end
|
647
|
+
|
648
|
+
def self.__validate_kwargs(method_symbol, schema) # :nodoc:
|
649
|
+
fnkeys = []
|
650
|
+
keyrest_p = false
|
651
|
+
self.instance_method(method_symbol).parameters.each do |kind, key|
|
652
|
+
case kind
|
653
|
+
when :key, :keyreq ; fnkeys << key # ex: f(x: nil), f(x:)
|
654
|
+
when :keyrest ; keyrest_p = true # ex: f(**x)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
#; [!xpg47] returns nil if `**kwargs` exist.
|
658
|
+
return nil if keyrest_p
|
659
|
+
#; [!qowwj] returns error message if option defined but corresponding keyword arg is missing.
|
660
|
+
optkeys = schema.each.collect(&:key)
|
661
|
+
optkeys.delete(:help)
|
662
|
+
missing = optkeys - fnkeys
|
663
|
+
missing.empty? or
|
664
|
+
return "Keyword argument `#{missing[0]}:` expected which corresponds to the `:#{missing[0]}` option, but not exist."
|
665
|
+
#toomuch = fnkeys - optkeys
|
666
|
+
#toomuch.empty? or
|
667
|
+
# return "Keyword argument `#{toomuch[0]}:` exist but has no corresponding option."
|
308
668
|
return nil
|
309
669
|
end
|
670
|
+
private_class_method :__validate_kwargs
|
310
671
|
|
311
|
-
def
|
312
|
-
#; [!
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
672
|
+
def self.__validate_action_method(action, meth, method_symbol) # :nodoc:
|
673
|
+
#; [!5a4d3] returns error message if action with same name already defined.
|
674
|
+
! REGISTRY.metadata_exist?(action) or
|
675
|
+
return "Action '#{action}' already defined (to redefine it, delete it beforehand by `undef_action()`)."
|
676
|
+
#; [!uxsx3] returns error message if method already defined in parent or ancestor class.
|
677
|
+
#; [!3fmpo] method override check is done with new method name (= prefixed name).
|
678
|
+
! Util.method_override?(self, meth) or
|
679
|
+
return "Please rename it to `#{method_symbol}_()`, because it overrides existing method in parent or ancestor class."
|
680
|
+
return nil
|
317
681
|
end
|
682
|
+
private_class_method :__validate_action_method
|
318
683
|
|
319
|
-
def
|
320
|
-
|
321
|
-
|
684
|
+
def self.current_prefix()
|
685
|
+
#; [!2zt0f] returns current prefix name such as 'foo:bar:'.
|
686
|
+
return @__prefixdef__ ? @__prefixdef__[0] : nil
|
687
|
+
end
|
688
|
+
|
689
|
+
def self.category(prefix, desc=nil, action: nil, alias_for: nil, &block)
|
690
|
+
#; [!mp1p5] raises DefinitionError if prefix is invalid.
|
691
|
+
errmsg = __validate_prefix(prefix)
|
692
|
+
errmsg == nil or
|
693
|
+
raise DefinitionError.new("category(#{prefix.inspect}): #{errmsg}")
|
694
|
+
#; [!q01ma] raises DefinitionError if action or alias name is invalid.
|
695
|
+
argstr, errmsg = __validate_action_and_alias(action, alias_for)
|
696
|
+
errmsg == nil or
|
697
|
+
raise DefinitionError.new("`category(#{prefix.inspect}, #{argstr})`: #{errmsg}")
|
698
|
+
#; [!kwst6] if block given...
|
699
|
+
if block_given?()
|
700
|
+
#; [!t8wwm] saves previous prefix data and restore them at end of block.
|
701
|
+
prev = @__prefixdef__
|
702
|
+
prefix = prev[0] + prefix if prev # ex: "foo:" => "parent:foo:"
|
703
|
+
@__prefixdef__ = [prefix, action, alias_for]
|
704
|
+
#; [!j00pk] registers prefix and description, even if no actions defined.
|
705
|
+
REGISTRY.category_add(prefix, desc)
|
706
|
+
begin
|
707
|
+
yield
|
708
|
+
#; [!w52y5] raises DefinitionError if `action:` specified but target action not defined.
|
709
|
+
if action
|
710
|
+
@__prefixdef__[1] == nil or
|
711
|
+
raise DefinitionError.new("category(#{prefix.inspect}, action: #{action.inspect}): Target action not defined.")
|
712
|
+
end
|
713
|
+
#; [!zs3b5] raises DefinitionError if `alias_for:` specified but target action not defined.
|
714
|
+
if alias_for
|
715
|
+
@__prefixdef__[2] == nil or
|
716
|
+
raise DefinitionError.new("category(#{prefix.inspect}, alias_for: #{alias_for.inspect}): Target action of alias not defined.")
|
717
|
+
end
|
718
|
+
ensure
|
719
|
+
@__prefixdef__ = prev
|
720
|
+
end
|
721
|
+
#; [!yqhm8] else...
|
322
722
|
else
|
323
|
-
#; [!
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
puts s
|
328
|
-
__run_action(*args, **kwargs)
|
329
|
-
s = "## exit: #{@name}"
|
330
|
-
s = "\e[33m#{s}\e[0m" if Util.colorize?
|
331
|
-
puts s
|
723
|
+
#; [!tgux9] just stores arguments into class.
|
724
|
+
@__prefixdef__ = [prefix, action, alias_for]
|
725
|
+
#; [!ncskq] registers prefix and description, even if no actions defined.
|
726
|
+
REGISTRY.category_add(prefix, desc)
|
332
727
|
end
|
333
728
|
nil
|
334
729
|
end
|
335
730
|
|
336
|
-
def
|
337
|
-
#; [!
|
338
|
-
|
339
|
-
|
340
|
-
|
731
|
+
def self.__validate_prefix(prefix) # :nodoc:
|
732
|
+
#; [!bac19] returns error message if prefix is not a string.
|
733
|
+
#; [!608fc] returns error message if prefix doesn't end with ':'.
|
734
|
+
#; [!vupza] returns error message if prefix contains '_'.
|
735
|
+
#; [!5vgn3] returns error message if prefix is invalid.
|
736
|
+
#; [!7rphu] returns nil if prefix is valid.
|
737
|
+
prefix.is_a?(String) or return "String expected, but got #{prefix.class.name}."
|
738
|
+
prefix =~ /:\z/ or return "Prefix name should end with ':'."
|
739
|
+
prefix !~ /_/ or return "Prefix name should not contain '_' (use '-' instead)."
|
740
|
+
rexp = /\A[a-z][-a-zA-Z0-9]*:([a-z][-a-zA-Z0-9]*:)*\z/
|
741
|
+
prefix =~ rexp or return "Invalid prefix name."
|
742
|
+
return nil
|
743
|
+
end
|
744
|
+
private_class_method :__validate_prefix
|
745
|
+
|
746
|
+
def self.__validate_action_and_alias(action, alias_for)
|
747
|
+
#; [!38ji9] returns error message if action name is not a string.
|
748
|
+
action == nil || action.is_a?(String) or
|
749
|
+
return "action: #{action.inspect}", "Action name should be a string, but got #{action.class.name} object."
|
750
|
+
#; [!qge3m] returns error message if alias name is not a string.
|
751
|
+
alias_for == nil || alias_for.is_a?(String) or
|
752
|
+
return "alias_for: #{alias_for.inspect}", "Alias name should be a string, but got #{alias_for.class.name} object."
|
753
|
+
#; [!ermv8] returns error message if both `action:` and `alias_for:` kwargs are specified.
|
754
|
+
! (action != nil && alias_for != nil) or
|
755
|
+
return "action: #{action.inspect}, alias_for: #{alias_for.inspect}", "`action:` and `alias_for:` are exclusive."
|
756
|
+
end
|
757
|
+
private_class_method :__validate_action_and_alias
|
758
|
+
|
759
|
+
def self.define_alias(alias_name, action_name, tag: nil, important: nil, hidden: nil)
|
760
|
+
#; [!tcpuz] just defines an alias when current prefix is nil.
|
761
|
+
prefix = self.current_prefix()
|
762
|
+
if prefix == nil
|
763
|
+
nil
|
764
|
+
#; [!b8ly2] supports array argument.
|
765
|
+
elsif action_name.is_a?(Array)
|
766
|
+
action_name[0] = prefix + action_name[0]
|
767
|
+
#; [!c6duw] defines an alias with prefix when current prefix exist.
|
341
768
|
else
|
342
|
-
|
769
|
+
action_name = prefix + action_name
|
343
770
|
end
|
771
|
+
#; [!0dkrj] keyword arguments are passed to higher function.
|
772
|
+
Benry::CmdApp.define_alias(alias_name, action_name, tag: tag, important: important, hidden: hidden)
|
344
773
|
end
|
345
|
-
private :__run_action
|
346
774
|
|
347
|
-
def
|
348
|
-
|
775
|
+
def self.optionset(&block)
|
776
|
+
#; [!us0g4] yields block with dummy action.
|
777
|
+
#; [!1idwv] clears default option items.
|
778
|
+
@action.("dummy action by optionset()")
|
779
|
+
schema = @__actiondef__[1]
|
780
|
+
schema.each.collect(&:key).each {|key| schema.delete(key) }
|
781
|
+
yield
|
782
|
+
#; [!sp3hk] clears `@__actiondef__` to make `@action.()` available.
|
783
|
+
schema = @__actiondef__[1]
|
784
|
+
@__actiondef__ = nil
|
785
|
+
#; [!mwbyc] returns new OptionSet object which contains option items.
|
786
|
+
return OptionSet.new.copy_from(schema)
|
349
787
|
end
|
350
|
-
protected :_new_action_object
|
351
788
|
|
352
|
-
def
|
353
|
-
#; [!
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
when :keyrest ; nil
|
364
|
-
else ; nil
|
365
|
-
end
|
366
|
-
end
|
367
|
-
#; [!w3rer] max is nil if variable argument exists.
|
368
|
-
return has_rest ? [n_req, nil] : [n_req, n_req + n_opt]
|
789
|
+
def run_once(action_name, *args, **kwargs)
|
790
|
+
#; [!nqjxk] runs action and returns true if not runned ever.
|
791
|
+
#; [!wcyut] not run action and returns false if already runned.
|
792
|
+
ctx = (@__context__ ||= CONTEXT_CLASS.new)
|
793
|
+
return ctx.invoke_action(action_name, args, kwargs, once: true)
|
794
|
+
end
|
795
|
+
|
796
|
+
def run_action(action_name, *args, **kwargs)
|
797
|
+
#; [!uwi68] runs action and returns true.
|
798
|
+
ctx = (@__context__ ||= CONTEXT_CLASS.new)
|
799
|
+
return ctx.invoke_action(action_name, args, kwargs, once: false)
|
369
800
|
end
|
370
801
|
|
371
|
-
def
|
372
|
-
#; [!
|
373
|
-
|
374
|
-
|
375
|
-
method_obj = @klass.instance_method(@method)
|
376
|
-
method_obj.parameters.each {|kind, param| kw_params << param if kind == :key }
|
377
|
-
opt_keys = @schema.each.collect {|item| item.key }
|
378
|
-
key = (opt_keys - kw_params).first
|
379
|
-
return nil if key == nil
|
380
|
-
return "Should have keyword parameter '#{key}' for '@option.(#{key.inspect})', but not."
|
802
|
+
def at_end(&block)
|
803
|
+
#; [!3mqcz] registers proc object to context object.
|
804
|
+
@__context__._add_end_block(block)
|
805
|
+
nil
|
381
806
|
end
|
382
807
|
|
383
|
-
def
|
384
|
-
#; [!
|
385
|
-
|
386
|
-
|
808
|
+
def option_error(errmsg)
|
809
|
+
#; [!engp2] returns OptionError object.
|
810
|
+
return OptionError.new(errmsg)
|
811
|
+
end
|
812
|
+
|
813
|
+
def action_error(errmsg)
|
814
|
+
#; [!2m7d6] returns ActionError object.
|
815
|
+
return ActionError.new(errmsg)
|
387
816
|
end
|
388
817
|
|
389
818
|
end
|
390
819
|
|
391
820
|
|
392
|
-
|
821
|
+
Action = ActionScope
|
393
822
|
|
394
823
|
|
395
|
-
class
|
824
|
+
class Registry
|
396
825
|
|
397
|
-
def initialize(
|
398
|
-
|
399
|
-
@
|
400
|
-
@
|
401
|
-
@kwargs = kwargs
|
826
|
+
def initialize()
|
827
|
+
@metadata_dict = {} # {name => (ActionMetadata|AliasMetadata)}
|
828
|
+
@category_dict = {} # {prefix => description}
|
829
|
+
@abbrev_dict = {}
|
402
830
|
end
|
403
831
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
else
|
411
|
-
return @action_metadata.__send__(meth, *args, **kwargs)
|
412
|
-
end
|
832
|
+
def metadata_add(metadata)
|
833
|
+
! @metadata_dict.key?(metadata.name) or raise "** assertion failed: metadata.name=#{metadata.name.inspect}"
|
834
|
+
#; [!8bhxu] registers metadata with it's name as key.
|
835
|
+
@metadata_dict[metadata.name] = metadata
|
836
|
+
#; [!k07kp] returns registered metadata objet.
|
837
|
+
return metadata
|
413
838
|
end
|
414
839
|
|
415
|
-
def
|
416
|
-
|
840
|
+
def metadata_get(name)
|
841
|
+
#; [!l5m49] returns metadata object corresponding to name.
|
842
|
+
#; [!rztk2] returns nil if metadata not found for the name.
|
843
|
+
return @metadata_dict[name]
|
417
844
|
end
|
418
845
|
|
419
|
-
def
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
846
|
+
def metadata_del(name)
|
847
|
+
@metadata_dict.key?(name) or raise "** assertion failed: name=#{name.inspect}"
|
848
|
+
#; [!69vo7] deletes metadata object corresponding to name.
|
849
|
+
#; [!8vg6w] returns deleted metadata object.
|
850
|
+
return @metadata_dict.delete(name)
|
424
851
|
end
|
425
852
|
|
426
|
-
|
853
|
+
def metadata_exist?(name)
|
854
|
+
#; [!0ck5n] returns true if metadata object registered.
|
855
|
+
#; [!x7ziz] returns false if metadata object not registered.
|
856
|
+
return @metadata_dict.key?(name)
|
857
|
+
end
|
427
858
|
|
859
|
+
def metadata_each(all: true, &b)
|
860
|
+
#; [!3l6r7] returns Enumerator object if block not given.
|
861
|
+
return enum_for(:metadata_each, all: all) unless block_given?()
|
862
|
+
#; [!r8mb3] yields each metadata object if block given.
|
863
|
+
#; [!qvc77] ignores hidden metadata if `all: false` passed.
|
864
|
+
@metadata_dict.keys.sort.each do |name|
|
865
|
+
metadata = @metadata_dict[name]
|
866
|
+
yield metadata if all || ! metadata.hidden?
|
867
|
+
end
|
868
|
+
nil
|
869
|
+
end
|
428
870
|
|
429
|
-
|
871
|
+
def metadata_action2aliases(all: true)
|
872
|
+
#; [!krry6] returns a Hash object (key: action name, value: alias metadatas).
|
873
|
+
dict = {} # {action_name => [alias_metadata]}
|
874
|
+
metadata_each(all: all) do |md|
|
875
|
+
#; [!zhcm6] skips actions which has no aliases.
|
876
|
+
(dict[md.action] ||= []) << md if md.alias?
|
877
|
+
end
|
878
|
+
return dict
|
879
|
+
end
|
430
880
|
|
431
|
-
def
|
432
|
-
#; [!
|
433
|
-
#; [!
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
881
|
+
def metadata_lookup(name)
|
882
|
+
#; [!dcs9v] looks up action metadata recursively if alias name specified.
|
883
|
+
#; [!f8fqx] returns action metadata and alias args.
|
884
|
+
alias_args = []
|
885
|
+
md = metadata_get(name)
|
886
|
+
while md != nil && md.alias?
|
887
|
+
alias_args = md.args + alias_args if md.args && ! md.args.empty?
|
888
|
+
md = metadata_get(md.action)
|
439
889
|
end
|
440
|
-
|
441
|
-
sb << "\n" unless content.end_with?("\n")
|
442
|
-
return sb.join()
|
890
|
+
return md, alias_args
|
443
891
|
end
|
444
892
|
|
445
|
-
def
|
893
|
+
def category_add(prefix, desc=nil)
|
894
|
+
#; [!k27in] registers prefix if not registered yet.
|
895
|
+
#; [!xubc8] registers prefix whenever desc is not a nil.
|
896
|
+
if ! @category_dict.key?(prefix) || desc
|
897
|
+
@category_dict[prefix] = desc
|
898
|
+
end
|
446
899
|
nil
|
447
900
|
end
|
448
901
|
|
449
|
-
def
|
450
|
-
|
451
|
-
|
452
|
-
|
902
|
+
def category_add_via_action(action)
|
903
|
+
#; [!ztrfj] registers prefix of action.
|
904
|
+
#; [!31pik] do nothing if prefix already registered.
|
905
|
+
#; [!oqq7j] do nothing if action has no prefix.
|
906
|
+
if action =~ /\A(?:[-\w]+:)+/
|
907
|
+
prefix = $&
|
908
|
+
@category_dict[prefix] = nil unless @category_dict.key?(prefix)
|
909
|
+
end
|
910
|
+
nil
|
453
911
|
end
|
454
912
|
|
455
|
-
|
913
|
+
def category_exist?(prefix)
|
914
|
+
#; [!79cyx] returns true if prefix is already registered.
|
915
|
+
#; [!jx7fk] returns false if prefix is not registered yet.
|
916
|
+
return @category_dict.key?(prefix)
|
917
|
+
end
|
456
918
|
|
919
|
+
def category_each(&block)
|
920
|
+
#; [!67r3i] returns Enumerator object if block not given.
|
921
|
+
return enum_for(:category_each) unless block_given?()
|
922
|
+
#; [!g3d1z] yields block with each prefix and desc.
|
923
|
+
@category_dict.each(&block)
|
924
|
+
nil
|
925
|
+
end
|
457
926
|
|
458
|
-
|
927
|
+
def category_get_desc(prefix)
|
928
|
+
#; [!d47kq] returns description if prefix is registered.
|
929
|
+
#; [!otp1b] returns nil if prefix is not registered.
|
930
|
+
return @category_dict[prefix]
|
931
|
+
end
|
932
|
+
|
933
|
+
def category_count_actions(depth, all: false)
|
934
|
+
dict = {}
|
935
|
+
#; [!8wipx] includes prefix of hidden actions if `all: true` passed.
|
936
|
+
metadata_each(all: all) do |metadata|
|
937
|
+
name = metadata.name
|
938
|
+
next unless name =~ /:/
|
939
|
+
#; [!5n3qj] counts prefix of specified depth.
|
940
|
+
arr = name.split(':') # ex: "a:b:c:xx" -> ["a", "b", "c", "xx"]
|
941
|
+
arr.pop() # ex: ["a", "b", "c", "xx"] -> ["a", "b", "c"]
|
942
|
+
arr = arr.take(depth) if depth > 0 # ex: ["a", "b", "c"] -> ["a", "b"] (if depth==2)
|
943
|
+
prefix = arr.join(':') + ':' # ex: ["a", "b"] -> "aa:bb:"
|
944
|
+
dict[prefix] = (dict[prefix] || 0) + 1 # ex: dict["aa:bb:"] = (dict["aa:bb:"] || 0) + 1
|
945
|
+
#; [!r2frb] counts prefix of lesser depth.
|
946
|
+
while (arr.pop(); ! arr.empty?) # ex: ["a", "b"] -> ["a"]
|
947
|
+
prefix = arr.join(':') + ':' # ex: ["a"] -> "a:"
|
948
|
+
dict[prefix] ||= 0 # ex: dict["a:"] ||= 0
|
949
|
+
end
|
950
|
+
end
|
951
|
+
return dict
|
952
|
+
end
|
459
953
|
|
460
|
-
def
|
461
|
-
|
954
|
+
def abbrev_add(abbrev, prefix)
|
955
|
+
#; [!n475k] registers abbrev with prefix.
|
956
|
+
@abbrev_dict[abbrev] = prefix
|
957
|
+
nil
|
462
958
|
end
|
463
959
|
|
464
|
-
def
|
465
|
-
|
466
|
-
|
467
|
-
sb << build_usage(command, all)
|
468
|
-
sb << build_options(command, all)
|
469
|
-
sb << build_postamble(command, all)
|
470
|
-
return sb.reject {|x| x.nil? || x.empty? }.join("\n")
|
960
|
+
def abbrev_get_prefix(abbrev)
|
961
|
+
#; [!h1dvb] returns prefix bound to abbrev.
|
962
|
+
return @abbrev_dict[abbrev]
|
471
963
|
end
|
472
964
|
|
473
|
-
|
965
|
+
def abbrev_exist?(abbrev)
|
966
|
+
#; [!tjbdy] returns true/false if abbrev registered or not.
|
967
|
+
return @abbrev_dict.key?(abbrev)
|
968
|
+
end
|
474
969
|
|
475
|
-
def
|
476
|
-
#; [!
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
sb << "\n"
|
481
|
-
sb << @am.detail
|
482
|
-
sb << "\n" unless @am.detail.end_with?("\n")
|
970
|
+
def abbrev_each()
|
971
|
+
#; [!2oo4o] yields each abbrev name and prefix.
|
972
|
+
@abbrev_dict.keys.sort.each do |abbrev|
|
973
|
+
prefix = @abbrev_dict[abbrev]
|
974
|
+
yield abbrev, prefix
|
483
975
|
end
|
484
|
-
|
976
|
+
nil
|
485
977
|
end
|
486
978
|
|
487
|
-
def
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
s = build_argstr().strip()
|
494
|
-
s = "[<options>] " + s unless Util.schema_empty?(@am.schema, all)
|
495
|
-
s = s.rstrip()
|
496
|
-
sb = []
|
497
|
-
sb << (format % ["#{command} #{@am.name}", s]) << "\n"
|
498
|
-
return build_section("Usage", sb.join(), nil)
|
499
|
-
end
|
500
|
-
|
501
|
-
def build_options(command, all=false)
|
502
|
-
config = $cmdapp_config
|
503
|
-
format = config ? config.format_help : Config::FORMAT_HELP
|
504
|
-
format += "\n"
|
505
|
-
#; [!g2ju5] adds 'Options:' section.
|
506
|
-
sb = []; width = nil; indent = nil
|
507
|
-
@am.schema.each do |item|
|
508
|
-
#; [!hghuj] ignores 'Options:' section when only hidden options speicified.
|
509
|
-
next unless all || ! item.hidden?
|
510
|
-
#; [!vqqq1] hidden option should be shown in weak format.
|
511
|
-
important = item.hidden? ? false : nil
|
512
|
-
sb << Util.format_help_line(format, item.optdef, item.desc, important)
|
513
|
-
#; [!dukm7] includes detailed description of option.
|
514
|
-
if item.detail
|
515
|
-
width ||= (Util.del_escape_seq(format % ["", ""])).length
|
516
|
-
indent ||= " " * (width - 1) # `-1` means "\n"
|
517
|
-
sb << item.detail.gsub(/^/, indent)
|
518
|
-
sb << "\n" unless item.detail.end_with?("\n")
|
519
|
-
end
|
979
|
+
def abbrev_resolve(action)
|
980
|
+
#; [!n7zsy] replaces abbrev in action name with prefix.
|
981
|
+
if action =~ /\A[-\w]+:/
|
982
|
+
abbrev = $&; rest = $'
|
983
|
+
prefix = @abbrev_dict[abbrev]
|
984
|
+
return prefix + rest if prefix
|
520
985
|
end
|
521
|
-
#; [!
|
522
|
-
return nil
|
523
|
-
|
986
|
+
#; [!kdi3o] returns nil if abbrev not found in action name.
|
987
|
+
return nil
|
988
|
+
end
|
989
|
+
|
990
|
+
end
|
991
|
+
|
992
|
+
|
993
|
+
REGISTRY = Registry.new()
|
994
|
+
|
995
|
+
|
996
|
+
class BuiltInAction < ActionScope
|
997
|
+
|
998
|
+
@action.("print help message (of action if specified)")
|
999
|
+
@option.(:all, "-a, --all", "show all options, including private ones")
|
1000
|
+
def help(action=nil, all: false)
|
1001
|
+
#; [!2n99u] raises ActionError if current application is not nil.
|
1002
|
+
app = Benry::CmdApp.current_app() or
|
1003
|
+
raise ActionError.new("'help' action is available only when invoked from application.")
|
1004
|
+
#; [!g0n06] prints application help message if action name not specified.
|
1005
|
+
#; [!epj74] prints action help message if action name specified.
|
1006
|
+
str = app.render_help_message(action, all: all)
|
1007
|
+
#; [!2t43b] deletes escape characters from help message when non-color mode.
|
1008
|
+
str = Util.delete_escape_chars(str) unless Util.color_mode?
|
1009
|
+
print str
|
524
1010
|
end
|
525
1011
|
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
|
1015
|
+
class ApplicationContext
|
1016
|
+
|
1017
|
+
def initialize(config, _registry: REGISTRY)
|
1018
|
+
@config = config
|
1019
|
+
@_registry = _registry
|
1020
|
+
#@scope_objects = {} # {action_name => ActionScope}
|
1021
|
+
@status_dict = {} # {action_name => (:done|:doing)}
|
1022
|
+
@curr_action = nil # ActionMetadata
|
1023
|
+
@end_blocks = [] # [Proc]
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
def _add_end_block(block) # :nodoc:
|
1027
|
+
@end_blocks << block
|
1028
|
+
nil
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
private
|
1032
|
+
|
1033
|
+
def teardown() # :nodoc:
|
1034
|
+
#; [!4df2f] invokes end blocks in reverse order of registration.
|
1035
|
+
#; [!vskre] end block list should be cleared.
|
1036
|
+
while ! @end_blocks.empty?
|
1037
|
+
block = @end_blocks.pop()
|
1038
|
+
block.call()
|
532
1039
|
end
|
533
|
-
|
1040
|
+
#@scope_objects.each {|_, scope| scope.__clear_recursive_reference() }
|
1041
|
+
#@scope_objects.clear()
|
1042
|
+
@status_dict.clear()
|
534
1043
|
end
|
535
1044
|
|
536
|
-
|
537
|
-
|
1045
|
+
public
|
1046
|
+
|
1047
|
+
def start_action(action_name, cmdline_args) ## called from Application#run()
|
1048
|
+
#; [!2mnh7] looks up action metadata with action or alias name.
|
1049
|
+
metadata, alias_args = @_registry.metadata_lookup(action_name)
|
1050
|
+
#; [!0ukvb] raises CommandError if action nor alias not found.
|
1051
|
+
metadata != nil or
|
1052
|
+
raise CommandError.new("#{action_name}: Action nor alias not found.")
|
1053
|
+
#; [!9n46s] if alias has its own args, combines them with command-line args.
|
1054
|
+
args = alias_args + cmdline_args
|
1055
|
+
#; [!5ru31] options in alias args are also parsed as well as command-line options.
|
1056
|
+
#; [!r3gfv] raises OptionError if invalid action options specified.
|
1057
|
+
options = metadata.parse_options(args)
|
1058
|
+
#; [!lg6br] runs action with command-line arguments.
|
1059
|
+
_invoke_action(metadata, args, options, once: false)
|
1060
|
+
return nil
|
1061
|
+
ensure
|
1062
|
+
#; [!jcguj] clears instance variables.
|
1063
|
+
teardown()
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
def invoke_action(action_name, args, kwargs, once: false) ## called from ActionScope#run_action_xxxx()
|
1067
|
+
action = action_name
|
1068
|
+
#; [!uw6rq] raises ActionError if action name is not a string.
|
1069
|
+
Util.name_should_be_a_string(action, 'Action', ActionError)
|
1070
|
+
#; [!dri6e] if called from other action containing prefix, looks up action with the prefix firstly.
|
1071
|
+
metadata = nil
|
1072
|
+
if action !~ /:/ && @curr_action && @curr_action.name =~ /\A(.*:)/
|
1073
|
+
prefix = $1
|
1074
|
+
metadata = @_registry.metadata_get(prefix + action)
|
1075
|
+
action = prefix + action if metadata
|
1076
|
+
end
|
1077
|
+
#; [!ygpsw] raises ActionError if action not found.
|
1078
|
+
metadata ||= @_registry.metadata_get(action)
|
1079
|
+
metadata != nil or
|
1080
|
+
raise ActionError.new("#{action}: Action not found.")
|
1081
|
+
#; [!de6a9] raises ActionError if alias name specified.
|
1082
|
+
! metadata.alias? or
|
1083
|
+
raise ActionError.new("#{action}: Action expected, but it is an alias.")
|
1084
|
+
return _invoke_action(metadata, args, kwargs, once: once)
|
538
1085
|
end
|
539
1086
|
|
540
1087
|
private
|
541
1088
|
|
542
|
-
def
|
543
|
-
|
544
|
-
#; [!
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
1089
|
+
def _invoke_action(action_metadata, args, kwargs, once: false)
|
1090
|
+
! action_metadata.alias? or raise "** assertion failed: action_metadata=#{action_metadata.inspect}"
|
1091
|
+
#; [!ev3qh] handles help option firstly if specified.
|
1092
|
+
action = action_metadata.name
|
1093
|
+
if kwargs[:help]
|
1094
|
+
invoke_action("help", [action], {}, once: false)
|
1095
|
+
return nil
|
1096
|
+
end
|
1097
|
+
#; [!6hoir] don't run action and returns false if `once: true` specified and the action already done.
|
1098
|
+
return false if once && @status_dict[action] == :done
|
1099
|
+
#; [!xwlou] raises ActionError if looped aciton detected.
|
1100
|
+
@status_dict[action] != :doing or
|
1101
|
+
raise ActionError.new("#{action}: Looped action detected.")
|
1102
|
+
#; [!peqk8] raises ActionError if args and opts not matched to action method.
|
1103
|
+
md = action_metadata
|
1104
|
+
scope_obj = new_scope_object(md)
|
1105
|
+
errmsg = Util.validate_args_and_kwargs(scope_obj, md.meth, args, kwargs)
|
1106
|
+
errmsg == nil or
|
1107
|
+
raise ActionError.new("#{md.name}: #{errmsg}")
|
1108
|
+
#; [!kao97] action invocation is nestable.
|
1109
|
+
@status_dict[action] ||= :doing
|
1110
|
+
prev_action = @curr_action
|
1111
|
+
@curr_action = md
|
1112
|
+
#; [!5jdlh] runs action method with scope object.
|
1113
|
+
begin
|
1114
|
+
#; [!9uue9] reports enter into and exit from action if global '-T' option specified.
|
1115
|
+
c1, c2 = Util.color_mode? ? ["\e[33m", "\e[0m"] : ["", ""]
|
1116
|
+
puts "#{c1}### enter: #{md.name}#{c2}" if @config.trace_mode
|
1117
|
+
if kwargs.empty? # for Ruby < 2.7
|
1118
|
+
scope_obj.__send__(md.meth, *args) # for Ruby < 2.7
|
1119
|
+
else
|
1120
|
+
scope_obj.__send__(md.meth, *args, **kwargs)
|
557
1121
|
end
|
1122
|
+
puts "#{c1}### exit: #{md.name}#{c2}" if @config.trace_mode
|
1123
|
+
ensure
|
1124
|
+
@curr_action = prev_action
|
558
1125
|
end
|
559
|
-
|
560
|
-
|
1126
|
+
@status_dict[action] = :done
|
1127
|
+
#; [!ndxc3] returns true if action invoked.
|
1128
|
+
return true
|
561
1129
|
end
|
562
1130
|
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
1131
|
+
protected
|
1132
|
+
|
1133
|
+
def new_scope_object(action_metadata)
|
1134
|
+
#; [!1uzs3] creates new scope object.
|
1135
|
+
md = action_metadata
|
1136
|
+
scope_obj = md.klass.new(@config, self)
|
1137
|
+
#scope_obj = (@scope_objects[md.klass.name] ||= md.klass.new(@config, self))
|
1138
|
+
return scope_obj
|
570
1139
|
end
|
571
1140
|
|
572
1141
|
end
|
573
1142
|
|
574
1143
|
|
575
|
-
|
1144
|
+
CONTEXT_CLASS = ApplicationContext
|
576
1145
|
|
577
1146
|
|
578
|
-
class
|
1147
|
+
class Config
|
579
1148
|
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
1149
|
+
FORMAT_OPTION = " %-18s : %s"
|
1150
|
+
FORMAT_ACTION = " %-18s : %s"
|
1151
|
+
FORMAT_ABBREV = " %-10s => %s"
|
1152
|
+
FORMAT_USAGE = " $ %s"
|
1153
|
+
FORMAT_CATEGORY = nil # same as 'config.format_action' if nil
|
1154
|
+
DECORATION_COMMAND = "\e[1m%s\e[0m" # bold
|
1155
|
+
DECORATION_HEADER = "\e[1;34m%s\e[0m" # bold, blue
|
1156
|
+
DECORATION_EXTRA = "\e[2m%s\e[0m" # gray color
|
1157
|
+
DECORATION_STRONG = "\e[1m%s\e[0m" # bold
|
1158
|
+
DECORATION_WEAK = "\e[2m%s\e[0m" # gray color
|
1159
|
+
DECORATION_HIDDEN = "\e[2m%s\e[0m" # gray color
|
1160
|
+
DECORATION_DEBUG = "\e[2m%s\e[0m" # gray color
|
1161
|
+
DECORATION_ERROR = "\e[31m%s\e[0m" # red color
|
1162
|
+
APP_USAGE = "<action> [<arguments>...]"
|
584
1163
|
|
585
|
-
def
|
586
|
-
|
587
|
-
|
1164
|
+
def initialize(app_desc, app_version=nil,
|
1165
|
+
app_name: nil, app_command: nil, app_usage: nil, app_detail: nil,
|
1166
|
+
default_action: nil,
|
1167
|
+
help_description: nil, help_postamble: nil,
|
1168
|
+
format_option: nil, format_action: nil, format_abbrev: nil, format_usage: nil, format_category: nil,
|
1169
|
+
deco_command: nil, deco_header: nil, deco_extra: nil,
|
1170
|
+
deco_strong: nil, deco_weak: nil, deco_hidden: nil, deco_debug: nil, deco_error: nil,
|
1171
|
+
option_help: true, option_version: nil, option_list: true, option_topic: :hidden, option_all: true,
|
1172
|
+
option_verbose: false, option_quiet: false, option_color: false,
|
1173
|
+
option_debug: :hidden, option_trace: false, option_dryrun: false,
|
1174
|
+
backtrace_ignore_rexp: nil)
|
1175
|
+
#; [!pzp34] if `option_version` is not specified, then set true if `app_version` is provided.
|
1176
|
+
option_version = !! app_version if option_version == nil
|
1177
|
+
#
|
1178
|
+
@app_desc = app_desc
|
1179
|
+
@app_version = app_version
|
1180
|
+
@app_name = app_name
|
1181
|
+
@app_command = app_command || File.basename($0)
|
1182
|
+
@app_usage = app_usage
|
1183
|
+
@app_detail = app_detail
|
1184
|
+
@default_action = default_action
|
1185
|
+
@help_description = help_description
|
1186
|
+
@help_postamble = help_postamble
|
1187
|
+
@format_option = format_option || FORMAT_OPTION
|
1188
|
+
@format_action = format_action || FORMAT_ACTION
|
1189
|
+
@format_abbrev = format_abbrev || FORMAT_ABBREV
|
1190
|
+
@format_usage = format_usage || FORMAT_USAGE
|
1191
|
+
@format_category = format_category # nil means to use @format_action
|
1192
|
+
@deco_command = deco_command || DECORATION_COMMAND # for command name in help
|
1193
|
+
@deco_header = deco_header || DECORATION_HEADER # for "Usage:" or "Actions"
|
1194
|
+
@deco_extra = deco_extra || DECORATION_EXTRA # for "(default: )" or "(depth=1)"
|
1195
|
+
@deco_strong = deco_strong || DECORATION_STRONG # for `important: true`
|
1196
|
+
@deco_weak = deco_weak || DECORATION_WEAK # for `important: false`
|
1197
|
+
@deco_hidden = deco_hidden || DECORATION_HIDDEN # for `hidden: true`
|
1198
|
+
@deco_debug = deco_error || DECORATION_DEBUG
|
1199
|
+
@deco_error = deco_error || DECORATION_ERROR
|
1200
|
+
@option_help = option_help # enable or disable `-h, --help`
|
1201
|
+
@option_version = option_version # enable or disable `-V, --version`
|
1202
|
+
@option_list = option_list # enable or disable `-l, --list`
|
1203
|
+
@option_topic = option_topic # enable or disable `-L <topic>`
|
1204
|
+
@option_all = option_all # enable or disable `-a, --all`
|
1205
|
+
@option_verbose = option_verbose # enable or disable `-v, --verbose`
|
1206
|
+
@option_quiet = option_quiet # enable or disable `-q, --quiet`
|
1207
|
+
@option_color = option_color # enable or disable `--color[=<on|off>]`
|
1208
|
+
@option_debug = option_debug # enable or disable `--debug`
|
1209
|
+
@option_trace = option_trace # enable or disable `-T, --trace`
|
1210
|
+
@option_dryrun = option_dryrun # enable or disable `-X, --dryrun`
|
1211
|
+
@backtrace_ignore_rexp = backtrace_ignore_rexp
|
1212
|
+
#
|
1213
|
+
#@verobse_mode = nil
|
1214
|
+
#@quiet_mode = nil
|
1215
|
+
#@color_mode = nil
|
1216
|
+
#@debug_mode = nil
|
1217
|
+
@trace_mode = nil
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
attr_accessor :app_desc, :app_version, :app_name, :app_command, :app_usage, :app_detail
|
1221
|
+
attr_accessor :default_action
|
1222
|
+
attr_accessor :format_option, :format_action, :format_abbrev, :format_usage, :format_category
|
1223
|
+
attr_accessor :deco_command, :deco_header, :deco_extra
|
1224
|
+
attr_accessor :help_description, :help_postamble
|
1225
|
+
attr_accessor :deco_strong, :deco_weak, :deco_hidden, :deco_debug, :deco_error
|
1226
|
+
attr_accessor :option_help, :option_version, :option_list, :option_topic, :option_all
|
1227
|
+
attr_accessor :option_verbose, :option_quiet, :option_color
|
1228
|
+
attr_accessor :option_debug, :option_trace, :option_dryrun
|
1229
|
+
attr_accessor :trace_mode #, :verbose_mode, :quiet_mode, :color_mode, :debug_mode
|
1230
|
+
attr_accessor :backtrace_ignore_rexp
|
1231
|
+
alias trace_mode? trace_mode
|
1232
|
+
|
1233
|
+
def each(sort: false, &b)
|
1234
|
+
#; [!yxi7r] returns Enumerator object if block not given.
|
1235
|
+
return enum_for(:each, sort: sort) unless block_given?()
|
1236
|
+
#; [!64zkf] yields each config name and value.
|
1237
|
+
#; [!0zatj] sorts key names if `sort: true` passed.
|
1238
|
+
ivars = instance_variables()
|
1239
|
+
ivars = ivars.sort() if sort
|
1240
|
+
ivars.each do |ivar|
|
1241
|
+
val = instance_variable_get(ivar)
|
1242
|
+
yield ivar.to_s[1..-1].intern, val
|
1243
|
+
end
|
1244
|
+
nil
|
588
1245
|
end
|
589
1246
|
|
590
|
-
|
1247
|
+
end
|
591
1248
|
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
raise ActionNotFoundError.new("#{action_name}: Action not found.")
|
599
|
-
name = metadata.name
|
600
|
-
#; [!u8mit] raises error if action flow is looped.
|
601
|
-
! INDEX.action_doing?(name) or
|
602
|
-
raise LoopedActionError.new("#{name}: Action loop detected.")
|
603
|
-
#; [!vhdo9] don't invoke action twice if 'once' arg is true.
|
604
|
-
if INDEX.action_done?(name)
|
605
|
-
return INDEX.action_result(name) if once
|
606
|
-
end
|
607
|
-
#; [!r8fbn] invokes action.
|
608
|
-
INDEX.action_doing(name)
|
609
|
-
ret = metadata.run_action(*args, **kwargs)
|
610
|
-
INDEX.action_done(name, ret)
|
611
|
-
return ret
|
1249
|
+
|
1250
|
+
class BaseHelpBuilder
|
1251
|
+
|
1252
|
+
def initialize(config, _registry: REGISTRY)
|
1253
|
+
@config = config
|
1254
|
+
@_registry = _registry
|
612
1255
|
end
|
613
1256
|
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
1257
|
+
HEADER_USAGE = "Usage:"
|
1258
|
+
HEADER_DESCRIPTION = "Description:"
|
1259
|
+
HEADER_OPTIONS = "Options:"
|
1260
|
+
HEADER_ACTIONS = "Actions:"
|
1261
|
+
HEADER_ALIASES = "Aliases:"
|
1262
|
+
HEADER_ABBREVS = "Abbreviations:"
|
1263
|
+
HEADER_CATEGORIES = "Categories:"
|
1264
|
+
|
1265
|
+
def build_help_message(x, all: false)
|
1266
|
+
#; [!0hy81] this is an abstract method.
|
1267
|
+
raise NotImplementedError.new("#{self.class.name}#build_help_message(): not implemented yet.")
|
625
1268
|
end
|
626
1269
|
|
627
|
-
|
1270
|
+
protected
|
628
1271
|
|
629
|
-
def
|
630
|
-
#; [!
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
#; [!
|
650
|
-
|
651
|
-
schema.add(param, optdef, desc, *rest, type: type, rexp: rexp, enum: enum, range: range, value: value, detail: detail, important: important, tag: tag, hidden: hidden, &block)
|
652
|
-
rescue Benry::CmdOpt::SchemaError => exc
|
653
|
-
raise OptionDefError.new(exc.message)
|
654
|
-
end
|
655
|
-
end
|
656
|
-
#; [!yrkxn] @copy_options is a Proc object and copies options from other action.
|
657
|
-
@copy_options = proc do |action_name, except: nil|
|
658
|
-
#; [!mhhn2] '@copy_options.()' raises error when action not found.
|
659
|
-
metadata = INDEX.get_action(action_name) or
|
660
|
-
raise OptionDefError.new("@copy_options.(#{action_name.inspect}): Action not found.")
|
661
|
-
@__option__ ||= SCHEMA_CLASS.new
|
662
|
-
@__option__.copy_from(metadata.schema, except: except)
|
1272
|
+
def render_section(header, content, extra=nil)
|
1273
|
+
#; [!61psk] returns section string with decorating header.
|
1274
|
+
#; [!0o8w4] appends '\n' to content if it doesn't end with '\n'.
|
1275
|
+
nl = content.end_with?("\n") ? nil : "\n"
|
1276
|
+
extra = " " + decorate_extra(extra) if extra
|
1277
|
+
return "#{decorate_header(header)}#{extra}\n#{content}#{nl}"
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
def render_sections(value, item, &b)
|
1281
|
+
#; [!tqau1] returns nil if value is nil or empty.
|
1282
|
+
#; [!ezb0d] returns value unchanged if value is a string.
|
1283
|
+
#; [!gipxn] builds sections of help message if value is a hash object.
|
1284
|
+
xs = value.is_a?(Array) ? value : [value]
|
1285
|
+
sb = []
|
1286
|
+
xs.each do |x|
|
1287
|
+
case x
|
1288
|
+
when nil ; nil
|
1289
|
+
when String ; sb << (x.end_with?("\n") ? x : x + "\n")
|
1290
|
+
when Hash ; x.each {|k, v| sb << render_section(k, v) }
|
1291
|
+
else
|
1292
|
+
#; [!944rt] raises ActionError if unexpected value found in value.
|
1293
|
+
raise ActionError.new("#{x.inspect}: Unexpected value found in `#{item}`.")
|
663
1294
|
end
|
664
1295
|
end
|
1296
|
+
return sb.empty? ? nil : sb.join("\n")
|
665
1297
|
end
|
666
1298
|
|
667
|
-
def
|
668
|
-
#; [!
|
669
|
-
|
670
|
-
#; [!
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
#; [!t8vbf] raises error if action name duplicated.
|
682
|
-
! INDEX.action_exist?(name) or
|
683
|
-
raise ActionDefError.new("def #{method}(): Action '#{name}' already exist.")
|
684
|
-
INDEX.register_action(name, metadata)
|
685
|
-
#; [!jpzbi] defines same name alias of action as prefix.
|
686
|
-
#; [!997gs] not raise error when action not found.
|
687
|
-
self.__define_alias_of_action(method, name)
|
688
|
-
end
|
689
|
-
|
690
|
-
def self.__method2action(method) # :nodoc:
|
691
|
-
#; [!5e5o0] when method name is same as default action name...
|
692
|
-
if method == @__default__ # when Symbol
|
693
|
-
#; [!myj3p] uses prefix name (expect last char ':') as action name.
|
694
|
-
@__prefix__ != nil or raise "** assertion failed"
|
695
|
-
name = @__prefix__.chomp(":")
|
696
|
-
#; [!j5oto] clears '@__default__'.
|
697
|
-
@__default__ = nil
|
698
|
-
#; [!agpwh] else...
|
699
|
-
else
|
700
|
-
#; [!3icc4] uses method name as action name.
|
701
|
-
#; [!c643b] converts action name 'aa_bb_cc_' into 'aa_bb_cc'.
|
702
|
-
#; [!3fkb3] converts action name 'aa__bb__cc' into 'aa:bb:cc'.
|
703
|
-
#; [!o9s9h] converts action name 'aa_bb:_cc_dd' into 'aa-bb:_cc-dd'.
|
704
|
-
name = Util.method2action(method.to_s)
|
705
|
-
#; [!8hlni] when action name is same as default name, uses prefix as action name.
|
706
|
-
if name == @__default__ # when String
|
707
|
-
name = @__prefix__.chomp(":")
|
708
|
-
#; [!q8oxi] clears '@__default__' when default name matched to action name.
|
709
|
-
@__default__ = nil
|
710
|
-
#; [!xfent] when prefix is provided, adds it to action name.
|
711
|
-
elsif @__prefix__
|
712
|
-
name = "#{@__prefix__}#{name}"
|
1299
|
+
def render_option_help(schema, format, all: false)
|
1300
|
+
#; [!muhem] returns option part of help message.
|
1301
|
+
#; [!4z70n] includes hidden options when `all: true` passed.
|
1302
|
+
#; [!hxy1f] includes `detail:` kwarg value with indentation.
|
1303
|
+
#; [!jcqdf] returns nil if no options.
|
1304
|
+
c = @config
|
1305
|
+
sb = []
|
1306
|
+
schema.each do |x|
|
1307
|
+
next if x.hidden? && ! all
|
1308
|
+
s = format % [x.optdef, x.desc]
|
1309
|
+
if x.detail
|
1310
|
+
space = (format % ["", ""]).gsub(/\S/, " ")
|
1311
|
+
s += "\n"
|
1312
|
+
s += x.detail.chomp("\n").gsub(/^/, space)
|
713
1313
|
end
|
1314
|
+
s = decorate_str(s, x.hidden?, x.important?)
|
1315
|
+
sb << s << "\n"
|
714
1316
|
end
|
715
|
-
return
|
716
|
-
end
|
717
|
-
|
718
|
-
def
|
719
|
-
|
720
|
-
@
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
1317
|
+
return sb.empty? ? nil : sb.join()
|
1318
|
+
end
|
1319
|
+
|
1320
|
+
def decorate_command(s)
|
1321
|
+
#; [!zffx5] decorates command string.
|
1322
|
+
return @config.deco_command % s
|
1323
|
+
end
|
1324
|
+
|
1325
|
+
def decorate_header(s)
|
1326
|
+
#; [!4ufhw] decorates header string.
|
1327
|
+
return @config.deco_header % s
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
def decorate_extra(s)
|
1331
|
+
#; [!9nch4] decorates extra string.
|
1332
|
+
return @config.deco_extra % s
|
1333
|
+
end
|
1334
|
+
|
1335
|
+
def decorate_str(s, hidden, important)
|
1336
|
+
#; [!9qesd] decorates string if `hidden` is true.
|
1337
|
+
#; [!uql2d] decorates string if `important` is true.
|
1338
|
+
#; [!mdhhr] decorates string if `important` is false.
|
1339
|
+
#; [!6uzbi] not decorates string if `hidden` is falthy and `important` is nil.
|
1340
|
+
c = @config
|
1341
|
+
if hidden ; return c.deco_hidden % s
|
1342
|
+
elsif important == true ; return c.deco_strong % s
|
1343
|
+
elsif important == false ; return c.deco_weak % s
|
1344
|
+
else ; return s
|
728
1345
|
end
|
729
1346
|
end
|
730
1347
|
|
1348
|
+
def header(symbol)
|
1349
|
+
#; [!ep064] returns constant value defined in the class.
|
1350
|
+
#; [!viwtn] constant value defined in child class is prior to one defined in parent class.
|
1351
|
+
return self.class.const_get(symbol)
|
1352
|
+
end
|
1353
|
+
|
731
1354
|
end
|
732
1355
|
|
733
1356
|
|
734
|
-
|
1357
|
+
class ApplicationHelpBuilder < BaseHelpBuilder
|
735
1358
|
|
1359
|
+
def build_help_message(gschema, all: false)
|
1360
|
+
#; [!ezcs4] returns help message string of application.
|
1361
|
+
#; [!ntj2y] includes hidden actions and options if `all: true` passed.
|
1362
|
+
sb = []
|
1363
|
+
sb << section_preamble()
|
1364
|
+
sb << section_usage()
|
1365
|
+
sb << section_description()
|
1366
|
+
sb << section_options(gschema, all: all)
|
1367
|
+
sb << section_actions(true, all: all)
|
1368
|
+
#sb << section_aliases(all: all)
|
1369
|
+
#sb << section_abbrevs(all: all)
|
1370
|
+
#sb << section_categories(0, all: all)
|
1371
|
+
sb << section_postamble()
|
1372
|
+
return sb.compact().join("\n")
|
1373
|
+
end
|
736
1374
|
|
737
|
-
|
1375
|
+
protected
|
738
1376
|
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
#; [!
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
app = $cmdapp_application
|
753
|
-
msg = app.help_message(all)
|
1377
|
+
def section_preamble()
|
1378
|
+
#; [!51v42] returns preamble part of application help message.
|
1379
|
+
#; [!bmh17] includes `config.app_name` or `config.app_command` into preamble.
|
1380
|
+
#; [!opii8] includes `config.app_versoin` into preamble if it is set.
|
1381
|
+
#; [!3h380] includes `config.app_detail` into preamble if it is set.
|
1382
|
+
c = @config
|
1383
|
+
s = c.deco_command % (c.app_name || c.app_command)
|
1384
|
+
sb = []
|
1385
|
+
v = c.app_version ? (" " + c.deco_weak % "(#{c.app_version})") : ""
|
1386
|
+
sb << "#{s}#{v} --- #{c.app_desc}\n"
|
1387
|
+
if c.app_detail
|
1388
|
+
sb << "\n"
|
1389
|
+
sb << render_sections(c.app_detail, 'config.app_detail')
|
754
1390
|
end
|
755
|
-
|
756
|
-
#; [!ihr5u] prints non-colorized help message when color mode is off.
|
757
|
-
msg = Util.del_escape_seq(msg) unless Util.colorize?
|
758
|
-
print msg
|
1391
|
+
return sb.join()
|
759
1392
|
end
|
760
1393
|
|
761
|
-
|
1394
|
+
def section_postamble()
|
1395
|
+
#; [!64hj1] returns postamble of application help message.
|
1396
|
+
#; [!z5k2w] returns nil if postamble not set.
|
1397
|
+
return render_sections(@config.help_postamble, 'config.help_postamble')
|
1398
|
+
end
|
762
1399
|
|
1400
|
+
def section_usage()
|
1401
|
+
#; [!h98me] returns 'Usage:' section of application help message.
|
1402
|
+
c = @config
|
1403
|
+
s = c.deco_command % c.app_command
|
1404
|
+
s = c.format_usage % s + " [<options>] "
|
1405
|
+
#; [!i9d4r] includes `config.app_usage` into help message if it is set.
|
1406
|
+
usage = s + (c.app_usage || @config.class.const_get(:APP_USAGE))
|
1407
|
+
return render_section(header(:HEADER_USAGE), usage + "\n") # "Usage:"
|
1408
|
+
end
|
763
1409
|
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
INDEX.register_alias(alias_, Alias.new(alias_, action_, *args, important: important, tag: tag))
|
782
|
-
end
|
1410
|
+
def section_description()
|
1411
|
+
c = @config
|
1412
|
+
#; [!qarrk] returns 'Description:' section if `config.help_description` is set.
|
1413
|
+
#; [!ealol] returns nil if `config.help_description` is nil.
|
1414
|
+
return nil unless c.help_description
|
1415
|
+
return render_section(header(:HEADER_DESCRIPTION), c.help_description)
|
1416
|
+
end
|
1417
|
+
|
1418
|
+
def section_options(gschema, all: false)
|
1419
|
+
#; [!f2n70] returns 'Options:' section of application help message.
|
1420
|
+
#; [!0bboq] includes hidden options into help message if `all: true` passed.
|
1421
|
+
#; [!fjhow] returns nil if no options.
|
1422
|
+
format = @config.format_option
|
1423
|
+
s = render_option_help(gschema, format, all: all)
|
1424
|
+
return nil if s == nil
|
1425
|
+
return render_section(header(:HEADER_OPTIONS), s) # "Options:"
|
1426
|
+
end
|
783
1427
|
|
1428
|
+
public
|
784
1429
|
|
785
|
-
|
1430
|
+
def section_actions(include_aliases=true, all: false)
|
1431
|
+
c = @config
|
1432
|
+
#; [!yn8ea] includes hidden actions into help message if `all: true` passed.
|
1433
|
+
str = _render_metadata_list(c.format_action, include_aliases, all: all) {|md|
|
1434
|
+
#; [!10qp0] includes aliases if the 1st argument is true.
|
1435
|
+
! md.alias?
|
1436
|
+
}
|
1437
|
+
#; [!24by5] returns nil if no actions defined.
|
1438
|
+
return nil if str.empty?
|
1439
|
+
#; [!8qz6a] adds default action name after header if it is set.
|
1440
|
+
extra = c.default_action ? "(default: #{c.default_action})" : nil
|
1441
|
+
#; [!typ67] returns 'Actions:' section of help message.
|
1442
|
+
return render_section(header(:HEADER_ACTIONS), str, extra) # "Actions:"
|
1443
|
+
end
|
786
1444
|
|
787
|
-
def
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
1445
|
+
def _render_metadata_list(format, include_aliases=true, all: false, &filter)
|
1446
|
+
registry = @_registry
|
1447
|
+
#; [!iokkp] builds list of actions or aliases.
|
1448
|
+
sb = []
|
1449
|
+
action2aliases = nil # {action_name => [alias_metadata]}
|
1450
|
+
ali_indent = nil
|
1451
|
+
registry.metadata_each(all: all) do |metadata|
|
1452
|
+
md = metadata
|
1453
|
+
#; [!grwkj] filters by block.
|
1454
|
+
next unless yield(md)
|
1455
|
+
s = format % [md.name, md.desc]
|
1456
|
+
sb << decorate_str(s, md.hidden?, md.important?) << "\n"
|
1457
|
+
#; [!hv7or] if action has any aliases, print them below of the action.
|
1458
|
+
next if ! include_aliases
|
1459
|
+
next if md.alias?
|
1460
|
+
action2aliases ||= registry.metadata_action2aliases()
|
1461
|
+
next unless action2aliases[md.name]
|
1462
|
+
ali_str = action2aliases[md.name].collect {|alias_metadata|
|
1463
|
+
alias_metadata.name_with_args()
|
1464
|
+
}.join(", ")
|
1465
|
+
ali_indent ||= (format % ["", ""]).gsub(/[^ ]/, " ")
|
1466
|
+
s2 = "#{ali_indent}(alias: #{ali_str})"
|
1467
|
+
sb << decorate_str(s2, nil, false) << "\n"
|
1468
|
+
end
|
1469
|
+
return sb.join()
|
793
1470
|
end
|
1471
|
+
private :_render_metadata_list
|
794
1472
|
|
795
|
-
|
1473
|
+
def section_availables(include_aliases=true, all: false)
|
1474
|
+
#; [!pz0cu] includes 'Actions:' and 'Aliases:' sections.
|
1475
|
+
s1 = section_actions(include_aliases, all: all)
|
1476
|
+
s2 = section_aliases(all: all)
|
1477
|
+
return [s1, s2].compact.join("\n")
|
1478
|
+
end
|
796
1479
|
|
797
|
-
def
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
1480
|
+
def section_candidates(prefix, all: false)
|
1481
|
+
c = @config
|
1482
|
+
registry = @_registry
|
1483
|
+
#; [!idm2h] includes hidden actions when `all: true` passed.
|
1484
|
+
prefix2 = prefix.chomp(':')
|
1485
|
+
str = _render_metadata_list(c.format_action, all: all) {|metadata|
|
1486
|
+
#; [!duhyd] includes actions which name is same as prefix.
|
1487
|
+
#; [!nwwrd] if prefix is 'xxx:' and alias name is 'xxx' and action name of alias matches to 'xxx:', skip it because it will be shown in 'Aliases:' section.
|
1488
|
+
_category_action?(metadata, prefix)
|
1489
|
+
}
|
1490
|
+
#s1 = str.empty? ? nil : render_section(header(:HEADER_ACTIONS), str)
|
1491
|
+
s1 = render_section(header(:HEADER_ACTIONS), str)
|
1492
|
+
#; [!otvbt] includes name of alias which corresponds to action starting with prefix.
|
1493
|
+
#; [!h5ek7] includes hidden aliases when `all: true` passed.
|
1494
|
+
str = _render_metadata_list(c.format_action, all: all) {|metadata|
|
1495
|
+
metadata.alias? && metadata.action.start_with?(prefix)
|
1496
|
+
}
|
1497
|
+
#; [!9lnn2] alias names in candidate list are sorted by action name.
|
1498
|
+
str = str.each_line.sort_by {|line|
|
1499
|
+
line =~ /'([^']+)'/
|
1500
|
+
[$1, line]
|
1501
|
+
}.join()
|
1502
|
+
#; [!80t51] alias names are displayed in separated section from actions.
|
1503
|
+
s2 = str.empty? ? nil : render_section(header(:HEADER_ALIASES), str)
|
1504
|
+
#; [!rqx7w] returns header string if both no actions nor aliases found with names starting with prefix.
|
1505
|
+
#; [!3c3f1] returns list of actions which name starts with prefix specified.
|
1506
|
+
return [s1, s2].compact().join("\n")
|
1507
|
+
end
|
1508
|
+
|
1509
|
+
def _category_action?(md, prefix)
|
1510
|
+
return true if md.name.start_with?(prefix)
|
1511
|
+
return false if md.name != prefix.chomp(':')
|
1512
|
+
return true if ! md.alias?
|
1513
|
+
return false if md.action.start_with?(prefix)
|
1514
|
+
return true
|
1515
|
+
end
|
1516
|
+
private :_category_action?
|
1517
|
+
|
1518
|
+
def section_aliases(all: false)
|
1519
|
+
registry = @_registry
|
1520
|
+
sb = []
|
1521
|
+
format = @config.format_action
|
1522
|
+
#; [!d7vee] ignores hidden aliases in default.
|
1523
|
+
#; [!4vvrs] include hidden aliases if `all: true` specifieid.
|
1524
|
+
#; [!v211d] sorts aliases by action names.
|
1525
|
+
registry.metadata_each(all: all).select {|md| md.alias? }.sort_by {|md| [md.action, md.name] }.each do |md|
|
1526
|
+
s = format % [md.name, md.desc]
|
1527
|
+
sb << decorate_str(s, md.hidden?, md.important?) << "\n"
|
1528
|
+
end
|
1529
|
+
#; [!fj1c7] returns nil if no aliases found.
|
1530
|
+
return nil if sb.empty?
|
1531
|
+
#; [!496qq] renders alias list.
|
1532
|
+
return render_section(header(:HEADER_ALIASES), sb.join()) # "Aliases:"
|
1533
|
+
end
|
1534
|
+
|
1535
|
+
def section_abbrevs(all: false)
|
1536
|
+
registry = @_registry
|
1537
|
+
format = @config.format_abbrev
|
1538
|
+
_ = all # not used
|
1539
|
+
sb = []
|
1540
|
+
registry.abbrev_each do |abbrev, prefix|
|
1541
|
+
sb << format % [abbrev, prefix] << "\n"
|
802
1542
|
end
|
1543
|
+
#; [!dnt12] returns nil if no abbrevs found.
|
1544
|
+
return nil if sb.empty?
|
1545
|
+
#; [!00ice] returns abbrev list string.
|
1546
|
+
return render_section(header(:HEADER_ABBREVS), sb.join()) # "Abbreviations:"
|
803
1547
|
end
|
804
1548
|
|
805
|
-
def
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
#; [!
|
810
|
-
|
811
|
-
|
1549
|
+
def section_categories(depth=0, all: false)
|
1550
|
+
registry = @_registry
|
1551
|
+
c = @config
|
1552
|
+
#; [!30l2j] includes number of actions per prefix.
|
1553
|
+
#; [!alteh] includes prefix of hidden actions if `all: true` passed.
|
1554
|
+
dict = registry.category_count_actions(depth, all: all)
|
1555
|
+
#registry.category_each {|prefix, _| dict[prefix] = 0 unless dict.key?(prefix) }
|
1556
|
+
#; [!p4j1o] returns nil if no prefix found.
|
1557
|
+
return nil if dict.empty?
|
1558
|
+
#; [!k3y6q] uses `config.format_category` or `config.format_action`.
|
1559
|
+
format = (c.format_category || c.format_action) + "\n"
|
1560
|
+
indent = /^( *)/.match(format)[1]
|
1561
|
+
str = dict.keys.sort.collect {|prefix|
|
1562
|
+
s = "#{prefix} (#{dict[prefix]})"
|
1563
|
+
#; [!qxoja] includes category description if registered.
|
1564
|
+
desc = registry.category_get_desc(prefix)
|
1565
|
+
desc ? (format % [s, desc]) : "#{indent}#{s}\n"
|
1566
|
+
}.join()
|
1567
|
+
#; [!crbav] returns top prefix list.
|
1568
|
+
return render_section(header(:HEADER_CATEGORIES), str, "(depth=#{depth})") # "Categories:"
|
812
1569
|
end
|
813
1570
|
|
814
1571
|
end
|
815
1572
|
|
816
1573
|
|
817
|
-
class
|
1574
|
+
class ActionHelpBuilder < BaseHelpBuilder
|
818
1575
|
|
819
|
-
|
820
|
-
|
821
|
-
|
1576
|
+
def build_help_message(metadata, all: false)
|
1577
|
+
#; [!f3436] returns help message of an action.
|
1578
|
+
#; [!8acs1] includes hidden options if `all: true` passed.
|
1579
|
+
#; [!mtvw8] includes 'Aliases:' section if action has any aliases.
|
1580
|
+
#; [!vcg9w] not include 'Options:' section if action has no options.
|
1581
|
+
#; [!1auu5] not include '[<options>]' in 'Usage:'section if action has no options.
|
1582
|
+
sb = []
|
1583
|
+
sb << section_preamble(metadata)
|
1584
|
+
sb << section_usage(metadata, all: all)
|
1585
|
+
sb << section_description(metadata)
|
1586
|
+
sb << section_options(metadata, all: all)
|
1587
|
+
sb << section_aliases(metadata, all: all)
|
1588
|
+
sb << section_postamble(metadata)
|
1589
|
+
return sb.compact().join("\n")
|
1590
|
+
end
|
822
1591
|
|
823
|
-
|
1592
|
+
protected
|
824
1593
|
|
825
|
-
|
826
|
-
|
827
|
-
|
1594
|
+
def section_preamble(metadata)
|
1595
|
+
#; [!a6nk4] returns preamble of action help message.
|
1596
|
+
#; [!imxdq] includes `config.app_command`, not `config.app_name`, into preamble.
|
1597
|
+
#; [!7uy4f] includes `detail:` kwarg value of `@action.()` if specified.
|
1598
|
+
md = metadata
|
1599
|
+
sb = []
|
1600
|
+
c = @config
|
1601
|
+
s = c.deco_command % "#{c.app_command} #{md.name}"
|
1602
|
+
sb << "#{s} --- #{md.desc}\n"
|
1603
|
+
if md.detail
|
1604
|
+
sb << "\n"
|
1605
|
+
sb << render_sections(md.detail, '@action.(detail: ...)')
|
1606
|
+
end
|
1607
|
+
return sb.join()
|
1608
|
+
end
|
828
1609
|
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
1610
|
+
def section_usage(metadata, all: false)
|
1611
|
+
md = metadata
|
1612
|
+
c = @config
|
1613
|
+
s = c.deco_command % "#{c.app_command} #{md.name}"
|
1614
|
+
s = c.format_usage % s
|
1615
|
+
#; [!jca5d] not add '[<options>]' if action has no options.
|
1616
|
+
s += " [<options>]" unless md.option_empty?(all: all)
|
1617
|
+
#; [!h5bp4] if `usage:` kwarg specified in `@action.()`, use it as usage string.
|
1618
|
+
if md.usage != nil
|
1619
|
+
#; [!nfuxz] `usage:` kwarg can be a string or an array of string.
|
1620
|
+
sb = [md.usage].flatten.collect {|x| "#{s} #{x}\n" }
|
1621
|
+
#; [!z3lh9] if `usage:` kwarg not specified in `@action.()`, generates usage string from method parameters.
|
1622
|
+
else
|
1623
|
+
sb = [s]
|
1624
|
+
sb << Util.method2help(md.klass.new(c), md.meth) << "\n"
|
1625
|
+
end
|
1626
|
+
#; [!iuctx] returns 'Usage:' section of action help message.
|
1627
|
+
return render_section(header(:HEADER_USAGE), sb.join()) # "Usage:"
|
1628
|
+
end
|
834
1629
|
|
835
|
-
def
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
#; [!
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
1630
|
+
def section_description(metadata)
|
1631
|
+
#; [!zeujz] returns 'Description:' section if action description is set.
|
1632
|
+
#; [!0zffw] returns nil if action description is nil.
|
1633
|
+
md = metadata
|
1634
|
+
return nil unless md.description
|
1635
|
+
return render_section(header(:HEADER_DESCRIPTION), md.description)
|
1636
|
+
end
|
1637
|
+
|
1638
|
+
def section_options(metadata, all: false)
|
1639
|
+
#; [!pafgs] returns 'Options:' section of help message.
|
1640
|
+
#; [!85wus] returns nil if action has no options.
|
1641
|
+
format = @config.format_option
|
1642
|
+
s = render_option_help(metadata.schema, format, all: all)
|
1643
|
+
return nil if s == nil
|
1644
|
+
return render_section(header(:HEADER_OPTIONS), s) # "Options:"
|
1645
|
+
end
|
1646
|
+
|
1647
|
+
def section_aliases(metadata, all: false)
|
1648
|
+
#; [!kjpt9] returns 'Aliases:' section of help message.
|
1649
|
+
#; [!cjr0q] returns nil if action has no options.
|
1650
|
+
format = @config.format_action
|
1651
|
+
registry = @_registry
|
1652
|
+
action_name = metadata.name
|
1653
|
+
sb = []
|
1654
|
+
registry.metadata_each(all: all) do |md|
|
1655
|
+
next unless md.alias?
|
1656
|
+
action_md, args = registry.metadata_lookup(md.name)
|
1657
|
+
next unless action_md
|
1658
|
+
next unless action_md.name == action_name
|
1659
|
+
desc = "alias for '#{([action_name] + args).join(' ')}'"
|
1660
|
+
s = format % [md.name, desc]
|
1661
|
+
#s = format % [md.name, md.desc]
|
1662
|
+
sb << decorate_str(s, md.hidden?, md.important?) << "\n"
|
1663
|
+
end
|
1664
|
+
return nil if sb.empty?
|
1665
|
+
return render_section(header(:HEADER_ALIASES), sb.join()) # "Aliases:"
|
1666
|
+
end
|
1667
|
+
|
1668
|
+
def section_postamble(metadata)
|
1669
|
+
#; [!q1jee] returns postamble of help message if `postamble:` kwarg specified in `@action.()`.
|
1670
|
+
#; [!jajse] returns nil if postamble is not set.
|
1671
|
+
return render_sections(metadata.postamble, '@action.(postamble: "...")')
|
1672
|
+
end
|
878
1673
|
|
879
1674
|
end
|
880
1675
|
|
881
1676
|
|
882
|
-
|
1677
|
+
APPLICATION_HELP_BUILDER_CLASS = ApplicationHelpBuilder
|
1678
|
+
ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
|
1679
|
+
|
1680
|
+
|
1681
|
+
class GlobalOptionSchema < OptionSchema
|
883
1682
|
|
884
|
-
def initialize(config
|
1683
|
+
def initialize(config)
|
885
1684
|
super()
|
886
|
-
|
1685
|
+
setup(config)
|
1686
|
+
end
|
1687
|
+
|
1688
|
+
def setup(config)
|
1689
|
+
#; [!umjw5] add nothing if config is nil.
|
1690
|
+
return if ! config
|
1691
|
+
#; [!ppcvp] adds options according to config object.
|
887
1692
|
c = config
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
#; [!
|
910
|
-
|
911
|
-
|
1693
|
+
topics = ["action", "actions", "alias", "aliases",
|
1694
|
+
"category", "categories", "abbrev", "abbrevs",
|
1695
|
+
"category1", "categories1", "category2", "categories2",
|
1696
|
+
"category3", "categories3", "category4", "categories4",
|
1697
|
+
"metadata"]
|
1698
|
+
_add(c, :help , "-h, --help" , "print help message (of action if specified)")
|
1699
|
+
_add(c, :version, "-V, --version", "print version")
|
1700
|
+
_add(c, :list , "-l, --list" , "list actions and aliases")
|
1701
|
+
_add(c, :topic , "-L <topic>" , "topic list (actions|aliases|categories|abbrevs)", enum: topics)
|
1702
|
+
_add(c, :all , "-a, --all" , "list hidden actions/options, too")
|
1703
|
+
_add(c, :verbose, "-v, --verbose", "verbose mode")
|
1704
|
+
_add(c, :quiet , "-q, --quiet" , "quiet mode")
|
1705
|
+
_add(c, :color , "--color[=<on|off>]", "color mode", type: TrueClass)
|
1706
|
+
_add(c, :debug , " --debug" , "debug mode")
|
1707
|
+
_add(c, :trace , "-T, --trace" , "trace mode")
|
1708
|
+
_add(c, :dryrun , "-X, --dryrun" , "dry-run mode (not run; just echoback)")
|
1709
|
+
end
|
1710
|
+
|
1711
|
+
def _add(c, key, optstr, desc, type: nil, enum: nil)
|
1712
|
+
flag = c.__send__("option_#{key}")
|
1713
|
+
return unless flag
|
1714
|
+
#; [!doj0k] if config option is `:hidden`, makes option as hidden.
|
1715
|
+
if flag == :hidden
|
1716
|
+
hidden = true
|
1717
|
+
optstr = optstr.sub(/^-\w, /, " ") # ex: "-T, --trace" -> " --trace"
|
1718
|
+
else
|
1719
|
+
hidden = nil
|
1720
|
+
end
|
1721
|
+
add(key, optstr, desc, hidden: hidden, type: type, enum: enum)
|
1722
|
+
end
|
1723
|
+
private :_add
|
1724
|
+
|
1725
|
+
def reorder_options(*keys)
|
1726
|
+
#; [!2cp9s] sorts options in order of keys specified.
|
1727
|
+
#; [!xe7e1] moves options which are not included in specified keys to end of option list.
|
1728
|
+
n = @items.length
|
1729
|
+
@items.sort_by! {|item| keys.index(item.key) || @items.index(item) + n }
|
912
1730
|
nil
|
913
1731
|
end
|
914
1732
|
|
915
1733
|
end
|
916
1734
|
|
917
|
-
|
918
|
-
|
1735
|
+
GLOBAL_OPTION_SCHEMA_CLASS = GlobalOptionSchema
|
1736
|
+
GLOBAL_OPTION_PARSER_CLASS = OptionParser
|
919
1737
|
|
920
1738
|
|
921
1739
|
class Application
|
922
1740
|
|
923
|
-
def initialize(config,
|
924
|
-
@config
|
925
|
-
|
926
|
-
@
|
927
|
-
|
928
|
-
@
|
929
|
-
|
930
|
-
|
1741
|
+
def initialize(config, global_option_schema=nil, app_help_builder=nil, action_help_builder=nil, _registry: REGISTRY)
|
1742
|
+
@config = config
|
1743
|
+
@option_schema = global_option_schema || GLOBAL_OPTION_SCHEMA_CLASS.new(config)
|
1744
|
+
@_registry = _registry
|
1745
|
+
@app_help_builder = app_help_builder
|
1746
|
+
@action_help_builder = action_help_builder
|
1747
|
+
end
|
1748
|
+
|
1749
|
+
attr_reader :config, :option_schema
|
1750
|
+
|
1751
|
+
def inspect()
|
1752
|
+
return super.split().first() + ">"
|
1753
|
+
end
|
1754
|
+
|
1755
|
+
def main(argv=ARGV)
|
1756
|
+
#; [!65e9n] returns `0` as status code.
|
1757
|
+
status_code = run(*argv)
|
1758
|
+
return status_code
|
1759
|
+
#rescue Benry::CmdOpt::OptionError => exc
|
1760
|
+
# raise if $DEBUG_MODE
|
1761
|
+
# print_error(exc)
|
1762
|
+
# return 1
|
1763
|
+
#; [!bkbb4] when error raised...
|
1764
|
+
rescue StandardError => exc
|
1765
|
+
#; [!k4qov] not catch error if debug mode is enabled.
|
1766
|
+
raise if $DEBUG_MODE
|
1767
|
+
#; [!lhlff] catches error if BaseError raised or `should_rescue?()` returns true.
|
1768
|
+
raise if ! should_rescue?(exc)
|
1769
|
+
#; [!35x5p] prints error into stderr.
|
1770
|
+
print_error(exc)
|
1771
|
+
#; [!z39bh] prints backtrace unless error is a CommandError.
|
1772
|
+
print_backtrace(exc) if ! exc.is_a?(BaseError) || exc.should_report_backtrace?()
|
1773
|
+
#; [!dzept] returns `1` as status code.
|
1774
|
+
return 1
|
931
1775
|
end
|
932
1776
|
|
933
|
-
|
1777
|
+
def run(*args)
|
1778
|
+
#; [!etbbc] calls setup method at beginning of this method.
|
1779
|
+
setup()
|
1780
|
+
#; [!hguvb] handles global options.
|
1781
|
+
global_opts = parse_global_options(args) # raises OptionError
|
1782
|
+
toggle_global_options(global_opts)
|
1783
|
+
status_code = handle_global_options(global_opts, args)
|
1784
|
+
return status_code if status_code
|
1785
|
+
return handle_action(args, global_opts)
|
1786
|
+
ensure
|
1787
|
+
#; [!pf1d2] calls teardown method at end of this method.
|
1788
|
+
teardown()
|
1789
|
+
end
|
934
1790
|
|
935
|
-
def
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
#; [!a7d4w] prints error message with '[ERROR]' prompt.
|
943
|
-
$stderr.puts "\033[0;31m[ERROR]\033[0m #{exc.message}"
|
944
|
-
#; [!r7opi] prints filename and line number on where error raised if DefinitionError.
|
945
|
-
if exc.is_a?(DefinitionError)
|
946
|
-
#; [!v0zrf] error location can be filtered by block.
|
947
|
-
if block_given?()
|
948
|
-
loc = exc.backtrace_locations.find(&block)
|
949
|
-
else
|
950
|
-
loc = exc.backtrace_locations.find {|x| x.path != __FILE__ }
|
951
|
-
end
|
952
|
-
raise unless loc
|
953
|
-
$stderr.puts "\t(file: #{loc.path}, line: #{loc.lineno})"
|
1791
|
+
def handle_action(args, global_opts)
|
1792
|
+
#; [!3qw3p] when no arguments specified...
|
1793
|
+
if args.empty?
|
1794
|
+
#; [!zl9em] lists actions if default action is not set.
|
1795
|
+
#; [!89hqb] lists all actions including hidden ones if `-a` or `--all` specified.
|
1796
|
+
if @config.default_action == nil
|
1797
|
+
return handle_blank_action(all: global_opts[:all])
|
954
1798
|
end
|
955
|
-
#; [!
|
956
|
-
|
1799
|
+
#; [!k4xxp] runs default action if it is set.
|
1800
|
+
action = @config.default_action
|
1801
|
+
#; [!xaamy] when prefix specified...
|
1802
|
+
elsif args[0].end_with?(':')
|
1803
|
+
#; [!7l3fh] lists actions starting with prefix.
|
1804
|
+
#; [!g0k1g] lists all actions including hidden ones if `-a` or `--all` specified.
|
1805
|
+
prefix = args.shift()
|
1806
|
+
return handle_prefix(prefix, all: global_opts[:all])
|
1807
|
+
#; [!vphz3] else...
|
957
1808
|
else
|
958
|
-
#; [!
|
959
|
-
|
1809
|
+
#; [!bq39a] runs action with arguments.
|
1810
|
+
action = args.shift()
|
960
1811
|
end
|
1812
|
+
#; [!5yd8x] returns 0 when action invoked successfully.
|
1813
|
+
return start_action(action, args)
|
961
1814
|
end
|
1815
|
+
protected :handle_action
|
962
1816
|
|
963
|
-
def
|
964
|
-
#; [!
|
965
|
-
|
966
|
-
#; [!
|
967
|
-
|
968
|
-
|
969
|
-
#; [!go9kk] sets global variables according to global options.
|
970
|
-
do_toggle_global_switches(args, global_opts)
|
971
|
-
#; [!pbug7] skip actions if callback method throws `:SKIP`.
|
972
|
-
skip_action = true
|
973
|
-
catch :SKIP do
|
974
|
-
#; [!5iczl] skip actions if help option or version option specified.
|
975
|
-
do_handle_global_options(args, global_opts)
|
976
|
-
#; [!w584g] calls callback method.
|
977
|
-
do_callback(args, global_opts)
|
978
|
-
skip_action = false
|
979
|
-
end
|
980
|
-
return if skip_action
|
981
|
-
#; [!avxos] prints candidate actions if action name ends with ':'.
|
982
|
-
#; [!eeh0y] candidates are not printed if 'config.feat_candidate' is false.
|
983
|
-
if ! args.empty? && args[0].end_with?(':') && @config.feat_candidate
|
984
|
-
do_print_candidates(args, global_opts)
|
985
|
-
return
|
986
|
-
end
|
987
|
-
#; [!agfdi] reports error when action not found.
|
988
|
-
#; [!o5i3w] reports error when default action not found.
|
989
|
-
#; [!n60o0] reports error when action nor default action not specified.
|
990
|
-
#; [!7h0ku] prints help if no action but 'config.default_help' is true.
|
991
|
-
#; [!l0g1l] skip actions if no action specified and 'config.default_help' is set.
|
992
|
-
metadata = do_find_action(args, global_opts)
|
993
|
-
if metadata == nil
|
994
|
-
do_print_help_message([], global_opts)
|
995
|
-
do_validate_actions(args, global_opts)
|
996
|
-
return
|
997
|
-
end
|
998
|
-
#; [!x1xgc] run action with options and arguments.
|
999
|
-
#; [!v5k56] runs default action if action not specified.
|
1000
|
-
do_run_action(metadata, args, global_opts)
|
1001
|
-
rescue => exc
|
1002
|
-
raise
|
1003
|
-
ensure
|
1004
|
-
#; [!hk6iu] unsets $cmdapp_config at end.
|
1005
|
-
#; [!wv22u] calls teardown method at end of running action.
|
1006
|
-
#; [!dhba4] calls teardown method even if exception raised.
|
1007
|
-
do_teardown(exc)
|
1817
|
+
def render_help_message(action=nil, all: false)
|
1818
|
+
#; [!2oax5] returns action help message if action name is specified.
|
1819
|
+
#; [!d6veb] returns application help message if action name is not specified.
|
1820
|
+
#; [!tf2wp] includes hidden actions and options into help message if `all: true` passed.
|
1821
|
+
return render_action_help(action, all: all) if action
|
1822
|
+
return render_application_help(all: all)
|
1008
1823
|
end
|
1009
1824
|
|
1010
1825
|
protected
|
1011
1826
|
|
1012
|
-
def
|
1013
|
-
#; [!
|
1014
|
-
|
1827
|
+
def setup()
|
1828
|
+
#; [!6hi1y] stores current application.
|
1829
|
+
Benry::CmdApp._set_current_app(self)
|
1015
1830
|
end
|
1016
1831
|
|
1017
|
-
def
|
1018
|
-
#; [!
|
1019
|
-
|
1832
|
+
def teardown()
|
1833
|
+
#; [!t44mv] removes current applicatin from data store.
|
1834
|
+
Benry::CmdApp._set_current_app(nil)
|
1020
1835
|
end
|
1021
1836
|
|
1022
|
-
def
|
1023
|
-
#; [!
|
1024
|
-
parser =
|
1025
|
-
global_opts = parser.parse(args, all: false)
|
1837
|
+
def parse_global_options(args)
|
1838
|
+
#; [!9c9r8] parses global options.
|
1839
|
+
parser = GLOBAL_OPTION_PARSER_CLASS.new(@option_schema)
|
1840
|
+
global_opts = parser.parse(args, all: false) # raises OptionError
|
1026
1841
|
return global_opts
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
#; [!p1l1i] sets $QUIET_MODE to true if '-q' or '--quiet' specified.
|
1035
|
-
#; [!2zvf9] sets $COLOR_MODE to true/false according to '--color' option.
|
1036
|
-
#; [!ywl1a] sets $DEBUG_MODE to true if '-D' or '--debug' specified.
|
1037
|
-
#; [!8trmz] sets $TRACE_MODE to true if '-T' or '--trace' specified.
|
1038
|
-
global_opts.each do |key, val|
|
1039
|
-
case key
|
1040
|
-
when :verbose ; $QUIET_MODE = ! val
|
1041
|
-
when :quiet ; $QUIET_MODE = val
|
1042
|
-
when :color ; $COLOR_MODE = val
|
1043
|
-
when :debug ; $DEBUG_MODE = val
|
1044
|
-
when :trace ; $TRACE_MODE = val
|
1045
|
-
else ; # do nothing
|
1046
|
-
end
|
1842
|
+
end
|
1843
|
+
|
1844
|
+
def toggle_global_options(global_opts)
|
1845
|
+
#; [!xwcyl] sets `$VERBOSE_MODE` and `$QUIET_MODE` according to global options.
|
1846
|
+
d = global_opts
|
1847
|
+
if d[:verbose] ; $VERBOSE_MODE = true ; $QUIET_MODE = false
|
1848
|
+
elsif d[:quiet] ; $VERBOSE_MODE = false; $QUIET_MODE = true
|
1047
1849
|
end
|
1850
|
+
#; [!510eb] sets `$COLOR_MODE` according to global option.
|
1851
|
+
$COLOR_MODE = d[:color] if d[:color] != nil
|
1852
|
+
#; [!sucqp] sets `$DEBUG_MODE` according to global options.
|
1853
|
+
$DEBUG_MODE = d[:debug] if d[:debug] != nil
|
1854
|
+
#; [!y9fow] sets `config.trace_mode` if global option specified.
|
1855
|
+
@config.trace_mode = d[:trace] if d[:trace] != nil
|
1856
|
+
#; [!dply7] sets `$DRYRUN_MODE` according to global option.
|
1857
|
+
$DRYRUN_MODE = d[:dryrun] if d[:dryrun] != nil
|
1858
|
+
nil
|
1048
1859
|
end
|
1049
1860
|
|
1050
|
-
def
|
1051
|
-
|
1052
|
-
#; [!
|
1861
|
+
def handle_global_options(global_opts, args)
|
1862
|
+
all = global_opts[:all]
|
1863
|
+
#; [!366kv] prints help message if global option `-h, --help` specified.
|
1864
|
+
#; [!7mapy] includes hidden actions into help message if `-a, --all` specified.
|
1053
1865
|
if global_opts[:help]
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1866
|
+
action = args.empty? ? nil : args[0]
|
1867
|
+
print_str render_action_help(action, all: all) if action
|
1868
|
+
print_str render_application_help(all: all) unless action
|
1869
|
+
return 0
|
1057
1870
|
end
|
1058
|
-
#; [!
|
1871
|
+
#; [!dkjw8] prints version number if global option `-V, --version` specified.
|
1059
1872
|
if global_opts[:version]
|
1060
|
-
|
1061
|
-
|
1873
|
+
print_str render_version()
|
1874
|
+
return 0
|
1062
1875
|
end
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
if @callback && ! @__called
|
1069
|
-
@__called = true
|
1070
|
-
@callback.call(args, global_opts, @config)
|
1876
|
+
#; [!hj4hf] prints action and alias list if global option `-l, --list` specified.
|
1877
|
+
#; [!tyxwo] includes hidden actions into action list if `-a, --all` specified.
|
1878
|
+
if global_opts[:list]
|
1879
|
+
print_str render_item_list(nil, all: all)
|
1880
|
+
return 0
|
1071
1881
|
end
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1882
|
+
#; [!ooiaf] prints topic list if global option '-L <topic>' specified.
|
1883
|
+
#; [!ymifi] includes hidden actions into topic list if `-a, --all` specified.
|
1884
|
+
if global_opts[:topic]
|
1885
|
+
print_str render_topic_list(global_opts[:topic], all: all)
|
1886
|
+
return 0
|
1887
|
+
end
|
1888
|
+
#; [!k31ry] returns `0` if help or version or actions printed.
|
1889
|
+
#; [!9agnb] returns `nil` if do nothing.
|
1890
|
+
return nil # do action
|
1891
|
+
end
|
1892
|
+
|
1893
|
+
def render_action_help(action, all: false)
|
1894
|
+
#; [!c510c] returns action help message.
|
1895
|
+
metadata, _alias_args = @_registry.metadata_lookup(action)
|
1896
|
+
metadata or
|
1897
|
+
raise CommandError.new("#{action}: Action not found.")
|
1898
|
+
builder = get_action_help_builder()
|
1899
|
+
return builder.build_help_message(metadata, all: all)
|
1900
|
+
end
|
1901
|
+
|
1902
|
+
def render_application_help(all: false)
|
1903
|
+
#; [!iyxxb] returns application help message.
|
1904
|
+
builder = get_app_help_builder()
|
1905
|
+
return builder.build_help_message(@option_schema, all: all)
|
1906
|
+
end
|
1907
|
+
|
1908
|
+
def render_version()
|
1909
|
+
#; [!bcp2g] returns version number string.
|
1910
|
+
return (@config.app_version || "?.?.?") + "\n"
|
1911
|
+
end
|
1912
|
+
|
1913
|
+
def render_item_list(prefix=nil, all: false)
|
1914
|
+
builder = get_app_help_builder()
|
1915
|
+
case prefix
|
1916
|
+
#; [!tftl5] when prefix is not specified...
|
1917
|
+
when nil
|
1918
|
+
#; [!36vz6] returns action list string if any actions defined.
|
1919
|
+
#; [!znuy4] raises CommandError if no actions defined.
|
1920
|
+
s = builder.section_availables(all: all) or
|
1921
|
+
raise CommandError.new("No actions defined.")
|
1922
|
+
return s
|
1923
|
+
#; [!jcq4z] when separator is specified...
|
1924
|
+
when /\A:+\z/
|
1925
|
+
#; [!w1j1e] returns top prefix list if ':' specified.
|
1926
|
+
#; [!bgput] returns two depth prefix list if '::' specified.
|
1927
|
+
#; [!tiihg] raises CommandError if no actions found having prefix.
|
1928
|
+
depth = prefix.length
|
1929
|
+
s = builder.section_categories(depth, all: all) or
|
1930
|
+
raise CommandError.new("Prefix of actions not found.")
|
1931
|
+
return s
|
1932
|
+
#; [!xut9o] when prefix is specified...
|
1933
|
+
when /:\z/
|
1934
|
+
#; [!z4dqn] filters action list by prefix if specified.
|
1935
|
+
#; [!1834c] raises CommandError if no actions found with names starting with that prefix.
|
1936
|
+
s = builder.section_candidates(prefix, all: all) or
|
1937
|
+
raise CommandError.new("No actions found with names starting with '#{prefix}'.")
|
1938
|
+
return s
|
1939
|
+
#; [!xjdrm] else...
|
1093
1940
|
else
|
1094
|
-
|
1941
|
+
#; [!9r4w9] raises ArgumentError.
|
1942
|
+
raise ArgumentError.new("#{prefix.inspect}: Invalid value as a prefix.")
|
1095
1943
|
end
|
1096
|
-
return metadata
|
1097
1944
|
end
|
1098
1945
|
|
1099
|
-
def
|
1100
|
-
|
1101
|
-
#; [!
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
raise
|
1112
|
-
elsif max && max < n
|
1113
|
-
raise CommandError.new("#{action_name}: Too much arguments (at most #{max}).")
|
1114
|
-
end
|
1115
|
-
#; [!cf45e] runs action with arguments and options.
|
1116
|
-
#; [!tsal4] detects looped action.
|
1117
|
-
INDEX.action_doing(action_name)
|
1118
|
-
ret = metadata.run_action(*args, **options)
|
1119
|
-
INDEX.action_done(action_name, ret)
|
1120
|
-
return ret
|
1121
|
-
end
|
1122
|
-
|
1123
|
-
def do_print_help_message(args, global_opts)
|
1124
|
-
#; [!4qs7y] shows private (hidden) actions/options if '--all' option specified.
|
1125
|
-
#; [!l4d6n] `all` flag should be true or false, not nil.
|
1126
|
-
all = !! global_opts[:all]
|
1127
|
-
#; [!eabis] prints help message of action if action name provided.
|
1128
|
-
action_name = args[0]
|
1129
|
-
if action_name
|
1130
|
-
#; [!cgxkb] error if action for help option not found.
|
1131
|
-
metadata = INDEX.lookup_action(action_name) or
|
1132
|
-
raise CommandError.new("#{action_name}: Action not found.")
|
1133
|
-
msg = metadata.help_message(@config.app_command, all)
|
1134
|
-
#; [!nv0x3] prints help message of command if action name not provided.
|
1135
|
-
else
|
1136
|
-
msg = help_message(all)
|
1137
|
-
end
|
1138
|
-
#; [!efaws] prints colorized help message when stdout is a tty.
|
1139
|
-
#; [!9vdy1] prints non-colorized help message when stdout is not a tty.
|
1140
|
-
#; [!gsdcu] prints colorized help message when '--color[=on]' specified.
|
1141
|
-
#; [!be8y2] prints non-colorized help message when '--color=off' specified.
|
1142
|
-
msg = Util.del_escape_seq(msg) unless Util.colorize?
|
1143
|
-
puts msg
|
1144
|
-
end
|
1145
|
-
|
1146
|
-
def do_validate_actions(_args, _global_opts)
|
1147
|
-
#; [!6xhvt] reports warning at end of help message.
|
1148
|
-
nl = "\n"
|
1149
|
-
ActionScope::SUBCLASSES.each do |klass|
|
1150
|
-
#; [!iy241] reports warning if `alias_of:` specified in action class but corresponding action not exist.
|
1151
|
-
alias_of = klass.instance_variable_get(:@__aliasof__)
|
1152
|
-
if alias_of
|
1153
|
-
warn "#{nl}** [warning] in '#{klass.name}' class, `alias_of: #{alias_of.inspect}` specified but corresponding action not exist."
|
1154
|
-
nl = ""
|
1946
|
+
def render_topic_list(topic, all: false)
|
1947
|
+
#; [!uzmml] renders topic list.
|
1948
|
+
#; [!vrzu0] topic 'category1' or 'categories2' is acceptable.
|
1949
|
+
#; [!xyn5g] global option '-L metadata' renders registry data in YAML format.
|
1950
|
+
builder = get_app_help_builder()
|
1951
|
+
return (
|
1952
|
+
case topic
|
1953
|
+
when "action", "actions"; builder.section_actions(false, all: all)
|
1954
|
+
when "alias" , "aliases"; builder.section_aliases(all: all)
|
1955
|
+
when "abbrev", "abbrevs"; builder.section_abbrevs(all: all)
|
1956
|
+
when /\Acategor(?:y|ies)(\d+)?\z/ ; builder.section_categories(($1 || 0).to_i, all: all)
|
1957
|
+
when "metadata"; MetadataRenderer.new(@_registry).render_metadata()
|
1958
|
+
else raise "** assertion failed: topic=#{topic.inspect}"
|
1155
1959
|
end
|
1156
|
-
|
1157
|
-
default = klass.instance_variable_get(:@__default__)
|
1158
|
-
if default
|
1159
|
-
warn "#{nl}** [warning] in '#{klass.name}' class, `action: #{default.inspect}` specified but corresponding action not exist."
|
1160
|
-
nl = ""
|
1161
|
-
end
|
1162
|
-
end
|
1960
|
+
)
|
1163
1961
|
end
|
1164
1962
|
|
1165
|
-
def
|
1166
|
-
#; [!
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
ali_name = ali_obj.alias_name
|
1183
|
-
next unless ali_name.start_with?(prefix) || ali_name == prefix2
|
1184
|
-
pairs << [ali_name, ali_obj.desc(), ali_obj.important?]
|
1185
|
-
end
|
1186
|
-
#; [!i2azi] raises error when no candidate actions found.
|
1187
|
-
! pairs.empty? or
|
1188
|
-
raise CommandError.new("No actions starting with '#{prefix}'.")
|
1189
|
-
INDEX.each_alias do |alias_obj|
|
1190
|
-
alias_ = alias_obj.alias_name
|
1191
|
-
action_ = alias_obj.action_name
|
1192
|
-
aname2aliases[action_] << alias_ if aname2aliases.key?(action_)
|
1963
|
+
def handle_blank_action(all: false)
|
1964
|
+
#; [!seba7] prints action list and returns `0`.
|
1965
|
+
print_str render_item_list(nil, all: all)
|
1966
|
+
return 0
|
1967
|
+
end
|
1968
|
+
|
1969
|
+
def handle_prefix(prefix, all: false)
|
1970
|
+
#; [!8w301] prints action list starting with prefix and returns `0`.
|
1971
|
+
print_str render_item_list(prefix, all: all)
|
1972
|
+
return 0
|
1973
|
+
end
|
1974
|
+
|
1975
|
+
def start_action(action_name, args)
|
1976
|
+
#; [!6htva] supports abbreviation of prefix.
|
1977
|
+
if ! @_registry.metadata_exist?(action_name)
|
1978
|
+
resolved = @_registry.abbrev_resolve(action_name)
|
1979
|
+
action_name = resolved if resolved
|
1193
1980
|
end
|
1981
|
+
#; [!vbymd] runs action with args and returns `0`.
|
1982
|
+
@_registry.metadata_get(action_name) or
|
1983
|
+
raise CommandError.new("#{action_name}: Action not found.")
|
1984
|
+
new_context().start_action(action_name, args)
|
1985
|
+
return 0
|
1986
|
+
end
|
1987
|
+
|
1988
|
+
private
|
1989
|
+
|
1990
|
+
def get_app_help_builder()
|
1991
|
+
return @app_help_builder || APPLICATION_HELP_BUILDER_CLASS.new(@config)
|
1992
|
+
end
|
1993
|
+
|
1994
|
+
def get_action_help_builder()
|
1995
|
+
return @action_help_builder || ACTION_HELP_BUILDER_CLASS.new(@config)
|
1996
|
+
end
|
1997
|
+
|
1998
|
+
def new_context()
|
1999
|
+
#; [!9ddcl] creates new context object with config object.
|
2000
|
+
return CONTEXT_CLASS.new(@config)
|
2001
|
+
end
|
2002
|
+
|
2003
|
+
def print_str(str)
|
2004
|
+
#; [!yiabh] do nothing if str is nil.
|
2005
|
+
return nil unless str
|
2006
|
+
#; [!6kyv9] prints string as is if color mode is enabled.
|
2007
|
+
#; [!lxhvq] deletes escape characters from string and prints it if color mode is disabled.
|
2008
|
+
str = Util.delete_escape_chars(str) unless Util.color_mode?
|
2009
|
+
print str
|
2010
|
+
nil
|
2011
|
+
end
|
2012
|
+
|
2013
|
+
def print_error(exc)
|
2014
|
+
#; [!sdbj8] prints exception as error message.
|
2015
|
+
#; [!6z0mu] prints colored error message if stderr is a tty.
|
2016
|
+
#; [!k1s3o] prints non-colored error message if stderr is not a tty.
|
2017
|
+
prompt = "[ERROR]"
|
2018
|
+
prompt = @config.deco_error % prompt if $stderr.tty?
|
2019
|
+
$stderr.puts "#{prompt} #{exc.message}"
|
2020
|
+
nil
|
2021
|
+
end
|
2022
|
+
|
2023
|
+
def print_backtrace(exc)
|
2024
|
+
cache = {} # {filename => [line]}
|
2025
|
+
color_p = $stderr.tty?
|
1194
2026
|
sb = []
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
#; [!
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
2027
|
+
exc.backtrace().each do |bt|
|
2028
|
+
#; [!i010e] skips backtrace in `benry/cmdapp.rb`.
|
2029
|
+
next if bt.start_with?(__FILE__)
|
2030
|
+
#; [!ilaxg] skips backtrace if `#skip_backtrace?()` returns truthy value.
|
2031
|
+
next if skip_backtrace?(bt)
|
2032
|
+
#; [!5sa5k] prints filename and line number in slant format if stdout is a tty.
|
2033
|
+
s = "From #{bt}"
|
2034
|
+
s = "\e[3m#{s}\e[0m" if color_p # slant
|
2035
|
+
sb << " #{s}\n"
|
2036
|
+
if bt =~ /:(\d+)/
|
2037
|
+
#; [!2sg9r] not to try to read file content if file not found.
|
2038
|
+
fname = $`; lineno = $1.to_i
|
2039
|
+
next unless File.exist?(fname)
|
2040
|
+
#; [!ihizf] prints lines of each backtrace entry.
|
2041
|
+
cache[fname] ||= read_file_as_lines(fname)
|
2042
|
+
line = cache[fname][lineno - 1]
|
2043
|
+
sb << " #{line.strip()}\n" if line
|
1205
2044
|
end
|
1206
2045
|
end
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
def do_setup()
|
1213
|
-
#; [!pkio4] sets config object to '$cmdapp_config'.
|
1214
|
-
$cmdapp_config = @config
|
1215
|
-
#; [!qwjjv] sets application object to '$cmdapp_application'.
|
1216
|
-
$cmdapp_application = self
|
1217
|
-
#; [!kqfn1] remove built-in 'help' action if `config.help_action == false`.
|
1218
|
-
if ! @config.help_action
|
1219
|
-
INDEX.delete_action("help") if INDEX.action_exist?("help")
|
1220
|
-
end
|
2046
|
+
#; [!8wzxg] prints backtrace of exception.
|
2047
|
+
$stderr.print sb.join()
|
2048
|
+
cache.clear()
|
2049
|
+
nil
|
1221
2050
|
end
|
1222
2051
|
|
1223
|
-
def
|
1224
|
-
#; [!
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
2052
|
+
def skip_backtrace?(bt)
|
2053
|
+
#; [!r2fmv] ignores backtraces if matched to 'config.backtrace_ignore_rexp'.
|
2054
|
+
#; [!c6f11] not ignore backtraces if 'config.backtrace_ignore_rexp' is not set.
|
2055
|
+
rexp = @config.backtrace_ignore_rexp
|
2056
|
+
return rexp ? bt =~ rexp : false
|
1228
2057
|
end
|
1229
2058
|
|
1230
|
-
|
2059
|
+
def read_file_as_lines(filename)
|
2060
|
+
#; [!e9c74] reads file content as an array of line.
|
2061
|
+
return File.read(filename, encoding: 'utf-8').each_line().to_a()
|
2062
|
+
end
|
2063
|
+
|
2064
|
+
protected
|
1231
2065
|
|
1232
|
-
def
|
1233
|
-
#; [!
|
1234
|
-
|
1235
|
-
return @help_builder.build_help_message(all, format)
|
2066
|
+
def should_rescue?(exc)
|
2067
|
+
#; [!8lwyn] returns trueif exception is a BaseError.
|
2068
|
+
return exc.is_a?(BaseError)
|
1236
2069
|
end
|
1237
2070
|
|
1238
2071
|
end
|
1239
2072
|
|
1240
2073
|
|
1241
|
-
class
|
2074
|
+
class MetadataRenderer
|
1242
2075
|
|
1243
|
-
def initialize(
|
1244
|
-
@
|
1245
|
-
@schema = schema
|
2076
|
+
def initialize(registry)
|
2077
|
+
@registry = registry
|
1246
2078
|
end
|
1247
2079
|
|
1248
|
-
def
|
1249
|
-
#; [!
|
1250
|
-
format ||= @config.format_help
|
2080
|
+
def render_metadata(all: false)
|
2081
|
+
#; [!gduge] renders registry data in YAML format.
|
1251
2082
|
sb = []
|
1252
|
-
sb <<
|
1253
|
-
sb <<
|
1254
|
-
sb <<
|
1255
|
-
sb <<
|
1256
|
-
|
1257
|
-
sb <<
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
sb <<
|
1263
|
-
return sb.
|
2083
|
+
sb << "actions:\n"
|
2084
|
+
render_actions(all: all) {|s| sb << s }
|
2085
|
+
sb << "\n"
|
2086
|
+
sb << "aliases:\n"
|
2087
|
+
render_aliases(all: all) {|s| sb << s }
|
2088
|
+
sb << "\n"
|
2089
|
+
sb << "categories:\n"
|
2090
|
+
render_categories(all: all) {|s| sb << s }
|
2091
|
+
sb << "\n"
|
2092
|
+
sb << "abbreviations:\n"
|
2093
|
+
render_abbrevs() {|s| sb << s }
|
2094
|
+
return sb.join()
|
1264
2095
|
end
|
1265
2096
|
|
1266
2097
|
protected
|
1267
2098
|
|
1268
|
-
def
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
2099
|
+
def render_actions(all: false, &b)
|
2100
|
+
@registry.metadata_each(all: all) do |metadata|
|
2101
|
+
md = metadata
|
2102
|
+
if ! md.alias?
|
2103
|
+
yield " - action: #{md.name}\n"
|
2104
|
+
yield " desc: #{qq(md.desc)}\n"
|
2105
|
+
yield " class: #{md.klass.name}\n"
|
2106
|
+
yield " method: #{md.meth}\n"
|
2107
|
+
yield " usage: #{md.usage.inspect}\n" if md.usage
|
2108
|
+
yield " detail: #{md.detail.inspect}\n" if md.detail
|
2109
|
+
yield " description: #{md.description.inspect}\n" if md.description
|
2110
|
+
yield " postamble: #{md.postamble.inspect.gsub(/"=>"/, '": "')}\n" if md.postamble
|
2111
|
+
yield " tag: #{md.tag}\n" if md.tag
|
2112
|
+
yield " important: #{md.important}\n" if md.important? != nil
|
2113
|
+
yield " hidden: #{md.hidden?}\n" if md.hidden? != nil
|
2114
|
+
obj = md.klass.new(nil)
|
2115
|
+
paramstr = Util.method2help(obj, md.meth).strip
|
2116
|
+
if ! paramstr.empty?
|
2117
|
+
yield " paramstr: #{qq(paramstr)}\n"
|
2118
|
+
yield " parameters:\n"
|
2119
|
+
render_parameters(md, all: all, &b)
|
2120
|
+
end
|
2121
|
+
if ! md.schema.empty?
|
2122
|
+
yield " options:\n"
|
2123
|
+
render_options(md, all: all, &b)
|
2124
|
+
end
|
2125
|
+
end
|
1285
2126
|
end
|
1286
|
-
#; [!rvhzd] no preamble when neigher app desc nor detail specified.
|
1287
|
-
return nil if sb.empty?
|
1288
|
-
return sb.join()
|
1289
2127
|
end
|
1290
2128
|
|
1291
|
-
def
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
return build_section("Usage", sb.join(), nil)
|
2129
|
+
def render_parameters(metadata, all: false)
|
2130
|
+
obj = metadata.klass.new(nil)
|
2131
|
+
obj.method(metadata.meth).parameters.each do |ptype, pname|
|
2132
|
+
yield " - param: #{pname}\n"
|
2133
|
+
yield " type: #{ptype}\n"
|
2134
|
+
end
|
1298
2135
|
end
|
1299
2136
|
|
1300
|
-
def
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
if
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
2137
|
+
def render_options(metadata, all: false)
|
2138
|
+
metadata.schema.each() do |item|
|
2139
|
+
next if all == false && item.hidden?
|
2140
|
+
yield " - key: #{item.key}\n"
|
2141
|
+
yield " desc: #{qq(item.desc)}\n"
|
2142
|
+
yield " optdef: #{qq(item.optdef)}\n"
|
2143
|
+
yield " short: #{item.short}\n" if item.short
|
2144
|
+
yield " long: #{item.long}\n" if item.long
|
2145
|
+
yield " param: #{qq(item.param)}\n" if item.param
|
2146
|
+
yield " paramreq: #{item.arg_requireness}\n"
|
2147
|
+
yield " type: #{item.type.name}\n" if item.type
|
2148
|
+
yield " rexp: #{item.rexp.inspect}\n" if item.rexp
|
2149
|
+
yield " enum: #{item.enum.inspect}\n" if item.enum
|
2150
|
+
yield " range: #{item.range.inspect}\n" if item.range
|
2151
|
+
yield " value: #{item.value.inspect}\n" if item.value != nil
|
2152
|
+
yield " detail: #{item.detail.inspect}\n" if item.detail != nil
|
2153
|
+
yield " tag: #{item.tag}\n" if item.tag
|
2154
|
+
yield " important: #{item.important}\n" if item.important? != nil
|
2155
|
+
yield " hidden: #{item.hidden?}\n" if item.hidden? != nil
|
1312
2156
|
end
|
1313
|
-
#; [!bm71g] ignores 'Options:' section if no options exist.
|
1314
|
-
return nil if sb.empty?
|
1315
|
-
#; [!proa4] includes description of global options.
|
1316
|
-
return build_section("Options", sb.join(), nil)
|
1317
2157
|
end
|
1318
2158
|
|
1319
|
-
def
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
#; [!yigf3] shows private (hidden) action names if 'all' flag is true.
|
1331
|
-
if all || ! Util.hidden_name?(name)
|
1332
|
-
#; [!5d9mc] shows hidden action in weak format.
|
1333
|
-
#; [!awk3l] shows important action in strong format.
|
1334
|
-
#; [!9k4dv] shows unimportant action in weak fomrat.
|
1335
|
-
sb << Util.format_help_line(format, name, desc, important)
|
2159
|
+
def render_aliases(all: false)
|
2160
|
+
@registry.metadata_each(all: all) do |metadata|
|
2161
|
+
md = metadata
|
2162
|
+
if md.alias?
|
2163
|
+
yield " - alias: #{md.name}\n"
|
2164
|
+
yield " desc: #{qq(md.desc)}\n"
|
2165
|
+
yield " action: #{md.action}\n"
|
2166
|
+
yield " args: #{md.args.inspect}\n" if md.args && ! md.args.empty?
|
2167
|
+
yield " tag: #{md.tag}\n" if md.tag
|
2168
|
+
yield " important: #{md.important}\n" if md.important != nil
|
2169
|
+
yield " hidden: #{md.hidden?}\n" if md.hidden? != nil
|
1336
2170
|
end
|
1337
2171
|
end
|
1338
|
-
return build_section("Actions", sb.join(), desc)
|
1339
2172
|
end
|
1340
2173
|
|
1341
|
-
def
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
alias_name = alias_obj.alias_name
|
1348
|
-
#; [!5g72a] not show hidden alias names in default.
|
1349
|
-
#; [!ekuqm] shows all alias names including private ones if 'all' flag is true.
|
1350
|
-
if all || ! Util.hidden_name?(alias_name)
|
1351
|
-
#; [!aey2k] shows alias in strong or weak format according to action.
|
1352
|
-
sb << Util.format_help_line(format, alias_name, alias_obj.desc(), alias_obj.important?)
|
1353
|
-
end
|
2174
|
+
def render_categories(all: false)
|
2175
|
+
dict = @registry.category_count_actions(0, all: all)
|
2176
|
+
@registry.category_each do |prefix, desc|
|
2177
|
+
yield " - prefix: \"#{prefix}\"\n"
|
2178
|
+
yield " count: #{dict[prefix] || 0}\n"
|
2179
|
+
yield " desc: #{qq(desc)}\n" if desc
|
1354
2180
|
end
|
1355
|
-
#; [!p3oh6] now show 'Aliases:' section if no aliases defined.
|
1356
|
-
return nil if sb.empty?
|
1357
|
-
#; [!we1l8] shows 'Aliases:' section if any aliases defined.
|
1358
|
-
return build_section("Aliases", sb.join(), nil)
|
1359
2181
|
end
|
1360
2182
|
|
1361
|
-
def
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
#; [!ckagw] adds '\n' at end of postamble text if it doesn't end with '\n'.
|
1366
|
-
s += "\n" unless s.end_with?("\n")
|
2183
|
+
def render_abbrevs()
|
2184
|
+
@registry.abbrev_each do |abbrev, prefix|
|
2185
|
+
yield " - abbrev: \"#{abbrev}\"\n"
|
2186
|
+
yield " prefix: \"#{prefix}\"\n"
|
1367
2187
|
end
|
1368
|
-
|
2188
|
+
end
|
2189
|
+
|
2190
|
+
private
|
2191
|
+
|
2192
|
+
def qq(s)
|
2193
|
+
return "" if s.nil?
|
2194
|
+
return '"' + s.gsub(/"/, '\\"') + '"'
|
1369
2195
|
end
|
1370
2196
|
|
1371
2197
|
end
|
1372
2198
|
|
1373
2199
|
|
1374
|
-
|
2200
|
+
def self.main(app_desc, app_version=nil, **kwargs)
|
2201
|
+
#; [!6mfxt] accepts the same arguments as 'Config#initialize()'.
|
2202
|
+
config = Config.new(app_desc, app_version, **kwargs)
|
2203
|
+
#; [!scpwa] runs application.
|
2204
|
+
app = Application.new(config)
|
2205
|
+
#; [!jbv9z] returns the status code.
|
2206
|
+
status_code = app.main()
|
2207
|
+
return status_code
|
2208
|
+
end
|
1375
2209
|
|
1376
2210
|
|
1377
2211
|
end
|