benry-cmdapp 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/benry/cmdapp.rb CHANGED
@@ -2,1376 +2,2210 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  ###
5
- ### $Release: 0.2.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
- SCHEMA_CLASS = Benry::CmdOpt::Schema
18
- PARSER_CLASS = Benry::CmdOpt::Parser
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 BaseError < StandardError; end
37
+ class ActionError < ExecutionError
38
+ end
22
39
 
23
- class DefinitionError < BaseError; end
24
- class ActionDefError < DefinitionError; end
25
- class OptionDefError < DefinitionError; end
26
- class AliasDefError < DefinitionError; end
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 ExecutionError < BaseError; end
29
- class CommandError < ExecutionError; end
30
- class InvalidOptionError < ExecutionError; end
31
- class ActionNotFoundError < ExecutionError; end
32
- class LoopedActionError < ExecutionError; end
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 hidden_name?(name)
39
- #; [!fcfic] returns true if name is '_foo'.
40
- #; [!po5co] returns true if name is '_foo:bar'.
41
- return true if name =~ /\A_/
42
- #; [!9iqz3] returns true if name is 'foo:_bar'.
43
- return true if name =~ /:_[-\w]*\z/
44
- #; [!mjjbg] returns false if else.
45
- return false
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 schema_empty?(schema, all=false)
49
- #; [!8t5ju] returns true if schema empty.
50
- #; [!c4ljy] returns true if schema contains only private (hidden) options.
51
- schema.each {|item| return false if all || ! item.hidden? }
52
- return true
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 method2action(name)
56
- #; [!801f9] converts action name 'aa_bb_cc_' into 'aa_bb_cc'.
57
- name = name.sub(/_+\z/, '') # ex: 'aa_bb_cc_' => 'aa_bb_cc'
58
- #; [!9pahu] converts action name 'aa__bb__cc' into 'aa:bb:cc'.
59
- name = name.gsub(/__/, ':') # ex: 'aa__bb__cc' => 'aa:bb:cc'
60
- #; [!7a1s7] converts action name 'aa_bb:_cc_dd' into 'aa-bb:_cc-dd'.
61
- name = name.gsub(/(?<=\w)_/, '-') # ex: 'aa_bb:_cc_dd' => 'aa-bb:_cc-dd'
62
- return name
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 colorize?()
66
- #; [!801y1] returns $COLOR_MODE value if it is not nil.
67
- return $COLOR_MODE if $COLOR_MODE != nil
68
- #; [!0harg] returns true if stdout is a tty.
69
- #; [!u1j1x] returns false if stdout is not a tty.
70
- return $stdout.tty?
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 del_escape_seq(str)
74
- #; [!wgp2b] deletes escape sequence.
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
- class Doing # :nodoc:
79
- def inspect(); "<DOING>"; end
80
- alias to_s inspect
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
- DOING = Doing.new # :nodoc:
84
-
85
- ## (obsolete)
86
- def _important?(tag) # :nodoc:
87
- #; [!0yz2h] returns nil if tag == nil.
88
- #; [!h5pid] returns true if tag == :important.
89
- #; [!7zval] returns false if tag == :unimportant.
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 str_strong(s)
105
- return "\e[4m#{s}\e[0m"
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
- def str_weak(s)
109
- return "\e[2m#{s}\e[0m"
110
- end
177
+ end
111
178
 
