benry-cmdapp 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +14 -0
- data/README.md +1698 -857
- data/benry-cmdapp.gemspec +4 -6
- data/doc/benry-cmdapp.html +1587 -911
- data/lib/benry/cmdapp.rb +1894 -1059
- 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 +21 -7
- data/test/action_test.rb +0 -1038
- data/test/index_test.rb +0 -185
data/lib/benry/cmdapp.rb
CHANGED
@@ -2,1375 +2,2210 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
###
|
5
|
-
### $Release:
|
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
|
496
|
+
def initialize(config, context=nil)
|
497
|
+
@config = config
|
498
|
+
@__context__ = context || CONTEXT_CLASS.new(config)
|
290
499
|
end
|
291
500
|
|
292
|
-
|
501
|
+
def __clear_recursive_reference() # :nodoc:
|
502
|
+
#; [!i68z0] clears instance var which refers context object.
|
503
|
+
@__context__ = nil
|
504
|
+
nil
|
505
|
+
end
|
293
506
|
|
294
|
-
def
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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."
|
307
668
|
return nil
|
308
669
|
end
|
670
|
+
private_class_method :__validate_kwargs
|
309
671
|
|
310
|
-
def
|
311
|
-
#; [!
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
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
|
316
681
|
end
|
682
|
+
private_class_method :__validate_action_method
|
317
683
|
|
318
|
-
def
|
319
|
-
|
320
|
-
|
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...
|
321
722
|
else
|
322
|
-
#; [!
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
puts s
|
327
|
-
__run_action(*args, **kwargs)
|
328
|
-
s = "## exit: #{@name}"
|
329
|
-
s = "\e[33m#{s}\e[0m" if Util.colorize?
|
330
|
-
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)
|
331
727
|
end
|
332
728
|
nil
|
333
729
|
end
|
334
730
|
|
335
|
-
def
|
336
|
-
#; [!
|
337
|
-
|
338
|
-
|
339
|
-
|
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.
|
340
768
|
else
|
341
|
-
|
769
|
+
action_name = prefix + action_name
|
342
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)
|
343
773
|
end
|
344
|
-
private :__run_action
|
345
774
|
|
346
|
-
def
|
347
|
-
|
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)
|
348
787
|
end
|
349
|
-
protected :_new_action_object
|
350
788
|
|
351
|
-
def
|
352
|
-
#; [!
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
when :keyrest ; nil
|
363
|
-
else ; nil
|
364
|
-
end
|
365
|
-
end
|
366
|
-
#; [!w3rer] max is nil if variable argument exists.
|
367
|
-
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)
|
368
800
|
end
|
369
801
|
|
370
|
-
def
|
371
|
-
#; [!
|
372
|
-
|
373
|
-
|
374
|
-
method_obj = @klass.instance_method(@method)
|
375
|
-
method_obj.parameters.each {|kind, param| kw_params << param if kind == :key }
|
376
|
-
opt_keys = @schema.each.collect {|item| item.key }
|
377
|
-
key = (opt_keys - kw_params).first
|
378
|
-
return nil if key == nil
|
379
|
-
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
|
380
806
|
end
|
381
807
|
|
382
|
-
def
|
383
|
-
#; [!
|
384
|
-
|
385
|
-
|
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)
|
386
816
|
end
|
387
817
|
|
388
818
|
end
|
389
819
|
|
390
820
|
|
391
|
-
|
821
|
+
Action = ActionScope
|
392
822
|
|
393
823
|
|
394
|
-
class
|
824
|
+
class Registry
|
395
825
|
|
396
|
-
def initialize(
|
397
|
-
|
398
|
-
@
|
399
|
-
@
|
400
|
-
@kwargs = kwargs
|
826
|
+
def initialize()
|
827
|
+
@metadata_dict = {} # {name => (ActionMetadata|AliasMetadata)}
|
828
|
+
@category_dict = {} # {prefix => description}
|
829
|
+
@abbrev_dict = {}
|
401
830
|
end
|
402
831
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
else
|
410
|
-
return @action_metadata.__send__(meth, *args, **kwargs)
|
411
|
-
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
|
412
838
|
end
|
413
839
|
|
414
|
-
def
|
415
|
-
|
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]
|
416
844
|
end
|
417
845
|
|
418
|
-
def
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
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)
|
423
851
|
end
|
424
852
|
|
425
|
-
|
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
|
426
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
|
427
870
|
|
428
|
-
|
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
|
429
880
|
|
430
|
-
def
|
431
|
-
#; [!
|
432
|
-
#; [!
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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)
|
438
889
|
end
|
439
|
-
|
440
|
-
sb << "\n" unless content.end_with?("\n")
|
441
|
-
return sb.join()
|
890
|
+
return md, alias_args
|
442
891
|
end
|
443
892
|
|
444
|
-
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
|
445
899
|
nil
|
446
900
|
end
|
447
901
|
|
448
|
-
def
|
449
|
-
|
450
|
-
|
451
|
-
|
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
|
452
911
|
end
|
453
912
|
|
454
|
-
|
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
|
455
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
|
456
926
|
|
457
|
-
|
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
|
458
953
|
|
459
|
-
def
|
460
|
-
|
954
|
+
def abbrev_add(abbrev, prefix)
|
955
|
+
#; [!n475k] registers abbrev with prefix.
|
956
|
+
@abbrev_dict[abbrev] = prefix
|
957
|
+
nil
|
461
958
|
end
|
462
959
|
|
463
|
-
def
|
464
|
-
|
465
|
-
|
466
|
-
sb << build_usage(command, all)
|
467
|
-
sb << build_options(command, all)
|
468
|
-
sb << build_postamble(command, all)
|
469
|
-
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]
|
470
963
|
end
|
471
964
|
|
472
|
-
|
965
|
+
def abbrev_exist?(abbrev)
|
966
|
+
#; [!tjbdy] returns true/false if abbrev registered or not.
|
967
|
+
return @abbrev_dict.key?(abbrev)
|
968
|
+
end
|
473
969
|
|
474
|
-
def
|
475
|
-
#; [!
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
sb << "\n"
|
480
|
-
sb << @am.detail
|
481
|
-
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
|
482
975
|
end
|
483
|
-
|
976
|
+
nil
|
484
977
|
end
|
485
978
|
|
486
|
-
def
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
s = build_argstr().strip()
|
493
|
-
s = "[<options>] " + s unless Util.schema_empty?(@am.schema, all)
|
494
|
-
s = s.rstrip()
|
495
|
-
sb = []
|
496
|
-
sb << (format % ["#{command} #{@am.name}", s]) << "\n"
|
497
|
-
return build_section("Usage", sb.join(), nil)
|
498
|
-
end
|
499
|
-
|
500
|
-
def build_options(command, all=false)
|
501
|
-
config = $cmdapp_config
|
502
|
-
format = config ? config.format_help : Config::FORMAT_HELP
|
503
|
-
format += "\n"
|
504
|
-
#; [!g2ju5] adds 'Options:' section.
|
505
|
-
sb = []; width = nil; indent = nil
|
506
|
-
@am.schema.each do |item|
|
507
|
-
#; [!hghuj] ignores 'Options:' section when only hidden options speicified.
|
508
|
-
next unless all || ! item.hidden?
|
509
|
-
#; [!vqqq1] hidden option should be shown in weak format.
|
510
|
-
important = item.hidden? ? false : nil
|
511
|
-
sb << Util.format_help_line(format, item.optdef, item.desc, important)
|
512
|
-
#; [!dukm7] includes detailed description of option.
|
513
|
-
if item.detail
|
514
|
-
width ||= (Util.del_escape_seq(format % ["", ""])).length
|
515
|
-
indent ||= " " * (width - 1) # `-1` means "\n"
|
516
|
-
sb << item.detail.gsub(/^/, indent)
|
517
|
-
sb << "\n" unless item.detail.end_with?("\n")
|
518
|
-
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
|
519
985
|
end
|
520
|
-
#; [!
|
521
|
-
return nil
|
522
|
-
|
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
|
523
1010
|
end
|
524
1011
|
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
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()
|
531
1039
|
end
|
532
|
-
|
1040
|
+
#@scope_objects.each {|_, scope| scope.__clear_recursive_reference() }
|
1041
|
+
#@scope_objects.clear()
|
1042
|
+
@status_dict.clear()
|
533
1043
|
end
|
534
1044
|
|
535
|
-
|
536
|
-
|
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)
|
537
1085
|
end
|
538
1086
|
|
539
1087
|
private
|
540
1088
|
|
541
|
-
def
|
542
|
-
|
543
|
-
#; [!
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
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)
|
556
1121
|
end
|
1122
|
+
puts "#{c1}### exit: #{md.name}#{c2}" if @config.trace_mode
|
1123
|
+
ensure
|
1124
|
+
@curr_action = prev_action
|
557
1125
|
end
|
558
|
-
|
559
|
-
|
1126
|
+
@status_dict[action] = :done
|
1127
|
+
#; [!ndxc3] returns true if action invoked.
|
1128
|
+
return true
|
560
1129
|
end
|
561
1130
|
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
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
|
569
1139
|
end
|
570
1140
|
|
571
1141
|
end
|
572
1142
|
|
573
1143
|
|
574
|
-
|
1144
|
+
CONTEXT_CLASS = ApplicationContext
|
575
1145
|
|
576
1146
|
|
577
|
-
class
|
1147
|
+
class Config
|
578
1148
|
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
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>...]"
|
583
1163
|
|
584
|
-
def
|
585
|
-
|
586
|
-
|
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
|
587
1245
|
end
|
588
1246
|
|
589
|
-
|
1247
|
+
end
|
590
1248
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
raise ActionNotFoundError.new("#{action_name}: Action not found.")
|
598
|
-
name = metadata.name
|
599
|
-
#; [!u8mit] raises error if action flow is looped.
|
600
|
-
! INDEX.action_doing?(name) or
|
601
|
-
raise LoopedActionError.new("#{name}: Action loop detected.")
|
602
|
-
#; [!vhdo9] don't invoke action twice if 'once' arg is true.
|
603
|
-
if INDEX.action_done?(name)
|
604
|
-
return INDEX.action_result(name) if once
|
605
|
-
end
|
606
|
-
#; [!r8fbn] invokes action.
|
607
|
-
INDEX.action_doing(name)
|
608
|
-
ret = metadata.run_action(*args, **kwargs)
|
609
|
-
INDEX.action_done(name, ret)
|
610
|
-
return ret
|
1249
|
+
|
1250
|
+
class BaseHelpBuilder
|
1251
|
+
|
1252
|
+
def initialize(config, _registry: REGISTRY)
|
1253
|
+
@config = config
|
1254
|
+
@_registry = _registry
|
611
1255
|
end
|
612
1256
|
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
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.")
|
624
1268
|
end
|
625
1269
|
|
626
|
-
|
1270
|
+
protected
|
627
1271
|
|
628
|
-
def
|
629
|
-
#; [!
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
#; [!
|
649
|
-
|
650
|
-
schema.add(param, optdef, desc, *rest, type: type, rexp: rexp, enum: enum, range: range, value: value, detail: detail, tag: nil, &block)
|
651
|
-
rescue Benry::CmdOpt::SchemaError => exc
|
652
|
-
raise OptionDefError.new(exc.message)
|
653
|
-
end
|
654
|
-
end
|
655
|
-
#; [!yrkxn] @copy_options is a Proc object and copies options from other action.
|
656
|
-
@copy_options = proc do |action_name, except: nil|
|
657
|
-
#; [!mhhn2] '@copy_options.()' raises error when action not found.
|
658
|
-
metadata = INDEX.get_action(action_name) or
|
659
|
-
raise OptionDefError.new("@copy_options.(#{action_name.inspect}): Action not found.")
|
660
|
-
@__option__ ||= SCHEMA_CLASS.new
|
661
|
-
@__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}`.")
|
662
1294
|
end
|
663
1295
|
end
|
1296
|
+
return sb.empty? ? nil : sb.join("\n")
|
664
1297
|
end
|
665
1298
|
|
666
|
-
def
|
667
|
-
#; [!
|
668
|
-
|
669
|
-
#; [!
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
#; [!t8vbf] raises error if action name duplicated.
|
681
|
-
! INDEX.action_exist?(name) or
|
682
|
-
raise ActionDefError.new("def #{method}(): Action '#{name}' already exist.")
|
683
|
-
INDEX.register_action(name, metadata)
|
684
|
-
#; [!jpzbi] defines same name alias of action as prefix.
|
685
|
-
#; [!997gs] not raise error when action not found.
|
686
|
-
self.__define_alias_of_action(method, name)
|
687
|
-
end
|
688
|
-
|
689
|
-
def self.__method2action(method) # :nodoc:
|
690
|
-
#; [!5e5o0] when method name is same as default action name...
|
691
|
-
if method == @__default__ # when Symbol
|
692
|
-
#; [!myj3p] uses prefix name (expect last char ':') as action name.
|
693
|
-
@__prefix__ != nil or raise "** assertion failed"
|
694
|
-
name = @__prefix__.chomp(":")
|
695
|
-
#; [!j5oto] clears '@__default__'.
|
696
|
-
@__default__ = nil
|
697
|
-
#; [!agpwh] else...
|
698
|
-
else
|
699
|
-
#; [!3icc4] uses method name as action name.
|
700
|
-
#; [!c643b] converts action name 'aa_bb_cc_' into 'aa_bb_cc'.
|
701
|
-
#; [!3fkb3] converts action name 'aa__bb__cc' into 'aa:bb:cc'.
|
702
|
-
#; [!o9s9h] converts action name 'aa_bb:_cc_dd' into 'aa-bb:_cc-dd'.
|
703
|
-
name = Util.method2action(method.to_s)
|
704
|
-
#; [!8hlni] when action name is same as default name, uses prefix as action name.
|
705
|
-
if name == @__default__ # when String
|
706
|
-
name = @__prefix__.chomp(":")
|
707
|
-
#; [!q8oxi] clears '@__default__' when default name matched to action name.
|
708
|
-
@__default__ = nil
|
709
|
-
#; [!xfent] when prefix is provided, adds it to action name.
|
710
|
-
elsif @__prefix__
|
711
|
-
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)
|
712
1313
|
end
|
1314
|
+
s = decorate_str(s, x.hidden?, x.important?)
|
1315
|
+
sb << s << "\n"
|
713
1316
|
end
|
714
|
-
return
|
715
|
-
end
|
716
|
-
|
717
|
-
def
|
718
|
-
|
719
|
-
@
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
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
|
727
1345
|
end
|
728
1346
|
end
|
729
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
|
+
|
730
1354
|
end
|
731
1355
|
|
732
1356
|
|
733
|
-
|
1357
|
+
class ApplicationHelpBuilder < BaseHelpBuilder
|
734
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
|
735
1374
|
|
736
|
-
|
1375
|
+
protected
|
737
1376
|
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
#; [!
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
app = $cmdapp_application
|
752
|
-
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')
|
753
1390
|
end
|
754
|
-
|
755
|
-
#; [!ihr5u] prints non-colorized help message when color mode is off.
|
756
|
-
msg = Util.del_escape_seq(msg) unless Util.colorize?
|
757
|
-
print msg
|
1391
|
+
return sb.join()
|
758
1392
|
end
|
759
1393
|
|
760
|
-
|
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
|
761
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
|
762
1409
|
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
INDEX.register_alias(alias_, Alias.new(alias_, action_, *args, important: important, tag: tag))
|
781
|
-
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
|
782
1427
|
|
1428
|
+
public
|
783
1429
|
|
784
|
-
|
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
|
785
1444
|
|
786
|
-
def
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
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()
|
792
1470
|
end
|
1471
|
+
private :_render_metadata_list
|
793
1472
|
|
794
|
-
|
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
|
795
1479
|
|
796
|
-
def
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
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"
|
801
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:"
|
802
1547
|
end
|
803
1548
|
|
804
|
-
def
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
#; [!
|
809
|
-
|
810
|
-
|
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:"
|
811
1569
|
end
|
812
1570
|
|
813
1571
|
end
|
814
1572
|
|
815
1573
|
|
816
|
-
class
|
1574
|
+
class ActionHelpBuilder < BaseHelpBuilder
|
817
1575
|
|
818
|
-
|
819
|
-
|
820
|
-
|
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
|
821
1591
|
|
822
|
-
|
1592
|
+
protected
|
823
1593
|
|
824
|
-
|
825
|
-
|
826
|
-
|
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
|
827
1609
|
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
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
|
833
1629
|
|
834
|
-
def
|
835
|
-
|
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
|
-
|
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
|
877
1673
|
|
878
1674
|
end
|
879
1675
|
|
880
1676
|
|
881
|
-
|
1677
|
+
APPLICATION_HELP_BUILDER_CLASS = ApplicationHelpBuilder
|
1678
|
+
ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
|
1679
|
+
|
1680
|
+
|
1681
|
+
class GlobalOptionSchema < OptionSchema
|
882
1682
|
|
883
|
-
def initialize(config
|
1683
|
+
def initialize(config)
|
884
1684
|
super()
|
885
|
-
|
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.
|
886
1692
|
c = config
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
#; [!
|
909
|
-
|
910
|
-
|
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 }
|
911
1730
|
nil
|
912
1731
|
end
|
913
1732
|
|
914
1733
|
end
|
915
1734
|
|
916
|
-
|
917
|
-
|
1735
|
+
GLOBAL_OPTION_SCHEMA_CLASS = GlobalOptionSchema
|
1736
|
+
GLOBAL_OPTION_PARSER_CLASS = OptionParser
|
918
1737
|
|
919
1738
|
|
920
1739
|
class Application
|
921
1740
|
|
922
|
-
def initialize(config,
|
923
|
-
@config
|
924
|
-
|
925
|
-
@
|
926
|
-
|
927
|
-
@
|
928
|
-
|
929
|
-
|
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
|
930
1775
|
end
|
931
1776
|
|
932
|
-
|
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
|
933
1790
|
|
934
|
-
def
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
#; [!a7d4w] prints error message with '[ERROR]' prompt.
|
942
|
-
$stderr.puts "\033[0;31m[ERROR]\033[0m #{exc.message}"
|
943
|
-
#; [!r7opi] prints filename and line number on where error raised if DefinitionError.
|
944
|
-
if exc.is_a?(DefinitionError)
|
945
|
-
#; [!v0zrf] error location can be filtered by block.
|
946
|
-
if block_given?()
|
947
|
-
loc = exc.backtrace_locations.find(&block)
|
948
|
-
else
|
949
|
-
loc = exc.backtrace_locations.find {|x| x.path != __FILE__ }
|
950
|
-
end
|
951
|
-
raise unless loc
|
952
|
-
$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])
|
953
1798
|
end
|
954
|
-
#; [!
|
955
|
-
|
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...
|
956
1808
|
else
|
957
|
-
#; [!
|
958
|
-
|
1809
|
+
#; [!bq39a] runs action with arguments.
|
1810
|
+
action = args.shift()
|
959
1811
|
end
|
1812
|
+
#; [!5yd8x] returns 0 when action invoked successfully.
|
1813
|
+
return start_action(action, args)
|
960
1814
|
end
|
1815
|
+
protected :handle_action
|
961
1816
|
|
962
|
-
def
|
963
|
-
#; [!
|
964
|
-
|
965
|
-
#; [!
|
966
|
-
|
967
|
-
|
968
|
-
#; [!go9kk] sets global variables according to global options.
|
969
|
-
do_toggle_global_switches(args, global_opts)
|
970
|
-
#; [!pbug7] skip actions if callback method throws `:SKIP`.
|
971
|
-
skip_action = true
|
972
|
-
catch :SKIP do
|
973
|
-
#; [!5iczl] skip actions if help option or version option specified.
|
974
|
-
do_handle_global_options(args, global_opts)
|
975
|
-
#; [!w584g] calls callback method.
|
976
|
-
do_callback(args, global_opts)
|
977
|
-
skip_action = false
|
978
|
-
end
|
979
|
-
return if skip_action
|
980
|
-
#; [!avxos] prints candidate actions if action name ends with ':'.
|
981
|
-
#; [!eeh0y] candidates are not printed if 'config.feat_candidate' is false.
|
982
|
-
if ! args.empty? && args[0].end_with?(':') && @config.feat_candidate
|
983
|
-
do_print_candidates(args, global_opts)
|
984
|
-
return
|
985
|
-
end
|
986
|
-
#; [!agfdi] reports error when action not found.
|
987
|
-
#; [!o5i3w] reports error when default action not found.
|
988
|
-
#; [!n60o0] reports error when action nor default action not specified.
|
989
|
-
#; [!7h0ku] prints help if no action but 'config.default_help' is true.
|
990
|
-
#; [!l0g1l] skip actions if no action specified and 'config.default_help' is set.
|
991
|
-
metadata = do_find_action(args, global_opts)
|
992
|
-
if metadata == nil
|
993
|
-
do_print_help_message([], global_opts)
|
994
|
-
do_validate_actions(args, global_opts)
|
995
|
-
return
|
996
|
-
end
|
997
|
-
#; [!x1xgc] run action with options and arguments.
|
998
|
-
#; [!v5k56] runs default action if action not specified.
|
999
|
-
do_run_action(metadata, args, global_opts)
|
1000
|
-
rescue => exc
|
1001
|
-
raise
|
1002
|
-
ensure
|
1003
|
-
#; [!hk6iu] unsets $cmdapp_config at end.
|
1004
|
-
#; [!wv22u] calls teardown method at end of running action.
|
1005
|
-
#; [!dhba4] calls teardown method even if exception raised.
|
1006
|
-
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)
|
1007
1823
|
end
|
1008
1824
|
|
1009
1825
|
protected
|
1010
1826
|
|
1011
|
-
def
|
1012
|
-
#; [!
|
1013
|
-
|
1827
|
+
def setup()
|
1828
|
+
#; [!6hi1y] stores current application.
|
1829
|
+
Benry::CmdApp._set_current_app(self)
|
1014
1830
|
end
|
1015
1831
|
|
1016
|
-
def
|
1017
|
-
#; [!
|
1018
|
-
|
1832
|
+
def teardown()
|
1833
|
+
#; [!t44mv] removes current applicatin from data store.
|
1834
|
+
Benry::CmdApp._set_current_app(nil)
|
1019
1835
|
end
|
1020
1836
|
|
1021
|
-
def
|
1022
|
-
#; [!
|
1023
|
-
parser =
|
1024
|
-
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
|
1025
1841
|
return global_opts
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
#; [!p1l1i] sets $QUIET_MODE to true if '-q' or '--quiet' specified.
|
1034
|
-
#; [!2zvf9] sets $COLOR_MODE to true/false according to '--color' option.
|
1035
|
-
#; [!ywl1a] sets $DEBUG_MODE to true if '-D' or '--debug' specified.
|
1036
|
-
#; [!8trmz] sets $TRACE_MODE to true if '-T' or '--trace' specified.
|
1037
|
-
global_opts.each do |key, val|
|
1038
|
-
case key
|
1039
|
-
when :verbose ; $QUIET_MODE = ! val
|
1040
|
-
when :quiet ; $QUIET_MODE = val
|
1041
|
-
when :color ; $COLOR_MODE = val
|
1042
|
-
when :debug ; $DEBUG_MODE = val
|
1043
|
-
when :trace ; $TRACE_MODE = val
|
1044
|
-
else ; # do nothing
|
1045
|
-
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
|
1046
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
|
1047
1859
|
end
|
1048
1860
|
|
1049
|
-
def
|
1050
|
-
|
1051
|
-
#; [!
|
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.
|
1052
1865
|
if global_opts[:help]
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
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
|
1056
1870
|
end
|
1057
|
-
#; [!
|
1871
|
+
#; [!dkjw8] prints version number if global option `-V, --version` specified.
|
1058
1872
|
if global_opts[:version]
|
1059
|
-
|
1060
|
-
|
1873
|
+
print_str render_version()
|
1874
|
+
return 0
|
1061
1875
|
end
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
if @callback && ! @__called
|
1068
|
-
@__called = true
|
1069
|
-
@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
|
1070
1881
|
end
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
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...
|
1092
1940
|
else
|
1093
|
-
|
1941
|
+
#; [!9r4w9] raises ArgumentError.
|
1942
|
+
raise ArgumentError.new("#{prefix.inspect}: Invalid value as a prefix.")
|
1094
1943
|
end
|
1095
|
-
return metadata
|
1096
1944
|
end
|
1097
1945
|
|
1098
|
-
def
|
1099
|
-
|
1100
|
-
#; [!
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
raise
|
1111
|
-
elsif max && max < n
|
1112
|
-
raise CommandError.new("#{action_name}: Too much arguments (at most #{max}).")
|
1113
|
-
end
|
1114
|
-
#; [!cf45e] runs action with arguments and options.
|
1115
|
-
#; [!tsal4] detects looped action.
|
1116
|
-
INDEX.action_doing(action_name)
|
1117
|
-
ret = metadata.run_action(*args, **options)
|
1118
|
-
INDEX.action_done(action_name, ret)
|
1119
|
-
return ret
|
1120
|
-
end
|
1121
|
-
|
1122
|
-
def do_print_help_message(args, global_opts)
|
1123
|
-
#; [!4qs7y] shows private (hidden) actions/options if '--all' option specified.
|
1124
|
-
#; [!l4d6n] `all` flag should be true or false, not nil.
|
1125
|
-
all = !! global_opts[:all]
|
1126
|
-
#; [!eabis] prints help message of action if action name provided.
|
1127
|
-
action_name = args[0]
|
1128
|
-
if action_name
|
1129
|
-
#; [!cgxkb] error if action for help option not found.
|
1130
|
-
metadata = INDEX.lookup_action(action_name) or
|
1131
|
-
raise CommandError.new("#{action_name}: Action not found.")
|
1132
|
-
msg = metadata.help_message(@config.app_command, all)
|
1133
|
-
#; [!nv0x3] prints help message of command if action name not provided.
|
1134
|
-
else
|
1135
|
-
msg = help_message(all)
|
1136
|
-
end
|
1137
|
-
#; [!efaws] prints colorized help message when stdout is a tty.
|
1138
|
-
#; [!9vdy1] prints non-colorized help message when stdout is not a tty.
|
1139
|
-
#; [!gsdcu] prints colorized help message when '--color[=on]' specified.
|
1140
|
-
#; [!be8y2] prints non-colorized help message when '--color=off' specified.
|
1141
|
-
msg = Util.del_escape_seq(msg) unless Util.colorize?
|
1142
|
-
puts msg
|
1143
|
-
end
|
1144
|
-
|
1145
|
-
def do_validate_actions(_args, _global_opts)
|
1146
|
-
#; [!6xhvt] reports warning at end of help message.
|
1147
|
-
nl = "\n"
|
1148
|
-
ActionScope::SUBCLASSES.each do |klass|
|
1149
|
-
#; [!iy241] reports warning if `alias_of:` specified in action class but corresponding action not exist.
|
1150
|
-
alias_of = klass.instance_variable_get(:@__aliasof__)
|
1151
|
-
if alias_of
|
1152
|
-
warn "#{nl}** [warning] in '#{klass.name}' class, `alias_of: #{alias_of.inspect}` specified but corresponding action not exist."
|
1153
|
-
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}"
|
1154
1959
|
end
|
1155
|
-
|
1156
|
-
default = klass.instance_variable_get(:@__default__)
|
1157
|
-
if default
|
1158
|
-
warn "#{nl}** [warning] in '#{klass.name}' class, `action: #{default.inspect}` specified but corresponding action not exist."
|
1159
|
-
nl = ""
|
1160
|
-
end
|
1161
|
-
end
|
1960
|
+
)
|
1162
1961
|
end
|
1163
1962
|
|
1164
|
-
def
|
1165
|
-
#; [!
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
ali_name = ali_obj.alias_name
|
1182
|
-
next unless ali_name.start_with?(prefix) || ali_name == prefix2
|
1183
|
-
pairs << [ali_name, ali_obj.desc(), ali_obj.important?]
|
1184
|
-
end
|
1185
|
-
#; [!i2azi] raises error when no candidate actions found.
|
1186
|
-
! pairs.empty? or
|
1187
|
-
raise CommandError.new("No actions starting with '#{prefix}'.")
|
1188
|
-
INDEX.each_alias do |alias_obj|
|
1189
|
-
alias_ = alias_obj.alias_name
|
1190
|
-
action_ = alias_obj.action_name
|
1191
|
-
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
|
1192
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?
|
1193
2026
|
sb = []
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
#; [!
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
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
|
1204
2044
|
end
|
1205
2045
|
end
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
def do_setup()
|
1212
|
-
#; [!pkio4] sets config object to '$cmdapp_config'.
|
1213
|
-
$cmdapp_config = @config
|
1214
|
-
#; [!qwjjv] sets application object to '$cmdapp_application'.
|
1215
|
-
$cmdapp_application = self
|
1216
|
-
#; [!kqfn1] remove built-in 'help' action if `config.help_action == false`.
|
1217
|
-
if ! @config.help_action
|
1218
|
-
INDEX.delete_action("help") if INDEX.action_exist?("help")
|
1219
|
-
end
|
2046
|
+
#; [!8wzxg] prints backtrace of exception.
|
2047
|
+
$stderr.print sb.join()
|
2048
|
+
cache.clear()
|
2049
|
+
nil
|
1220
2050
|
end
|
1221
2051
|
|
1222
|
-
def
|
1223
|
-
#; [!
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
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
|
1227
2057
|
end
|
1228
2058
|
|
1229
|
-
|
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
|
1230
2065
|
|
1231
|
-
def
|
1232
|
-
#; [!
|
1233
|
-
|
1234
|
-
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)
|
1235
2069
|
end
|
1236
2070
|
|
1237
2071
|
end
|
1238
2072
|
|
1239
2073
|
|
1240
|
-
class
|
2074
|
+
class MetadataRenderer
|
1241
2075
|
|
1242
|
-
def initialize(
|
1243
|
-
@
|
1244
|
-
@schema = schema
|
2076
|
+
def initialize(registry)
|
2077
|
+
@registry = registry
|
1245
2078
|
end
|
1246
2079
|
|
1247
|
-
def
|
1248
|
-
#; [!
|
1249
|
-
format ||= @config.format_help
|
2080
|
+
def render_metadata(all: false)
|
2081
|
+
#; [!gduge] renders registry data in YAML format.
|
1250
2082
|
sb = []
|
1251
|
-
sb <<
|
1252
|
-
sb <<
|
1253
|
-
sb <<
|
1254
|
-
sb <<
|
1255
|
-
|
1256
|
-
sb <<
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
sb <<
|
1262
|
-
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()
|
1263
2095
|
end
|
1264
2096
|
|
1265
2097
|
protected
|
1266
2098
|
|
1267
|
-
def
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
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
|
1284
2126
|
end
|
1285
|
-
#; [!rvhzd] no preamble when neigher app desc nor detail specified.
|
1286
|
-
return nil if sb.empty?
|
1287
|
-
return sb.join()
|
1288
2127
|
end
|
1289
2128
|
|
1290
|
-
def
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
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
|
1297
2135
|
end
|
1298
2136
|
|
1299
|
-
def
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
if
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
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
|
1311
2156
|
end
|
1312
|
-
#; [!bm71g] ignores 'Options:' section if no options exist.
|
1313
|
-
return nil if sb.empty?
|
1314
|
-
#; [!proa4] includes description of global options.
|
1315
|
-
return build_section("Options", sb.join(), nil)
|
1316
2157
|
end
|
1317
2158
|
|
1318
|
-
def
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
#; [!yigf3] shows private (hidden) action names if 'all' flag is true.
|
1330
|
-
if all || ! Util.hidden_name?(name)
|
1331
|
-
#; [!5d9mc] shows hidden action in weak format.
|
1332
|
-
#; [!awk3l] shows important action in strong format.
|
1333
|
-
#; [!9k4dv] shows unimportant action in weak fomrat.
|
1334
|
-
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
|
1335
2170
|
end
|
1336
2171
|
end
|
1337
|
-
return build_section("Actions", sb.join(), desc)
|
1338
2172
|
end
|
1339
2173
|
|
1340
|
-
def
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
alias_name = alias_obj.alias_name
|
1347
|
-
#; [!5g72a] not show hidden alias names in default.
|
1348
|
-
#; [!ekuqm] shows all alias names including private ones if 'all' flag is true.
|
1349
|
-
if all || ! Util.hidden_name?(alias_name)
|
1350
|
-
#; [!aey2k] shows alias in strong or weak format according to action.
|
1351
|
-
sb << Util.format_help_line(format, alias_name, alias_obj.desc(), alias_obj.important?)
|
1352
|
-
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
|
1353
2180
|
end
|
1354
|
-
#; [!p3oh6] now show 'Aliases:' section if no aliases defined.
|
1355
|
-
return nil if sb.empty?
|
1356
|
-
#; [!we1l8] shows 'Aliases:' section if any aliases defined.
|
1357
|
-
return build_section("Aliases", sb.join(), nil)
|
1358
2181
|
end
|
1359
2182
|
|
1360
|
-
def
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
#; [!ckagw] adds '\n' at end of postamble text if it doesn't end with '\n'.
|
1365
|
-
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"
|
1366
2187
|
end
|
1367
|
-
|
2188
|
+
end
|
2189
|
+
|
2190
|
+
private
|
2191
|
+
|
2192
|
+
def qq(s)
|
2193
|
+
return "" if s.nil?
|
2194
|
+
return '"' + s.gsub(/"/, '\\"') + '"'
|
1368
2195
|
end
|
1369
2196
|
|
1370
2197
|
end
|
1371
2198
|
|
1372
2199
|
|
1373
|
-
|
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
|
1374
2209
|
|
1375
2210
|
|
1376
2211
|
end
|