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