112
- def format_help_line(format, name, desc, important)
113
- #; [!xx1vj] if `important == nil` then format help line with no decoration.
114
- #; [!oaxp1] if `important == true` then format help line with strong decoration.
115
- #; [!bdhh6] if `important == false` then format help line with weak decoration.
116
- if important != nil
117
- name = fill_with_decoration(format, name) {|s|
118
- important ? str_strong(s) : str_weak(s)
119
- }
120
- format = format.sub(/%-?(\d+)s/, '%s')
121
- end
122
- return format % [name, desc]
123
- end
124
-
125
- def fill_with_decoration(format, name, &block)
126
- #; [!udrbj] returns decorated string with padding by white spaces.
127
- if format =~ /%(-)?(\d+)s/
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
- class ActionIndex
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
- def initialize()
146
- @actions = {} # {action_name => ActionMetadata}
147
- @aliases = {} # {alias_name => Alias}
148
- @done = {} # {action_name => (Object|DOING)}
149
- end
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 each_action_name_and_desc(include_alias=true, all: false, &block)
172
- #; [!5lahm] yields action name, description, and important flag.
173
- #; [!27j8b] includes alias names when the first arg is true.
174
- #; [!8xt8s] rejects hidden actions if 'all: false' kwarg specified.
175
- #; [!5h7s5] includes hidden actions if 'all: true' kwarg specified.
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 get_action(action_name)
189
- return @actions[action_name.to_s]
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 register_action(action_name, action_metadata)
193
- @actions[action_name.to_s] = action_metadata
194
- action_metadata
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 delete_action(action_name)
198
- #; [!08e1s] unregisters action.
199
- #; [!zjpq0] raises error if action not registered.
200
- @actions.delete(action_name.to_s) or
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
- def action_exist?(action_name)
205
- return @actions.key?(action_name.to_s)
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
- def each_action(&block)
209
- @actions.values().each(&block)
210
- nil
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
- def action_result(action_name)
214
- return @done[action_name.to_s]
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
- def action_done(action_name, val)
218
- @done[action_name.to_s] = val
219
- val
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 action_done?(action_name)
223
- return @done.key?(action_name.to_s) && ! action_doing?(action_name)
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 action_doing(action_name)
227
- @done[action_name.to_s] = Util::DOING
228
- nil
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 action_doing?(action_name)
232
- return action_result(action_name) == Util::DOING
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 register_alias(alias_name, alias_obj)
236
- @aliases[alias_name.to_s] = alias_obj
237
- alias_obj
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
- def delete_alias(alias_name)
241
- #; [!8ls45] unregisters alias.
242
- #; [!fdfyq] raises error if alias not registered.
243
- @aliases.delete(alias_name.to_s) or
244
- raise ActionNotFoundError.new("delete_alias(#{alias_name.inspect}): Alias not found.")
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
- def get_alias(alias_name)
248
- return @aliases[alias_name.to_s]
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 alias_exist?(alias_name)
252
- return @aliases.key?(alias_name)
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 each_alias(&block)
256
- @aliases.values().each(&block)
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
- INDEX = ActionIndex.new
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
- def self.delete_action(action_name)
266
- #; [!era7d] deletes action.
267
- #; [!ifaj1] raises error if action not exist.
268
- INDEX.delete_action(action_name)
269
- end
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 ActionMetadata
494
+ class ActionScope
279
495
 
280
- def initialize(name, klass, method, desc, schema, detail: nil, postamble: nil, important: nil, tag: nil, hidden: nil)
281
- @name = name
282
- @klass = klass
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
- attr_reader :name, :method, :klass, :schema, :desc, :detail, :postamble, :important, :tag, :hidden
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 hidden?()
296
- #; [!kp10p] returns true when action method is private.
297
- #; [!nw322] returns false when action method is not private.
298
- return @hidden != nil ? @hidden : ! @klass.method_defined?(@method)
299
- end
300
-
301
- def important?()
302
- #; [!52znh] returns true if `@important == true`.
303
- #; [!rlfac] returns false if `@important == false`.
304
- #; [!j3trl] returns false if `@important == nil`. and action is hidden.
305
- #; [!hhef8] returns nil if `@important == nil`.
306
- return @important if @important != nil
307
- return false if hidden?()
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 parse_options(argv, all=true)
312
- #; [!ab3j8] parses argv and returns options.
313
- return PARSER_CLASS.new(@schema).parse(argv, all: all)
314
- #; [!56da8] raises InvalidOptionError if option value is invalid.
315
- rescue Benry::CmdOpt::OptionError => exc
316
- raise InvalidOptionError.new(exc.message)
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 run_action(*args, **kwargs)
320
- if ! $TRACE_MODE
321
- __run_action(*args, **kwargs)
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
- #; [!tubhv] if $TRACE_MODE is on, prints tracing info.
324
- #; [!zgp14] tracing info is colored when stdout is a tty.
325
- s = "## enter: #{@name}"
326
- s = "\e[33m#{s}\e[0m" if Util.colorize?
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 __run_action(*args, **kwargs)
337
- #; [!veass] runs action with args and kwargs.
338
- action_obj = _new_action_object()
339
- if kwargs.empty? # for Ruby < 2.7
340
- action_obj.__send__(@method, *args) # for Ruby < 2.7
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
- action_obj.__send__(@method, *args, **kwargs)
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 _new_action_object()
348
- return @klass.new
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 method_arity()
353
- #; [!7v4tp] returns min and max number of positional arguments.
354
- n_req = 0
355
- n_opt = 0
356
- has_rest = false
357
- @klass.instance_method(@method).parameters.each do |kind, _|
358
- case kind
359
- when :req ; n_req += 1
360
- when :opt ; n_opt += 1
361
- when :rest ; has_rest = true
362
- when :key ; nil
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 validate_method_params()
372
- #; [!plkhs] returns error message if keyword parameter for option not exist.
373
- #; [!1koi8] returns nil if all keyword parameters for option exist.
374
- kw_params = []
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 help_message(command, all=false)
384
- #; [!i7siu] returns help message of action.
385
- builder = ACTION_HELP_BUILDER_CLASS.new(self)
386
- return builder.build_help_message(command, all)
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
- ACTION_METADATA_CLASS = ActionMetadata
821
+ Action = ActionScope
393
822
 
394
823
 
395
- class ActionWithArgs
824
+ class Registry
396
825
 
397
- def initialize(action_metadata, args, kwargs)
398
- #; [!6jklb] keeps ActionMetadata, args, and kwargs.
399
- @action_metadata = action_metadata
400
- @args = args
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
- attr_reader :action_metadata, :args, :kwargs
405
-
406
- def method_missing(meth, *args, **kwargs)
407
- #; [!14li3] behaves as ActionMetadata.
408
- if kwargs.empty? # Ruby < 2.7
409
- return @action_metadata.__send__(meth, *args) # Ruby < 2.7
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 method()
416
- return @action_metadata.method
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 run_action(*args, **kwargs)
420
- #; [!fl26i] invokes action with args and kwargs.
421
- args = @args + args if @args
422
- kwargs = @kwargs.merge(kwargs) if @kwargs
423
- super(*args, **kwargs)
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
- end
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
- class HelpBuilder
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 build_section(title, content, desc=nil)
432
- #; [!cfijh] includes section title and content if specified by config.
433
- #; [!09jzn] third argument can be nil.
434
- sb = []
435
- if desc
436
- sb << heading(title) << " " << desc << "\n"
437
- else
438
- sb << heading(title) << "\n"
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
- sb << content
441
- sb << "\n" unless content.end_with?("\n")
442
- return sb.join()
890
+ return md, alias_args
443
891
  end
444
892
 
445
- def config()
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 heading(title)
450
- c = config()
451
- format = c ? c.format_heading : Config::FORMAT_HEADING
452
- return format % title
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
- end
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
- class ActionHelpBuilder < HelpBuilder
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 initialize(action_metadata)
461
- @am = action_metadata
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 build_help_message(command, all=false)
465
- sb = []
466
- sb << build_preamble(command, all)
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
- protected
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 build_preamble(command, all=false)
476
- #; [!pqoup] adds detail text into help if specified.
477
- sb = []
478
- sb << "#{command} #{@am.name} -- #{@am.desc}\n"
479
- if @am.detail
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
- return sb.join()
976
+ nil
485
977
  end
486
978
 
487
- def build_usage(command, all=false)
488
- config = $cmdapp_config
489
- format = config ? config.format_usage : Config::FORMAT_USAGE
490
- #; [!zbc4y] adds '[<options>]' into 'Usage:' section only when any options exist.
491
- #; [!8b02e] ignores '[<options>]' in 'Usage:' when only hidden options speicified.
492
- #; [!ou3md] not add extra whiespace when no arguments of command.
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
- #; [!pvu56] ignores 'Options:' section when no options exist.
522
- return nil if sb.empty?
523
- return build_section("Options", sb.join(), nil)
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
- def build_postamble(command, all=false)
527
- #; [!0p2gt] adds postamble text if specified.
528
- s = @am.postamble
529
- if s
530
- #; [!v5567] adds '\n' at end of preamble text if it doesn't end with '\n'.
531
- s += "\n" unless s.end_with?("\n")
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
- return s
1040
+ #@scope_objects.each {|_, scope| scope.__clear_recursive_reference() }
1041
+ #@scope_objects.clear()
1042
+ @status_dict.clear()
534
1043
  end
535
1044
 
536
- def config()
537
- return @cmdapp_config
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 build_argstr()
543
- #; [!x0z89] required arg is represented as '<arg>'.
544
- #; [!md7ly] optional arg is represented as '[<arg>]'.
545
- #; [!xugkz] variable args are represented as '[<arg>...]'.
546
- method_obj = @am.klass.instance_method(@am.method)
547
- sb = []; n = 0
548
- method_obj.parameters.each do |kind, param|
549
- arg = param2arg(param)
550
- case kind
551
- when :req ; sb << " <#{arg}>"
552
- when :opt ; sb << " [<#{arg}>" ; n += 1
553
- when :rest ; sb << " [<#{arg}>..." ; n += 1
554
- when :key ; nil
555
- when :keyrest ; nil
556
- else ; nil
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
- sb << ("]" * n)
560
- return sb.join()
1126
+ @status_dict[action] = :done
1127
+ #; [!ndxc3] returns true if action invoked.
1128
+ return true
561
1129
  end
562
1130
 
563
- def param2arg(param)
564
- #; [!eou4h] converts arg name 'xx_or_yy_or_zz' into 'xx|yy|zz'.
565
- #; [!naoft] converts arg name '_xx_yy_zz' into '_xx-yy-zz'.
566
- s = param.to_s
567
- s = s.gsub(/_or_/, '|') # ex: 'file_or_dir' => 'file|dir'
568
- s = s.gsub(/(?<=\w)_/, '-') # ex: 'aa_bb_cc' => 'aa-bb-cc'
569
- return s
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
- ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
1144
+ CONTEXT_CLASS = ApplicationContext
576
1145
 
577
1146
 
578
- class ActionScope
1147
+ class Config
579
1148
 
580
- def run_action_once(action_name, *args, **kwargs)
581
- #; [!oh8dc] don't invoke action if already invoked.
582
- return __run_action(action_name, true, args, kwargs)
583
- end
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 run_action_anyway(action_name, *args, **kwargs)
586
- #; [!2yrc2] invokes action even if already invoked.
587
- return __run_action(action_name, false, args, kwargs)
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
- private
1247
+ end
591
1248
 
592
- def __run_action(action_name, once, args, kwargs)
593
- #; [!lbp9r] invokes action name with prefix if prefix defined.
594
- #; [!7vszf] raises error if action specified not found.
595
- prefix = self.class.instance_variable_get('@__prefix__')
596
- metadata = INDEX.lookup_action("#{prefix}#{action_name}") || \
597
- INDEX.lookup_action(action_name) or
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
- def self.prefix(str, alias_of: nil, action: nil)
615
- #; [!1gwyv] converts symbol into string.
616
- str = str.to_s
617
- #; [!pz46w] error if prefix contains extra '_'.
618
- str =~ /\A\w[-a-zA-Z0-9]*(:\w[-a-zA-Z0-9]*)*\z/ or
619
- raise ActionDefError.new("#{str}: Invalid prefix name (please use ':' or '-' instead of '_' as word separator).")
620
- #; [!9pu01] adds ':' at end of prefix name if prefix not end with ':'.
621
- str += ':' unless str.end_with?(':')
622
- @__prefix__ = str
623
- @__aliasof__ = alias_of # method name if symbol, or action name if string
624
- @__default__ = action # method name if symbol, or action name if string
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
- SUBCLASSES = []
1270
+ protected
628
1271
 
629
- def self.inherited(subclass)
630
- #; [!f826w] registers all subclasses into 'ActionScope::SUBCLASSES'.
631
- SUBCLASSES << subclass
632
- #; [!2imrb] sets class instance variables in subclass.
633
- subclass.instance_eval do
634
- @__action__ = nil # ex: ["action desc", {detail: nil, postamble: nil}]
635
- @__option__ = nil # Benry::CmdOpt::Schema object
636
- @__prefix__ = nil # ex: "foo:bar:"
637
- @__aliasof__ = nil # ex: :method_name or "action-name"
638
- @__default__ = nil # ex: :method_name or "action-name"
639
- #; [!1qv12] @action is a Proc object and saves args.
640
- @action = proc do |desc, detail: nil, postamble: nil, important: nil, tag: nil, hidden: nil|
641
- @__action__ = [desc, {detail: detail, postamble: postamble, important: important, tag: tag, hidden: hidden}]
642
- end
643
- #; [!33ma7] @option is a Proc object and saves args.
644
- @option = proc do |param, optdef, desc, *rest, type: nil, rexp: nil, enum: nil, range: nil, value: nil, detail: nil, important: nil, tag: nil, hidden: nil, &block|
645
- #; [!gxybo] '@option.()' raises error when '@action.()' not called.
646
- @__action__ != nil or
647
- raise OptionDefError.new("@option.(#{param.inspect}): `@action.()` Required but not called.")
648
- schema = (@__option__ ||= SCHEMA_CLASS.new)
649
- #; [!ga6zh] '@option.()' raises error when invalid option info specified.
650
- begin
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 self.method_added(method)
668
- #; [!idh1j] do nothing if '@__action__' is nil.
669
- return unless @__action__
670
- #; [!ernnb] clears both '@__action__' and '@__option__'.
671
- desc, kws = @__action__
672
- schema = @__option__ || SCHEMA_CLASS.new
673
- @__action__ = @__option__ = nil
674
- #; [!n8tem] creates ActionMetadata object if '@__action__' is not nil.
675
- name = __method2action(method)
676
- metadata = ACTION_METADATA_CLASS.new(name, self, method, desc, schema, **kws)
677
- #; [!4pbsc] raises error if keyword param for option not exist in method.
678
- errmsg = metadata.validate_method_params()
679
- errmsg == nil or
680
- raise ActionDefError.new("def #{method}(): #{errmsg}")
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 name
716
- end
717
-
718
- def self.__define_alias_of_action(method, action_name)
719
- return if @__aliasof__ == nil
720
- @__prefix__ != nil or raise "** internal error"
721
- alias_of = @__aliasof__
722
- if alias_of == method || alias_of == Util.method2action(method.to_s)
723
- alias_name = @__prefix__.chomp(":")
724
- #; [!349nr] raises error when same name action or alias with prefix already exists.
725
- Benry::CmdApp.action_alias(alias_name, action_name)
726
- #; [!tvjb0] clears '@__aliasof__' only when alias created.
727
- @__aliasof__ = nil
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
- Action = ActionScope
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
- class BuiltInAction < ActionScope
1375
+ protected
738
1376
 
739
- @action.("print help message (of action)")
740
- @option.(:all, "-a, --all", "show private (hidden) options, too")
741
- def help(action=nil, all: false)
742
- action_name = action
743
- #; [!jfgsy] prints help message of action if action name specified.
744
- if action_name
745
- action_metadata = INDEX.get_action(action_name) or
746
- raise ActionNotFoundError.new("#{action}: Action not found.")
747
- help_builder = ACTION_HELP_BUILDER_CLASS.new(action_metadata)
748
- config = $cmdapp_config
749
- msg = help_builder.build_help_message(config.app_command, all)
750
- #; [!fhpjg] prints help message of command if action name not specified.
751
- else
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
- #; [!6g7jh] prints colorized help message when color mode is on.
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
- end
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
- def self.action_alias(alias_name, action_name, *args, important: nil, tag: nil)
765
- invocation = "action_alias(#{alias_name.inspect}, #{action_name.inspect})"
766
- #; [!5immb] convers both alias name and action name into string.
767
- alias_ = alias_name.to_s
768
- action_ = action_name.to_s
769
- #; [!nrz3d] error if action not found.
770
- INDEX.action_exist?(action_) or
771
- raise AliasDefError.new("#{invocation}: Action not found.")
772
- #; [!vvmwd] error when action with same name as alias exists.
773
- ! INDEX.action_exist?(alias_) or
774
- raise AliasDefError.new("#{invocation}: Not allowed to define same name alias as existing action.")
775
- #; [!i9726] error if alias already defined.
776
- ! INDEX.alias_exist?(alias_) or
777
- raise AliasDefError.new("#{invocation}: Alias name duplicated.")
778
- #; [!vzlrb] registers alias name with action name.
779
- #; [!0cq6o] supports args.
780
- #; [!4wtxj] supports 'tag:' keyword arg.
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
- class Alias
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 initialize(alias_name, action_name, *args, important: nil, tag: nil)
788
- @alias_name = alias_name
789
- @action_name = action_name
790
- @args = args.freeze if ! args.empty?
791
- @important = important if important != nil
792
- @tag = tag if tag != nil
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
- attr_reader :alias_name, :action_name, :args, :important, :tag
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 desc()
798
- if @args && ! @args.empty?
799
- return "alias of '#{@action_name} #{@args.join(' ')}'"
800
- else
801
- return "alias of '#{@action_name}' action"
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 important?()
806
- #; [!5juwq] returns true if `@important == true`.
807
- #; [!1gnbc] returns false if `@important == false`.
808
- return @important if @important != nil
809
- #; [!h3nm3] returns true or false according to action object if `@important == nil`.
810
- action_obj = INDEX.get_action(@action_name)
811
- return action_obj.important?
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 Config #< BasicObject
1574
+ class ActionHelpBuilder < BaseHelpBuilder
818
1575
 
819
- #FORMAT_HELP = " %-18s : %s"
820
- FORMAT_HELP = " \e[1m%-18s\e[0m : %s" # bold
821
- #FORMAT_HELP = " \e[34m%-18s\e[0m : %s" # blue
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
- FORMAT_APPNAME = "\e[1m%s\e[0m"
1592
+ protected
824
1593
 
825
- #FORMAT_USAGE = " $ %s %s"
826
- FORMAT_USAGE = " $ \e[1m%s\e[0m %s" # bold
827
- #FORMAT_USAGE = " $ \e[34m%s\e[0m %s" # blue
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
- #FORMAT_HEADING = "%s:"
830
- #FORMAT_HEADING = "\e[1m%s:\e[0m" # bold
831
- #FORMAT_HEADING = "\e[1;4m%s:\e[0m" # bold, underline
832
- FORMAT_HEADING = "\e[34m%s:\e[0m" # blue
833
- #FORMAT_HEADING = "\e[33;4m%s:\e[0m" # yellow, underline
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 initialize(app_desc, app_version=nil,
836
- app_name: nil, app_command: nil, app_detail: nil,
837
- default_action: "help", default_help: false,
838
- option_help: true, option_all: false,
839
- option_verbose: false, option_quiet: false, option_color: false,
840
- option_debug: false, option_trace: false,
841
- help_action: true, help_aliases: false, help_sections: [], help_postamble: nil,
842
- format_help: nil, format_appname: nil, format_usage: nil, format_heading: nil,
843
- feat_candidate: true)
844
- #; [!uve4e] sets command name automatically if not provided.
845
- @app_desc = app_desc # ex: "sample application"
846
- @app_version = app_version # ex: "1.0.0"
847
- @app_name = app_name || ::File.basename($0) # ex: "MyApp"
848
- @app_command = app_command || ::File.basename($0) # ex: "myapp"
849
- @app_detail = app_detail # ex: "See https://.... for details.\n"
850
- @default_action = default_action # default action name
851
- @default_help = default_help # print help message if action not specified
852
- @option_help = option_help # '-h' and '--help' are enabled when true
853
- @option_all = option_all # '-a' and '--all' are enable when true
854
- @option_verbose = option_verbose # '-v' and '--verbose' are enabled when true
855
- @option_quiet = option_quiet # '-q' and '--quiet' are enabled when true
856
- @option_color = option_color # '--color[=<on|off>]' enabled when true
857
- @option_debug = option_debug # '-D' and '--debug' are enabled when true
858
- @option_trace = option_trace # '-T' and '--trace' are enabled when true
859
- @help_action = help_action # define built-in 'help' action when true
860
- @help_aliases = help_aliases # 'Aliases:' section printed when true
861
- @help_sections = help_sections # ex: [["Example", "..text.."], ...]
862
- @help_postamble = help_postamble # ex: "(Tips: ....)\n"
863
- @format_help = format_help || FORMAT_HELP
864
- @format_appname = format_appname || FORMAT_APPNAME
865
- @format_usage = format_usage || FORMAT_USAGE
866
- @format_heading = format_heading || FORMAT_HEADING
867
- @feat_candidate = feat_candidate # if arg is 'foo:', list actions starting with 'foo:'
868
- end
869
-
870
- attr_accessor :app_desc, :app_version, :app_name, :app_command, :app_detail
871
- attr_accessor :default_action, :default_help
872
- attr_accessor :option_help, :option_all
873
- attr_accessor :option_verbose, :option_quiet, :option_color
874
- attr_accessor :option_debug, :option_trace
875
- attr_accessor :help_action, :help_aliases, :help_sections, :help_postamble
876
- attr_accessor :format_help, :format_appname, :format_usage, :format_heading
877
- attr_accessor :feat_candidate
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
- class AppOptionSchema < Benry::CmdOpt::Schema
1677
+ APPLICATION_HELP_BUILDER_CLASS = ApplicationHelpBuilder
1678
+ ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
1679
+
1680
+
1681
+ class GlobalOptionSchema < OptionSchema
883
1682
 
884
- def initialize(config=nil)
1683
+ def initialize(config)
885
1684
  super()
886
- #; [!3ihzx] do nothing when config is nil.
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
- return nil if c == nil
889
- #; [!tq2ol] adds '-h, --help' option if 'config.option_help' is set.
890
- add(:help , "-h, --help" , "print help message") if c.option_help
891
- #; [!mbtw0] adds '-V, --version' option if 'config.app_version' is set.
892
- add(:version, "-V, --version", "print version") if c.app_version
893
- #; [!f5do6] adds '-a, --all' option if 'config.option_all' is set.
894
- add(:all , "-a, --all" , "list all actions including private (hidden) ones") if c.option_all
895
- #; [!cracf] adds '-v, --verbose' option if 'config.option_verbose' is set.
896
- add(:verbose, "-v, --verbose", "verbose mode") if c.option_verbose
897
- #; [!2vil6] adds '-q, --quiet' option if 'config.option_quiet' is set.
898
- add(:quiet , "-q, --quiet" , "quiet mode") if c.option_quiet
899
- #; [!6zw3j] adds '--color=<on|off>' option if 'config.option_color' is set.
900
- add(:color , "--color[=<on|off>]", "enable/disable color", type: TrueClass) if c.option_color
901
- #; [!29wfy] adds '-D, --debug' option if 'config.option_debug' is set.
902
- add(:debug , "-D, --debug" , "debug mode (set $DEBUG_MODE to true)") if c.option_debug
903
- #; [!s97go] adds '-T, --trace' option if 'config.option_trace' is set.
904
- add(:trace , "-T, --trace" , "report enter into and exit from actions") if c.option_trace
905
- end
906
-
907
- def sort_options_in_this_order(*keys)
908
- #; [!6udxr] sorts options in order of keys specified.
909
- #; [!8hhuf] options which key doesn't appear in keys are moved at end of options.
910
- len = @items.length
911
- @items.sort_by! {|item| keys.index(item.key) || @items.index(item) + len }
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
- APP_OPTION_SCHEMA_CLASS = AppOptionSchema
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, schema=nil, help_builder=nil, &callback)
924
- @config = config
925
- #; [!h786g] acceps callback block.
926
- @callback = callback
927
- #; [!jkprn] creates option schema object according to config.
928
- @schema = schema || do_create_global_option_schema(config)
929
- @help_builder = help_builder
930
- @global_options = nil
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
- attr_reader :config, :schema, :help_builder, :callback
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 main(argv=ARGV, &block)
936
- begin
937
- #; [!y6q9z] runs action with options.
938
- self.run(*argv)
939
- rescue ExecutionError, DefinitionError => exc
940
- #; [!6ro6n] not catch error when $DEBUG_MODE is on.
941
- raise if $DEBUG_MODE
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
- #; [!qk5q5] returns 1 as exit code when error occurred.
956
- return 1
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
- #; [!5oypr] returns 0 as exit code when no errors occurred.
959
- return 0
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 run(*args)
964
- #; [!t4ypg] sets $cmdapp_config at beginning.
965
- do_setup()
966
- #; [!pyotc] sets global options to '@global_options'.
967
- global_opts = do_parse_global_options(args)
968
- @global_options = global_opts
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 do_create_global_option_schema(config)
1013
- #; [!u3zdg] creates global option schema object according to config.
1014
- return APP_OPTION_SCHEMA_CLASS.new(config)
1827
+ def setup()
1828
+ #; [!6hi1y] stores current application.
1829
+ Benry::CmdApp._set_current_app(self)
1015
1830
  end
1016
1831
 
1017
- def do_create_help_message_builder(config, schema)
1018
- #; [!pk5da] creates help message builder object.
1019
- return APP_HELP_BUILDER_CLASS.new(config, schema)
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 do_parse_global_options(args)
1023
- #; [!5br6t] parses only global options and not parse action options.
1024
- parser = PARSER_CLASS.new(@schema)
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
- #; [!kklah] raises InvalidOptionError if global option value is invalid.
1028
- rescue Benry::CmdOpt::OptionError => exc
1029
- raise InvalidOptionError.new(exc.message)
1030
- end
1031
-
1032
- def do_toggle_global_switches(_args, global_opts)
1033
- #; [!j6u5x] sets $QUIET_MODE to false if '-v' or '--verbose' specified.
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 do_handle_global_options(args, global_opts)
1051
- #; [!xvj6s] prints help message if '-h' or '--help' specified.
1052
- #; [!lpoz7] prints help message of action if action name specified with help option.
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
- do_print_help_message(args, global_opts)
1055
- do_validate_actions(args, global_opts)
1056
- throw :SKIP # done
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
- #; [!fslsy] prints version if '-V' or '--version' specified.
1871
+ #; [!dkjw8] prints version number if global option `-V, --version` specified.
1059
1872
  if global_opts[:version]
1060
- puts @config.app_version
1061
- throw :SKIP # done
1873
+ print_str render_version()
1874
+ return 0
1062
1875
  end
1063
- end
1064
-
1065
- def do_callback(args, global_opts)
1066
- #; [!xwo0v] calls callback if provided.
1067
- #; [!lljs1] calls callback only once.
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
- end
1073
-
1074
- def do_find_action(args, _global_opts)
1075
- c = @config
1076
- #; [!bm8np] returns action metadata.
1077
- if ! args.empty?
1078
- action_name = args.shift()
1079
- #; [!vl0zr] error when action not found.
1080
- metadata = INDEX.lookup_action(action_name) or
1081
- raise CommandError.new("#{action_name}: Unknown action.")
1082
- #; [!gucj7] if no action specified, finds default action instead.
1083
- elsif c.default_action
1084
- action_name = c.default_action
1085
- #; [!388rs] error when default action not found.
1086
- metadata = INDEX.lookup_action(action_name) or
1087
- raise CommandError.new("#{action_name}: Unknown default action.")
1088
- #; [!drmls] returns nil if no action specified but 'config.default_help' is set.
1089
- elsif c.default_help
1090
- #do_print_help_message([])
1091
- return nil
1092
- #; [!hs589] error when action nor default action not specified.
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
- raise CommandError.new("#{c.app_command}: Action name required (run `#{c.app_command} -h` for details).")
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 do_run_action(metadata, args, _global_opts)
1100
- action_name = metadata.name
1101
- #; [!62gv9] parses action options even if specified after args.
1102
- options = metadata.parse_options(args, true)
1103
- #; [!6mlol] error if action requries argument but nothing specified.
1104
- #; [!72jla] error if action requires N args but specified less than N args.
1105
- #; [!zawxe] error if action requires N args but specified over than N args.
1106
- #; [!y97o3] action can take any much args if action has variable arg.
1107
- min, max = metadata.method_arity()
1108
- n = args.length
1109
- if n < min
1110
- raise CommandError.new("#{action_name}: Argument required.") if n == 0
1111
- raise CommandError.new("#{action_name}: Too less arguments (at least #{min}).")
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
- #; [!h7lon] reports warning if `action:` specified in action class but corresponding action not exist.
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 do_print_candidates(args, _global_opts)
1166
- #; [!0e8vt] prints candidate action names including prefix name without tailing ':'.
1167
- prefix = args[0]
1168
- prefix2 = prefix.chomp(':')
1169
- pairs = []
1170
- aname2aliases = {}
1171
- INDEX.each_action do |ameta|
1172
- aname = ameta.name
1173
- next unless aname.start_with?(prefix) || aname == prefix2
1174
- #; [!k3lw0] private (hidden) action should not be printed as candidates.
1175
- next if ameta.hidden?
1176
- #
1177
- pairs << [aname, ameta.desc, ameta.important?]
1178
- aname2aliases[aname] = []
1179
- end
1180
- #; [!85i5m] candidate actions should include alias names.
1181
- INDEX.each_alias do |ali_obj|
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
- sb << @config.format_heading % "Actions" << "\n"
1196
- format = @config.format_help
1197
- indent = " " * (Util.del_escape_seq(format) % ['', '']).length
1198
- pairs.sort_by {|aname, _, _| aname }.each do |aname, adesc, important|
1199
- #; [!j4b54] shows candidates in strong format if important.
1200
- #; [!q3819] shows candidates in weak format if not important.
1201
- sb << Util.format_help_line(format, aname, adesc, important) << "\n"
1202
- aliases = aname2aliases[aname]
1203
- if aliases && ! aliases.empty?
1204
- sb << indent << "(alias: " << aliases.join(", ") << ")\n"
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
- s = sb.join()
1208
- s = Util.del_escape_seq(s) unless Util.colorize?
1209
- puts s
1210
- end
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 do_teardown(exc)
1224
- #; [!zxeo7] clears '$cmdapp_config'.
1225
- $cmdapp_config = nil
1226
- #; [!ufm1d] clears '$cmdapp_application'.
1227
- $cmdapp_application = nil
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
- public
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 help_message(all=false, format=nil)
1233
- #; [!owg9y] returns help message.
1234
- @help_builder ||= do_create_help_message_builder(@config, @schema)
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 AppHelpBuilder < HelpBuilder
2074
+ class MetadataRenderer
1242
2075
 
1243
- def initialize(config, schema)
1244
- @config = config
1245
- @schema = schema
2076
+ def initialize(registry)
2077
+ @registry = registry
1246
2078
  end
1247
2079
 
1248
- def build_help_message(all=false, format=nil)
1249
- #; [!rvpdb] returns help message.
1250
- format ||= @config.format_help
2080
+ def render_metadata(all: false)
2081
+ #; [!gduge] renders registry data in YAML format.
1251
2082
  sb = []
1252
- sb << build_preamble(all)
1253
- sb << build_usage(all)
1254
- sb << build_options(all, format)
1255
- sb << build_actions(all, format)
1256
- #; [!oxpda] prints 'Aliases:' section only when 'config.help_aliases' is true.
1257
- sb << build_aliases(all, format) if @config.help_aliases
1258
- @config.help_sections.each do |title, content, desc|
1259
- #; [!kqnxl] array of section may have two or three elements.
1260
- sb << build_section(title, content, desc)
1261
- end if @config.help_sections
1262
- sb << build_postamble(all)
1263
- return sb.reject {|s| s.nil? || s.empty? }.join("\n")
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 build_preamble(all=false)
1269
- #; [!34y8e] includes application name specified by config.
1270
- #; [!744lx] includes application description specified by config.
1271
- #; [!d1xz4] includes version number if specified by config.
1272
- c = @config
1273
- sb = []
1274
- if c.app_desc
1275
- app_name = c.format_appname % c.app_name
1276
- ver = c.app_version ? " (#{c.app_version})" : ""
1277
- sb << "#{app_name}#{ver} -- #{c.app_desc}\n"
1278
- end
1279
- #; [!775jb] includes detail text if specified by config.
1280
- #; [!t3tbi] adds '\n' before detail text only when app desc specified.
1281
- if c.app_detail
1282
- sb << "\n" unless sb.empty?
1283
- sb << c.app_detail
1284
- sb << "\n" unless c.app_detail.end_with?("\n")
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 build_usage(all=false)
1292
- c = @config
1293
- format = c.format_usage + "\n"
1294
- #; [!o176w] includes command name specified by config.
1295
- sb = []
1296
- sb << (format % [c.app_command, "[<options>] [<action> [<arguments>...]]"])
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 build_options(all=false, format=nil)
1301
- format ||= @config.format_help
1302
- format += "\n"
1303
- #; [!in3kf] ignores private (hidden) options.
1304
- #; [!ywarr] not ignore private (hidden) options if 'all' flag is true.
1305
- sb = []
1306
- @schema.each do |item|
1307
- if all || ! item.hidden?
1308
- #; [!p1tu9] prints option in weak format if option is hidden.
1309
- important = item.hidden? ? false : nil
1310
- sb << Util.format_help_line(format, item.optdef, item.desc, important)
1311
- end
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 build_actions(all=false, format=nil)
1320
- c = @config
1321
- format ||= c.format_help
1322
- format += "\n"
1323
- sb = []
1324
- #; [!df13s] includes default action name if specified by config.
1325
- desc = c.default_action ? "(default: #{c.default_action})" : nil
1326
- #; [!jat15] includes action names ordered by name.
1327
- include_alias = ! @config.help_aliases
1328
- INDEX.each_action_name_and_desc(include_alias, all: all) do |name, desc, important|
1329
- #; [!b3l3m] not show private (hidden) action names in default.
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 build_aliases(all=false, format=nil)
1342
- format ||= @config.format_help
1343
- format += "\n"
1344
- #; [!tri8x] includes alias names in order of registration.
1345
- sb = []
1346
- INDEX.each_alias do |alias_obj|
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 build_postamble(all=false)
1362
- #; [!i04hh] includes postamble text if specified by config.
1363
- s = @config.help_postamble
1364
- if s
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
- return s
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
- APP_HELP_BUILDER_CLASS = AppHelpBuilder
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