benry-cmdapp 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/benry/cmdapp.rb CHANGED
@@ -2,1375 +2,2210 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  ###
5
- ### $Release: 0.1.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)
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
496
+ def initialize(config, context=nil)
497
+ @config = config
498
+ @__context__ = context || CONTEXT_CLASS.new(config)
290
499
  end
291
500
 
292
- attr_reader :name, :method, :klass, :schema, :desc, :detail, :postamble, :important, :tag
501
+ def __clear_recursive_reference() # :nodoc:
502
+ #; [!i68z0] clears instance var which refers context object.
503
+ @__context__ = nil
504
+ nil
505
+ end
293
506
 
294
- def hidden?()
295
- #; [!kp10p] returns true when action method is private.
296
- #; [!nw322] returns false when action method is not private.
297
- return ! @klass.method_defined?(@method)
298
- end
299
-
300
- def important?()
301
- #; [!52znh] returns true if `@important == true`.
302
- #; [!rlfac] returns false if `@important == false`.
303
- #; [!j3trl] returns false if `@important == nil`. and action is hidden.
304
- #; [!hhef8] returns nil if `@important == nil`.
305
- return @important if @important != nil
306
- 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."
307
668
  return nil
308
669
  end
670
+ private_class_method :__validate_kwargs
309
671
 
310
- def parse_options(argv, all=true)
311
- #; [!ab3j8] parses argv and returns options.
312
- return PARSER_CLASS.new(@schema).parse(argv, all: all)
313
- #; [!56da8] raises InvalidOptionError if option value is invalid.
314
- rescue Benry::CmdOpt::OptionError => exc
315
- 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
316
681
  end
682
+ private_class_method :__validate_action_method
317
683
 
318
- def run_action(*args, **kwargs)
319
- if ! $TRACE_MODE
320
- __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...
321
722
  else
322
- #; [!tubhv] if $TRACE_MODE is on, prints tracing info.
323
- #; [!zgp14] tracing info is colored when stdout is a tty.
324
- s = "## enter: #{@name}"
325
- s = "\e[33m#{s}\e[0m" if Util.colorize?
326
- puts s
327
- __run_action(*args, **kwargs)
328
- s = "## exit: #{@name}"
329
- s = "\e[33m#{s}\e[0m" if Util.colorize?
330
- puts s
723
+ #; [!tgux9] just stores arguments into class.
724
+ @__prefixdef__ = [prefix, action, alias_for]
725
+ #; [!ncskq] registers prefix and description, even if no actions defined.
726
+ REGISTRY.category_add(prefix, desc)
331
727
  end
332
728
  nil
333
729
  end
334
730
 
335
- def __run_action(*args, **kwargs)
336
- #; [!veass] runs action with args and kwargs.
337
- action_obj = _new_action_object()
338
- if kwargs.empty? # for Ruby < 2.7
339
- 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.
340
768
  else
341
- action_obj.__send__(@method, *args, **kwargs)
769
+ action_name = prefix + action_name
342
770
  end
771
+ #; [!0dkrj] keyword arguments are passed to higher function.
772
+ Benry::CmdApp.define_alias(alias_name, action_name, tag: tag, important: important, hidden: hidden)
343
773
  end
344
- private :__run_action
345
774
 
346
- def _new_action_object()
347
- 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)
348
787
  end
349
- protected :_new_action_object
350
788
 
351
- def method_arity()
352
- #; [!7v4tp] returns min and max number of positional arguments.
353
- n_req = 0
354
- n_opt = 0
355
- has_rest = false
356
- @klass.instance_method(@method).parameters.each do |kind, _|
357
- case kind
358
- when :req ; n_req += 1
359
- when :opt ; n_opt += 1
360
- when :rest ; has_rest = true
361
- when :key ; nil
362
- when :keyrest ; nil
363
- else ; nil
364
- end
365
- end
366
- #; [!w3rer] max is nil if variable argument exists.
367
- return has_rest ? [n_req, nil] : [n_req, n_req + n_opt]
789
+ def run_once(action_name, *args, **kwargs)
790
+ #; [!nqjxk] runs action and returns true if not runned ever.
791
+ #; [!wcyut] not run action and returns false if already runned.
792
+ ctx = (@__context__ ||= CONTEXT_CLASS.new)
793
+ return ctx.invoke_action(action_name, args, kwargs, once: true)
794
+ end
795
+
796
+ def run_action(action_name, *args, **kwargs)
797
+ #; [!uwi68] runs action and returns true.
798
+ ctx = (@__context__ ||= CONTEXT_CLASS.new)
799
+ return ctx.invoke_action(action_name, args, kwargs, once: false)
368
800
  end
369
801
 
370
- def validate_method_params()
371
- #; [!plkhs] returns error message if keyword parameter for option not exist.
372
- #; [!1koi8] returns nil if all keyword parameters for option exist.
373
- kw_params = []
374
- method_obj = @klass.instance_method(@method)
375
- method_obj.parameters.each {|kind, param| kw_params << param if kind == :key }
376
- opt_keys = @schema.each.collect {|item| item.key }
377
- key = (opt_keys - kw_params).first
378
- return nil if key == nil
379
- return "Should have keyword parameter '#{key}' for '@option.(#{key.inspect})', but not."
802
+ def at_end(&block)
803
+ #; [!3mqcz] registers proc object to context object.
804
+ @__context__._add_end_block(block)
805
+ nil
380
806
  end
381
807
 
382
- def help_message(command, all=false)
383
- #; [!i7siu] returns help message of action.
384
- builder = ACTION_HELP_BUILDER_CLASS.new(self)
385
- 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)
386
816
  end
387
817
 
388
818
  end
389
819
 
390
820
 
391
- ACTION_METADATA_CLASS = ActionMetadata
821
+ Action = ActionScope
392
822
 
393
823
 
394
- class ActionWithArgs
824
+ class Registry
395
825
 
396
- def initialize(action_metadata, args, kwargs)
397
- #; [!6jklb] keeps ActionMetadata, args, and kwargs.
398
- @action_metadata = action_metadata
399
- @args = args
400
- @kwargs = kwargs
826
+ def initialize()
827
+ @metadata_dict = {} # {name => (ActionMetadata|AliasMetadata)}
828
+ @category_dict = {} # {prefix => description}
829
+ @abbrev_dict = {}
401
830
  end
402
831
 
403
- attr_reader :action_metadata, :args, :kwargs
404
-
405
- def method_missing(meth, *args, **kwargs)
406
- #; [!14li3] behaves as ActionMetadata.
407
- if kwargs.empty? # Ruby < 2.7
408
- return @action_metadata.__send__(meth, *args) # Ruby < 2.7
409
- else
410
- return @action_metadata.__send__(meth, *args, **kwargs)
411
- end
832
+ def metadata_add(metadata)
833
+ ! @metadata_dict.key?(metadata.name) or raise "** assertion failed: metadata.name=#{metadata.name.inspect}"
834
+ #; [!8bhxu] registers metadata with it's name as key.
835
+ @metadata_dict[metadata.name] = metadata
836
+ #; [!k07kp] returns registered metadata objet.
837
+ return metadata
412
838
  end
413
839
 
414
- def method()
415
- 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]
416
844
  end
417
845
 
418
- def run_action(*args, **kwargs)
419
- #; [!fl26i] invokes action with args and kwargs.
420
- args = @args + args if @args
421
- kwargs = @kwargs.merge(kwargs) if @kwargs
422
- 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)
423
851
  end
424
852
 
425
- 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
426
858
 
859
+ def metadata_each(all: true, &b)
860
+ #; [!3l6r7] returns Enumerator object if block not given.
861
+ return enum_for(:metadata_each, all: all) unless block_given?()
862
+ #; [!r8mb3] yields each metadata object if block given.
863
+ #; [!qvc77] ignores hidden metadata if `all: false` passed.
864
+ @metadata_dict.keys.sort.each do |name|
865
+ metadata = @metadata_dict[name]
866
+ yield metadata if all || ! metadata.hidden?
867
+ end
868
+ nil
869
+ end
427
870
 
428
- 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
429
880
 
430
- def build_section(title, content, desc=nil)
431
- #; [!cfijh] includes section title and content if specified by config.
432
- #; [!09jzn] third argument can be nil.
433
- sb = []
434
- if desc
435
- sb << heading(title) << " " << desc << "\n"
436
- else
437
- 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)
438
889
  end
439
- sb << content
440
- sb << "\n" unless content.end_with?("\n")
441
- return sb.join()
890
+ return md, alias_args
442
891
  end
443
892
 
444
- 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
445
899
  nil
446
900
  end
447
901
 
448
- def heading(title)
449
- c = config()
450
- format = c ? c.format_heading : Config::FORMAT_HEADING
451
- 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
452
911
  end
453
912
 
454
- 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
455
918
 
919
+ def category_each(&block)
920
+ #; [!67r3i] returns Enumerator object if block not given.
921
+ return enum_for(:category_each) unless block_given?()
922
+ #; [!g3d1z] yields block with each prefix and desc.
923
+ @category_dict.each(&block)
924
+ nil
925
+ end
456
926
 
457
- 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
458
953
 
459
- def initialize(action_metadata)
460
- @am = action_metadata
954
+ def abbrev_add(abbrev, prefix)
955
+ #; [!n475k] registers abbrev with prefix.
956
+ @abbrev_dict[abbrev] = prefix
957
+ nil
461
958
  end
462
959
 
463
- def build_help_message(command, all=false)
464
- sb = []
465
- sb << build_preamble(command, all)
466
- sb << build_usage(command, all)
467
- sb << build_options(command, all)
468
- sb << build_postamble(command, all)
469
- return sb.reject {|x| x.nil? || x.empty? }.join("\n")
960
+ def abbrev_get_prefix(abbrev)
961
+ #; [!h1dvb] returns prefix bound to abbrev.
962
+ return @abbrev_dict[abbrev]
470
963
  end
471
964
 
472
- protected
965
+ def abbrev_exist?(abbrev)
966
+ #; [!tjbdy] returns true/false if abbrev registered or not.
967
+ return @abbrev_dict.key?(abbrev)
968
+ end
473
969
 
474
- def build_preamble(command, all=false)
475
- #; [!pqoup] adds detail text into help if specified.
476
- sb = []
477
- sb << "#{command} #{@am.name} -- #{@am.desc}\n"
478
- if @am.detail
479
- sb << "\n"
480
- sb << @am.detail
481
- sb << "\n" unless @am.detail.end_with?("\n")
970
+ def abbrev_each()
971
+ #; [!2oo4o] yields each abbrev name and prefix.
972
+ @abbrev_dict.keys.sort.each do |abbrev|
973
+ prefix = @abbrev_dict[abbrev]
974
+ yield abbrev, prefix
482
975
  end
483
- return sb.join()
976
+ nil
484
977
  end
485
978
 
486
- def build_usage(command, all=false)
487
- config = $cmdapp_config
488
- format = config ? config.format_usage : Config::FORMAT_USAGE
489
- #; [!zbc4y] adds '[<options>]' into 'Usage:' section only when any options exist.
490
- #; [!8b02e] ignores '[<options>]' in 'Usage:' when only hidden options speicified.
491
- #; [!ou3md] not add extra whiespace when no arguments of command.
492
- s = build_argstr().strip()
493
- s = "[<options>] " + s unless Util.schema_empty?(@am.schema, all)
494
- s = s.rstrip()
495
- sb = []
496
- sb << (format % ["#{command} #{@am.name}", s]) << "\n"
497
- return build_section("Usage", sb.join(), nil)
498
- end
499
-
500
- def build_options(command, all=false)
501
- config = $cmdapp_config
502
- format = config ? config.format_help : Config::FORMAT_HELP
503
- format += "\n"
504
- #; [!g2ju5] adds 'Options:' section.
505
- sb = []; width = nil; indent = nil
506
- @am.schema.each do |item|
507
- #; [!hghuj] ignores 'Options:' section when only hidden options speicified.
508
- next unless all || ! item.hidden?
509
- #; [!vqqq1] hidden option should be shown in weak format.
510
- important = item.hidden? ? false : nil
511
- sb << Util.format_help_line(format, item.optdef, item.desc, important)
512
- #; [!dukm7] includes detailed description of option.
513
- if item.detail
514
- width ||= (Util.del_escape_seq(format % ["", ""])).length
515
- indent ||= " " * (width - 1) # `-1` means "\n"
516
- sb << item.detail.gsub(/^/, indent)
517
- sb << "\n" unless item.detail.end_with?("\n")
518
- end
979
+ def abbrev_resolve(action)
980
+ #; [!n7zsy] replaces abbrev in action name with prefix.
981
+ if action =~ /\A[-\w]+:/
982
+ abbrev = $&; rest = $'
983
+ prefix = @abbrev_dict[abbrev]
984
+ return prefix + rest if prefix
519
985
  end
520
- #; [!pvu56] ignores 'Options:' section when no options exist.
521
- return nil if sb.empty?
522
- 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
523
1010
  end
524
1011
 
525
- def build_postamble(command, all=false)
526
- #; [!0p2gt] adds postamble text if specified.
527
- s = @am.postamble
528
- if s
529
- #; [!v5567] adds '\n' at end of preamble text if it doesn't end with '\n'.
530
- 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()
531
1039
  end
532
- return s
1040
+ #@scope_objects.each {|_, scope| scope.__clear_recursive_reference() }
1041
+ #@scope_objects.clear()
1042
+ @status_dict.clear()
533
1043
  end
534
1044
 
535
- def config()
536
- 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)
537
1085
  end
538
1086
 
539
1087
  private
540
1088
 
541
- def build_argstr()
542
- #; [!x0z89] required arg is represented as '<arg>'.
543
- #; [!md7ly] optional arg is represented as '[<arg>]'.
544
- #; [!xugkz] variable args are represented as '[<arg>...]'.
545
- method_obj = @am.klass.instance_method(@am.method)
546
- sb = []; n = 0
547
- method_obj.parameters.each do |kind, param|
548
- arg = param2arg(param)
549
- case kind
550
- when :req ; sb << " <#{arg}>"
551
- when :opt ; sb << " [<#{arg}>" ; n += 1
552
- when :rest ; sb << " [<#{arg}>..." ; n += 1
553
- when :key ; nil
554
- when :keyrest ; nil
555
- 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)
556
1121
  end
1122
+ puts "#{c1}### exit: #{md.name}#{c2}" if @config.trace_mode
1123
+ ensure
1124
+ @curr_action = prev_action
557
1125
  end
558
- sb << ("]" * n)
559
- return sb.join()
1126
+ @status_dict[action] = :done
1127
+ #; [!ndxc3] returns true if action invoked.
1128
+ return true
560
1129
  end
561
1130
 
562
- def param2arg(param)
563
- #; [!eou4h] converts arg name 'xx_or_yy_or_zz' into 'xx|yy|zz'.
564
- #; [!naoft] converts arg name '_xx_yy_zz' into '_xx-yy-zz'.
565
- s = param.to_s
566
- s = s.gsub(/_or_/, '|') # ex: 'file_or_dir' => 'file|dir'
567
- s = s.gsub(/(?<=\w)_/, '-') # ex: 'aa_bb_cc' => 'aa-bb-cc'
568
- 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
569
1139
  end
570
1140
 
571
1141
  end
572
1142
 
573
1143
 
574
- ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
1144
+ CONTEXT_CLASS = ApplicationContext
575
1145
 
576
1146
 
577
- class ActionScope
1147
+ class Config
578
1148
 
579
- def run_action_once(action_name, *args, **kwargs)
580
- #; [!oh8dc] don't invoke action if already invoked.
581
- return __run_action(action_name, true, args, kwargs)
582
- 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>...]"
583
1163
 
584
- def run_action!(action_name, *args, **kwargs)
585
- #; [!2yrc2] invokes action even if already invoked.
586
- 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
587
1245
  end
588
1246
 
589
- private
1247
+ end
590
1248
 
591
- def __run_action(action_name, once, args, kwargs)
592
- #; [!lbp9r] invokes action name with prefix if prefix defined.
593
- #; [!7vszf] raises error if action specified not found.
594
- prefix = self.class.instance_variable_get('@__prefix__')
595
- metadata = INDEX.lookup_action("#{prefix}#{action_name}") || \
596
- INDEX.lookup_action(action_name) or
597
- raise ActionNotFoundError.new("#{action_name}: Action not found.")
598
- name = metadata.name
599
- #; [!u8mit] raises error if action flow is looped.
600
- ! INDEX.action_doing?(name) or
601
- raise LoopedActionError.new("#{name}: Action loop detected.")
602
- #; [!vhdo9] don't invoke action twice if 'once' arg is true.
603
- if INDEX.action_done?(name)
604
- return INDEX.action_result(name) if once
605
- end
606
- #; [!r8fbn] invokes action.
607
- INDEX.action_doing(name)
608
- ret = metadata.run_action(*args, **kwargs)
609
- INDEX.action_done(name, ret)
610
- return ret
1249
+
1250
+ class BaseHelpBuilder
1251
+
1252
+ def initialize(config, _registry: REGISTRY)
1253
+ @config = config
1254
+ @_registry = _registry
611
1255
  end
612
1256
 
613
- def self.prefix(str, alias_of: nil, action: nil)
614
- #; [!1gwyv] converts symbol into string.
615
- str = str.to_s
616
- #; [!pz46w] error if prefix contains extra '_'.
617
- str =~ /\A\w[-a-zA-Z0-9]*(:\w[-a-zA-Z0-9]*)*\z/ or
618
- raise ActionDefError.new("#{str}: Invalid prefix name (please use ':' or '-' instead of '_' as word separator).")
619
- #; [!9pu01] adds ':' at end of prefix name if prefix not end with ':'.
620
- str += ':' unless str.end_with?(':')
621
- @__prefix__ = str
622
- @__aliasof__ = alias_of # method name if symbol, or action name if string
623
- @__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.")
624
1268
  end
625
1269
 
626
- SUBCLASSES = []
1270
+ protected
627
1271
 
628
- def self.inherited(subclass)
629
- #; [!f826w] registers all subclasses into 'ActionScope::SUBCLASSES'.
630
- SUBCLASSES << subclass
631
- #; [!2imrb] sets class instance variables in subclass.
632
- subclass.instance_eval do
633
- @__action__ = nil # ex: ["action desc", {detail: nil, postamble: nil}]
634
- @__option__ = nil # Benry::CmdOpt::Schema object
635
- @__prefix__ = nil # ex: "foo:bar:"
636
- @__aliasof__ = nil # ex: :method_name or "action-name"
637
- @__default__ = nil # ex: :method_name or "action-name"
638
- #; [!1qv12] @action is a Proc object and saves args.
639
- @action = proc do |desc, detail: nil, postamble: nil, important: nil, tag: nil|
640
- @__action__ = [desc, {detail: detail, postamble: postamble, important: important, tag: tag}]
641
- end
642
- #; [!33ma7] @option is a Proc object and saves args.
643
- @option = proc do |param, optdef, desc, *rest, type: nil, rexp: nil, enum: nil, range: nil, value: nil, detail: nil, tag: nil, &block|
644
- #; [!gxybo] '@option.()' raises error when '@action.()' not called.
645
- @__action__ != nil or
646
- raise OptionDefError.new("@option.(#{param.inspect}): `@action.()` Required but not called.")
647
- schema = (@__option__ ||= SCHEMA_CLASS.new)
648
- #; [!ga6zh] '@option.()' raises error when invalid option info specified.
649
- begin
650
- schema.add(param, optdef, desc, *rest, type: type, rexp: rexp, enum: enum, range: range, value: value, detail: detail, tag: nil, &block)
651
- rescue Benry::CmdOpt::SchemaError => exc
652
- raise OptionDefError.new(exc.message)
653
- end
654
- end
655
- #; [!yrkxn] @copy_options is a Proc object and copies options from other action.
656
- @copy_options = proc do |action_name, except: nil|
657
- #; [!mhhn2] '@copy_options.()' raises error when action not found.
658
- metadata = INDEX.get_action(action_name) or
659
- raise OptionDefError.new("@copy_options.(#{action_name.inspect}): Action not found.")
660
- @__option__ ||= SCHEMA_CLASS.new
661
- @__option__.copy_from(metadata.schema, except: except)
1272
+ def render_section(header, content, extra=nil)
1273
+ #; [!61psk] returns section string with decorating header.
1274
+ #; [!0o8w4] appends '\n' to content if it doesn't end with '\n'.
1275
+ nl = content.end_with?("\n") ? nil : "\n"
1276
+ extra = " " + decorate_extra(extra) if extra
1277
+ return "#{decorate_header(header)}#{extra}\n#{content}#{nl}"
1278
+ end
1279
+
1280
+ def render_sections(value, item, &b)
1281
+ #; [!tqau1] returns nil if value is nil or empty.
1282
+ #; [!ezb0d] returns value unchanged if value is a string.
1283
+ #; [!gipxn] builds sections of help message if value is a hash object.
1284
+ xs = value.is_a?(Array) ? value : [value]
1285
+ sb = []
1286
+ xs.each do |x|
1287
+ case x
1288
+ when nil ; nil
1289
+ when String ; sb << (x.end_with?("\n") ? x : x + "\n")
1290
+ when Hash ; x.each {|k, v| sb << render_section(k, v) }
1291
+ else
1292
+ #; [!944rt] raises ActionError if unexpected value found in value.
1293
+ raise ActionError.new("#{x.inspect}: Unexpected value found in `#{item}`.")
662
1294
  end
663
1295
  end
1296
+ return sb.empty? ? nil : sb.join("\n")
664
1297
  end
665
1298
 
666
- def self.method_added(method)
667
- #; [!idh1j] do nothing if '@__action__' is nil.
668
- return unless @__action__
669
- #; [!ernnb] clears both '@__action__' and '@__option__'.
670
- desc, kws = @__action__
671
- schema = @__option__ || SCHEMA_CLASS.new
672
- @__action__ = @__option__ = nil
673
- #; [!n8tem] creates ActionMetadata object if '@__action__' is not nil.
674
- name = __method2action(method)
675
- metadata = ACTION_METADATA_CLASS.new(name, self, method, desc, schema, **kws)
676
- #; [!4pbsc] raises error if keyword param for option not exist in method.
677
- errmsg = metadata.validate_method_params()
678
- errmsg == nil or
679
- raise ActionDefError.new("def #{method}(): #{errmsg}")
680
- #; [!t8vbf] raises error if action name duplicated.
681
- ! INDEX.action_exist?(name) or
682
- raise ActionDefError.new("def #{method}(): Action '#{name}' already exist.")
683
- INDEX.register_action(name, metadata)
684
- #; [!jpzbi] defines same name alias of action as prefix.
685
- #; [!997gs] not raise error when action not found.
686
- self.__define_alias_of_action(method, name)
687
- end
688
-
689
- def self.__method2action(method) # :nodoc:
690
- #; [!5e5o0] when method name is same as default action name...
691
- if method == @__default__ # when Symbol
692
- #; [!myj3p] uses prefix name (expect last char ':') as action name.
693
- @__prefix__ != nil or raise "** assertion failed"
694
- name = @__prefix__.chomp(":")
695
- #; [!j5oto] clears '@__default__'.
696
- @__default__ = nil
697
- #; [!agpwh] else...
698
- else
699
- #; [!3icc4] uses method name as action name.
700
- #; [!c643b] converts action name 'aa_bb_cc_' into 'aa_bb_cc'.
701
- #; [!3fkb3] converts action name 'aa__bb__cc' into 'aa:bb:cc'.
702
- #; [!o9s9h] converts action name 'aa_bb:_cc_dd' into 'aa-bb:_cc-dd'.
703
- name = Util.method2action(method.to_s)
704
- #; [!8hlni] when action name is same as default name, uses prefix as action name.
705
- if name == @__default__ # when String
706
- name = @__prefix__.chomp(":")
707
- #; [!q8oxi] clears '@__default__' when default name matched to action name.
708
- @__default__ = nil
709
- #; [!xfent] when prefix is provided, adds it to action name.
710
- elsif @__prefix__
711
- name = "#{@__prefix__}#{name}"
1299
+ def render_option_help(schema, format, all: false)
1300
+ #; [!muhem] returns option part of help message.
1301
+ #; [!4z70n] includes hidden options when `all: true` passed.
1302
+ #; [!hxy1f] includes `detail:` kwarg value with indentation.
1303
+ #; [!jcqdf] returns nil if no options.
1304
+ c = @config
1305
+ sb = []
1306
+ schema.each do |x|
1307
+ next if x.hidden? && ! all
1308
+ s = format % [x.optdef, x.desc]
1309
+ if x.detail
1310
+ space = (format % ["", ""]).gsub(/\S/, " ")
1311
+ s += "\n"
1312
+ s += x.detail.chomp("\n").gsub(/^/, space)
712
1313
  end
1314
+ s = decorate_str(s, x.hidden?, x.important?)
1315
+ sb << s << "\n"
713
1316
  end
714
- return name
715
- end
716
-
717
- def self.__define_alias_of_action(method, action_name)
718
- return if @__aliasof__ == nil
719
- @__prefix__ != nil or raise "** internal error"
720
- alias_of = @__aliasof__
721
- if alias_of == method || alias_of == Util.method2action(method.to_s)
722
- alias_name = @__prefix__.chomp(":")
723
- #; [!349nr] raises error when same name action or alias with prefix already exists.
724
- Benry::CmdApp.action_alias(alias_name, action_name)
725
- #; [!tvjb0] clears '@__aliasof__' only when alias created.
726
- @__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
727
1345
  end
728
1346
  end
729
1347
 
1348
+ def header(symbol)
1349
+ #; [!ep064] returns constant value defined in the class.
1350
+ #; [!viwtn] constant value defined in child class is prior to one defined in parent class.
1351
+ return self.class.const_get(symbol)
1352
+ end
1353
+
730
1354
  end
731
1355
 
732
1356
 
733
- Action = ActionScope
1357
+ class ApplicationHelpBuilder < BaseHelpBuilder
734
1358
 
1359
+ def build_help_message(gschema, all: false)
1360
+ #; [!ezcs4] returns help message string of application.
1361
+ #; [!ntj2y] includes hidden actions and options if `all: true` passed.
1362
+ sb = []
1363
+ sb << section_preamble()
1364
+ sb << section_usage()
1365
+ sb << section_description()
1366
+ sb << section_options(gschema, all: all)
1367
+ sb << section_actions(true, all: all)
1368
+ #sb << section_aliases(all: all)
1369
+ #sb << section_abbrevs(all: all)
1370
+ #sb << section_categories(0, all: all)
1371
+ sb << section_postamble()
1372
+ return sb.compact().join("\n")
1373
+ end
735
1374
 
736
- class BuiltInAction < ActionScope
1375
+ protected
737
1376
 
738
- @action.("print help message (of action)")
739
- @option.(:all, "-a, --all", "show private (hidden) options, too")
740
- def help(action=nil, all: false)
741
- action_name = action
742
- #; [!jfgsy] prints help message of action if action name specified.
743
- if action_name
744
- action_metadata = INDEX.get_action(action_name) or
745
- raise ActionNotFoundError.new("#{action}: Action not found.")
746
- help_builder = ACTION_HELP_BUILDER_CLASS.new(action_metadata)
747
- config = $cmdapp_config
748
- msg = help_builder.build_help_message(config.app_command, all)
749
- #; [!fhpjg] prints help message of command if action name not specified.
750
- else
751
- app = $cmdapp_application
752
- msg = app.help_message(all)
1377
+ def section_preamble()
1378
+ #; [!51v42] returns preamble part of application help message.
1379
+ #; [!bmh17] includes `config.app_name` or `config.app_command` into preamble.
1380
+ #; [!opii8] includes `config.app_versoin` into preamble if it is set.
1381
+ #; [!3h380] includes `config.app_detail` into preamble if it is set.
1382
+ c = @config
1383
+ s = c.deco_command % (c.app_name || c.app_command)
1384
+ sb = []
1385
+ v = c.app_version ? (" " + c.deco_weak % "(#{c.app_version})") : ""
1386
+ sb << "#{s}#{v} --- #{c.app_desc}\n"
1387
+ if c.app_detail
1388
+ sb << "\n"
1389
+ sb << render_sections(c.app_detail, 'config.app_detail')
753
1390
  end
754
- #; [!6g7jh] prints colorized help message when color mode is on.
755
- #; [!ihr5u] prints non-colorized help message when color mode is off.
756
- msg = Util.del_escape_seq(msg) unless Util.colorize?
757
- print msg
1391
+ return sb.join()
758
1392
  end
759
1393
 
760
- 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
761
1399
 
1400
+ def section_usage()
1401
+ #; [!h98me] returns 'Usage:' section of application help message.
1402
+ c = @config
1403
+ s = c.deco_command % c.app_command
1404
+ s = c.format_usage % s + " [<options>] "
1405
+ #; [!i9d4r] includes `config.app_usage` into help message if it is set.
1406
+ usage = s + (c.app_usage || @config.class.const_get(:APP_USAGE))
1407
+ return render_section(header(:HEADER_USAGE), usage + "\n") # "Usage:"
1408
+ end
762
1409
 
763
- def self.action_alias(alias_name, action_name, *args, important: nil, tag: nil)
764
- invocation = "action_alias(#{alias_name.inspect}, #{action_name.inspect})"
765
- #; [!5immb] convers both alias name and action name into string.
766
- alias_ = alias_name.to_s
767
- action_ = action_name.to_s
768
- #; [!nrz3d] error if action not found.
769
- INDEX.action_exist?(action_) or
770
- raise AliasDefError.new("#{invocation}: Action not found.")
771
- #; [!vvmwd] error when action with same name as alias exists.
772
- ! INDEX.action_exist?(alias_) or
773
- raise AliasDefError.new("#{invocation}: Not allowed to define same name alias as existing action.")
774
- #; [!i9726] error if alias already defined.
775
- ! INDEX.alias_exist?(alias_) or
776
- raise AliasDefError.new("#{invocation}: Alias name duplicated.")
777
- #; [!vzlrb] registers alias name with action name.
778
- #; [!0cq6o] supports args.
779
- #; [!4wtxj] supports 'tag:' keyword arg.
780
- INDEX.register_alias(alias_, Alias.new(alias_, action_, *args, important: important, tag: tag))
781
- end
1410
+ def section_description()
1411
+ c = @config
1412
+ #; [!qarrk] returns 'Description:' section if `config.help_description` is set.
1413
+ #; [!ealol] returns nil if `config.help_description` is nil.
1414
+ return nil unless c.help_description
1415
+ return render_section(header(:HEADER_DESCRIPTION), c.help_description)
1416
+ end
1417
+
1418
+ def section_options(gschema, all: false)
1419
+ #; [!f2n70] returns 'Options:' section of application help message.
1420
+ #; [!0bboq] includes hidden options into help message if `all: true` passed.
1421
+ #; [!fjhow] returns nil if no options.
1422
+ format = @config.format_option
1423
+ s = render_option_help(gschema, format, all: all)
1424
+ return nil if s == nil
1425
+ return render_section(header(:HEADER_OPTIONS), s) # "Options:"
1426
+ end
782
1427
 
1428
+ public
783
1429
 
784
- 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
785
1444
 
786
- def initialize(alias_name, action_name, *args, important: nil, tag: nil)
787
- @alias_name = alias_name
788
- @action_name = action_name
789
- @args = args.freeze if ! args.empty?
790
- @important = important if important != nil
791
- @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()
792
1470
  end
1471
+ private :_render_metadata_list
793
1472
 
794
- 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
795
1479
 
796
- def desc()
797
- if @args && ! @args.empty?
798
- return "alias of '#{@action_name} #{@args.join(' ')}'"
799
- else
800
- 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"
801
1542
  end
1543
+ #; [!dnt12] returns nil if no abbrevs found.
1544
+ return nil if sb.empty?
1545
+ #; [!00ice] returns abbrev list string.
1546
+ return render_section(header(:HEADER_ABBREVS), sb.join()) # "Abbreviations:"
802
1547
  end
803
1548
 
804
- def important?()
805
- #; [!5juwq] returns true if `@important == true`.
806
- #; [!1gnbc] returns false if `@important == false`.
807
- return @important if @important != nil
808
- #; [!h3nm3] returns true or false according to action object if `@important == nil`.
809
- action_obj = INDEX.get_action(@action_name)
810
- 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:"
811
1569
  end
812
1570
 
813
1571
  end
814
1572
 
815
1573
 
816
- class Config #< BasicObject
1574
+ class ActionHelpBuilder < BaseHelpBuilder
817
1575
 
818
- #FORMAT_HELP = " %-18s : %s"
819
- FORMAT_HELP = " \e[1m%-18s\e[0m : %s" # bold
820
- #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
821
1591
 
822
- FORMAT_APPNAME = "\e[1m%s\e[0m"
1592
+ protected
823
1593
 
824
- #FORMAT_USAGE = " $ %s %s"
825
- FORMAT_USAGE = " $ \e[1m%s\e[0m %s" # bold
826
- #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
827
1609
 
828
- #FORMAT_HEADING = "%s:"
829
- #FORMAT_HEADING = "\e[1m%s:\e[0m" # bold
830
- #FORMAT_HEADING = "\e[1;4m%s:\e[0m" # bold, underline
831
- FORMAT_HEADING = "\e[34m%s:\e[0m" # blue
832
- #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
833
1629
 
834
- def initialize(app_desc, app_version=nil,
835
- app_name: nil, app_command: nil, app_detail: nil,
836
- default_action: "help", default_help: false,
837
- option_help: true, option_all: false,
838
- option_verbose: false, option_quiet: false, option_color: false,
839
- option_debug: false, option_trace: false,
840
- help_action: true, help_aliases: false, help_sections: [], help_postamble: nil,
841
- format_help: nil, format_appname: nil, format_usage: nil, format_heading: nil,
842
- feat_candidate: true)
843
- #; [!uve4e] sets command name automatically if not provided.
844
- @app_desc = app_desc # ex: "sample application"
845
- @app_version = app_version # ex: "1.0.0"
846
- @app_name = app_name || ::File.basename($0) # ex: "MyApp"
847
- @app_command = app_command || ::File.basename($0) # ex: "myapp"
848
- @app_detail = app_detail # ex: "See https://.... for details.\n"
849
- @default_action = default_action # default action name
850
- @default_help = default_help # print help message if action not specified
851
- @option_help = option_help # '-h' and '--help' are enabled when true
852
- @option_all = option_all # '-a' and '--all' are enable when true
853
- @option_verbose = option_verbose # '-v' and '--verbose' are enabled when true
854
- @option_quiet = option_quiet # '-q' and '--quiet' are enabled when true
855
- @option_color = option_color # '--color[=<on|off>]' enabled when true
856
- @option_debug = option_debug # '-D' and '--debug' are enabled when true
857
- @option_trace = option_trace # '-T' and '--trace' are enabled when true
858
- @help_action = help_action # define built-in 'help' action when true
859
- @help_aliases = help_aliases # 'Aliases:' section printed when true
860
- @help_sections = help_sections # ex: [["Example", "..text.."], ...]
861
- @help_postamble = help_postamble # ex: "(Tips: ....)\n"
862
- @format_help = format_help || FORMAT_HELP
863
- @format_appname = format_appname || FORMAT_APPNAME
864
- @format_usage = format_usage || FORMAT_USAGE
865
- @format_heading = format_heading || FORMAT_HEADING
866
- @feat_candidate = feat_candidate # if arg is 'foo:', list actions starting with 'foo:'
867
- end
868
-
869
- attr_accessor :app_desc, :app_version, :app_name, :app_command, :app_detail
870
- attr_accessor :default_action, :default_help
871
- attr_accessor :option_help, :option_all
872
- attr_accessor :option_verbose, :option_quiet, :option_color
873
- attr_accessor :option_debug, :option_trace
874
- attr_accessor :help_action, :help_aliases, :help_sections, :help_postamble
875
- attr_accessor :format_help, :format_appname, :format_usage, :format_heading
876
- 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
877
1673
 
878
1674
  end
879
1675
 
880
1676
 
881
- class AppOptionSchema < Benry::CmdOpt::Schema
1677
+ APPLICATION_HELP_BUILDER_CLASS = ApplicationHelpBuilder
1678
+ ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
1679
+
1680
+
1681
+ class GlobalOptionSchema < OptionSchema
882
1682
 
883
- def initialize(config=nil)
1683
+ def initialize(config)
884
1684
  super()
885
- #; [!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.
886
1692
  c = config
887
- return nil if c == nil
888
- #; [!tq2ol] adds '-h, --help' option if 'config.option_help' is set.
889
- add(:help , "-h, --help" , "print help message") if c.option_help
890
- #; [!mbtw0] adds '-V, --version' option if 'config.app_version' is set.
891
- add(:version, "-V, --version", "print version") if c.app_version
892
- #; [!f5do6] adds '-a, --all' option if 'config.option_all' is set.
893
- add(:all , "-a, --all" , "list all actions including private (hidden) ones") if c.option_all
894
- #; [!cracf] adds '-v, --verbose' option if 'config.option_verbose' is set.
895
- add(:verbose, "-v, --verbose", "verbose mode") if c.option_verbose
896
- #; [!2vil6] adds '-q, --quiet' option if 'config.option_quiet' is set.
897
- add(:quiet , "-q, --quiet" , "quiet mode") if c.option_quiet
898
- #; [!6zw3j] adds '--color=<on|off>' option if 'config.option_color' is set.
899
- add(:color , "--color[=<on|off>]", "enable/disable color", type: TrueClass) if c.option_color
900
- #; [!29wfy] adds '-D, --debug' option if 'config.option_debug' is set.
901
- add(:debug , "-D, --debug" , "debug mode (set $DEBUG_MODE to true)") if c.option_debug
902
- #; [!s97go] adds '-T, --trace' option if 'config.option_trace' is set.
903
- add(:trace , "-T, --trace" , "report enter into and exit from actions") if c.option_trace
904
- end
905
-
906
- def sort_options_in_this_order(*keys)
907
- #; [!6udxr] sorts options in order of keys specified.
908
- #; [!8hhuf] options which key doesn't appear in keys are moved at end of options.
909
- len = @items.length
910
- @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 }
911
1730
  nil
912
1731
  end
913
1732
 
914
1733
  end
915
1734
 
916
-
917
- APP_OPTION_SCHEMA_CLASS = AppOptionSchema
1735
+ GLOBAL_OPTION_SCHEMA_CLASS = GlobalOptionSchema
1736
+ GLOBAL_OPTION_PARSER_CLASS = OptionParser
918
1737
 
919
1738
 
920
1739
  class Application
921
1740
 
922
- def initialize(config, schema=nil, help_builder=nil, &callback)
923
- @config = config
924
- #; [!h786g] acceps callback block.
925
- @callback = callback
926
- #; [!jkprn] creates option schema object according to config.
927
- @schema = schema || do_create_global_option_schema(config)
928
- @help_builder = help_builder
929
- @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
930
1775
  end
931
1776
 
932
- 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
933
1790
 
934
- def main(argv=ARGV, &block)
935
- begin
936
- #; [!y6q9z] runs action with options.
937
- self.run(*argv)
938
- rescue ExecutionError, DefinitionError => exc
939
- #; [!6ro6n] not catch error when $DEBUG_MODE is on.
940
- raise if $DEBUG_MODE
941
- #; [!a7d4w] prints error message with '[ERROR]' prompt.
942
- $stderr.puts "\033[0;31m[ERROR]\033[0m #{exc.message}"
943
- #; [!r7opi] prints filename and line number on where error raised if DefinitionError.
944
- if exc.is_a?(DefinitionError)
945
- #; [!v0zrf] error location can be filtered by block.
946
- if block_given?()
947
- loc = exc.backtrace_locations.find(&block)
948
- else
949
- loc = exc.backtrace_locations.find {|x| x.path != __FILE__ }
950
- end
951
- raise unless loc
952
- $stderr.puts "\t(file: #{loc.path}, line: #{loc.lineno})"
1791
+ def handle_action(args, global_opts)
1792
+ #; [!3qw3p] when no arguments specified...
1793
+ if args.empty?
1794
+ #; [!zl9em] lists actions if default action is not set.
1795
+ #; [!89hqb] lists all actions including hidden ones if `-a` or `--all` specified.
1796
+ if @config.default_action == nil
1797
+ return handle_blank_action(all: global_opts[:all])
953
1798
  end
954
- #; [!qk5q5] returns 1 as exit code when error occurred.
955
- 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...
956
1808
  else
957
- #; [!5oypr] returns 0 as exit code when no errors occurred.
958
- return 0
1809
+ #; [!bq39a] runs action with arguments.
1810
+ action = args.shift()
959
1811
  end
1812
+ #; [!5yd8x] returns 0 when action invoked successfully.
1813
+ return start_action(action, args)
960
1814
  end
1815
+ protected :handle_action
961
1816
 
962
- def run(*args)
963
- #; [!t4ypg] sets $cmdapp_config at beginning.
964
- do_setup()
965
- #; [!pyotc] sets global options to '@global_options'.
966
- global_opts = do_parse_global_options(args)
967
- @global_options = global_opts
968
- #; [!go9kk] sets global variables according to global options.
969
- do_toggle_global_switches(args, global_opts)
970
- #; [!pbug7] skip actions if callback method throws `:SKIP`.
971
- skip_action = true
972
- catch :SKIP do
973
- #; [!5iczl] skip actions if help option or version option specified.
974
- do_handle_global_options(args, global_opts)
975
- #; [!w584g] calls callback method.
976
- do_callback(args, global_opts)
977
- skip_action = false
978
- end
979
- return if skip_action
980
- #; [!avxos] prints candidate actions if action name ends with ':'.
981
- #; [!eeh0y] candidates are not printed if 'config.feat_candidate' is false.
982
- if ! args.empty? && args[0].end_with?(':') && @config.feat_candidate
983
- do_print_candidates(args, global_opts)
984
- return
985
- end
986
- #; [!agfdi] reports error when action not found.
987
- #; [!o5i3w] reports error when default action not found.
988
- #; [!n60o0] reports error when action nor default action not specified.
989
- #; [!7h0ku] prints help if no action but 'config.default_help' is true.
990
- #; [!l0g1l] skip actions if no action specified and 'config.default_help' is set.
991
- metadata = do_find_action(args, global_opts)
992
- if metadata == nil
993
- do_print_help_message([], global_opts)
994
- do_validate_actions(args, global_opts)
995
- return
996
- end
997
- #; [!x1xgc] run action with options and arguments.
998
- #; [!v5k56] runs default action if action not specified.
999
- do_run_action(metadata, args, global_opts)
1000
- rescue => exc
1001
- raise
1002
- ensure
1003
- #; [!hk6iu] unsets $cmdapp_config at end.
1004
- #; [!wv22u] calls teardown method at end of running action.
1005
- #; [!dhba4] calls teardown method even if exception raised.
1006
- do_teardown(exc)
1817
+ def render_help_message(action=nil, all: false)
1818
+ #; [!2oax5] returns action help message if action name is specified.
1819
+ #; [!d6veb] returns application help message if action name is not specified.
1820
+ #; [!tf2wp] includes hidden actions and options into help message if `all: true` passed.
1821
+ return render_action_help(action, all: all) if action
1822
+ return render_application_help(all: all)
1007
1823
  end
1008
1824
 
1009
1825
  protected
1010
1826
 
1011
- def do_create_global_option_schema(config)
1012
- #; [!u3zdg] creates global option schema object according to config.
1013
- return APP_OPTION_SCHEMA_CLASS.new(config)
1827
+ def setup()
1828
+ #; [!6hi1y] stores current application.
1829
+ Benry::CmdApp._set_current_app(self)
1014
1830
  end
1015
1831
 
1016
- def do_create_help_message_builder(config, schema)
1017
- #; [!pk5da] creates help message builder object.
1018
- 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)
1019
1835
  end
1020
1836
 
1021
- def do_parse_global_options(args)
1022
- #; [!5br6t] parses only global options and not parse action options.
1023
- parser = PARSER_CLASS.new(@schema)
1024
- global_opts = parser.parse(args, all: false)
1837
+ def parse_global_options(args)
1838
+ #; [!9c9r8] parses global options.
1839
+ parser = GLOBAL_OPTION_PARSER_CLASS.new(@option_schema)
1840
+ global_opts = parser.parse(args, all: false) # raises OptionError
1025
1841
  return global_opts
1026
- #; [!kklah] raises InvalidOptionError if global option value is invalid.
1027
- rescue Benry::CmdOpt::OptionError => exc
1028
- raise InvalidOptionError.new(exc.message)
1029
- end
1030
-
1031
- def do_toggle_global_switches(_args, global_opts)
1032
- #; [!j6u5x] sets $QUIET_MODE to false if '-v' or '--verbose' specified.
1033
- #; [!p1l1i] sets $QUIET_MODE to true if '-q' or '--quiet' specified.
1034
- #; [!2zvf9] sets $COLOR_MODE to true/false according to '--color' option.
1035
- #; [!ywl1a] sets $DEBUG_MODE to true if '-D' or '--debug' specified.
1036
- #; [!8trmz] sets $TRACE_MODE to true if '-T' or '--trace' specified.
1037
- global_opts.each do |key, val|
1038
- case key
1039
- when :verbose ; $QUIET_MODE = ! val
1040
- when :quiet ; $QUIET_MODE = val
1041
- when :color ; $COLOR_MODE = val
1042
- when :debug ; $DEBUG_MODE = val
1043
- when :trace ; $TRACE_MODE = val
1044
- else ; # do nothing
1045
- end
1842
+ end
1843
+
1844
+ def toggle_global_options(global_opts)
1845
+ #; [!xwcyl] sets `$VERBOSE_MODE` and `$QUIET_MODE` according to global options.
1846
+ d = global_opts
1847
+ if d[:verbose] ; $VERBOSE_MODE = true ; $QUIET_MODE = false
1848
+ elsif d[:quiet] ; $VERBOSE_MODE = false; $QUIET_MODE = true
1046
1849
  end
1850
+ #; [!510eb] sets `$COLOR_MODE` according to global option.
1851
+ $COLOR_MODE = d[:color] if d[:color] != nil
1852
+ #; [!sucqp] sets `$DEBUG_MODE` according to global options.
1853
+ $DEBUG_MODE = d[:debug] if d[:debug] != nil
1854
+ #; [!y9fow] sets `config.trace_mode` if global option specified.
1855
+ @config.trace_mode = d[:trace] if d[:trace] != nil
1856
+ #; [!dply7] sets `$DRYRUN_MODE` according to global option.
1857
+ $DRYRUN_MODE = d[:dryrun] if d[:dryrun] != nil
1858
+ nil
1047
1859
  end
1048
1860
 
1049
- def do_handle_global_options(args, global_opts)
1050
- #; [!xvj6s] prints help message if '-h' or '--help' specified.
1051
- #; [!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.
1052
1865
  if global_opts[:help]
1053
- do_print_help_message(args, global_opts)
1054
- do_validate_actions(args, global_opts)
1055
- 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
1056
1870
  end
1057
- #; [!fslsy] prints version if '-V' or '--version' specified.
1871
+ #; [!dkjw8] prints version number if global option `-V, --version` specified.
1058
1872
  if global_opts[:version]
1059
- puts @config.app_version
1060
- throw :SKIP # done
1873
+ print_str render_version()
1874
+ return 0
1061
1875
  end
1062
- end
1063
-
1064
- def do_callback(args, global_opts)
1065
- #; [!xwo0v] calls callback if provided.
1066
- #; [!lljs1] calls callback only once.
1067
- if @callback && ! @__called
1068
- @__called = true
1069
- @callback.call(args, global_opts, @config)
1876
+ #; [!hj4hf] prints action and alias list if global option `-l, --list` specified.
1877
+ #; [!tyxwo] includes hidden actions into action list if `-a, --all` specified.
1878
+ if global_opts[:list]
1879
+ print_str render_item_list(nil, all: all)
1880
+ return 0
1070
1881
  end
1071
- end
1072
-
1073
- def do_find_action(args, _global_opts)
1074
- c = @config
1075
- #; [!bm8np] returns action metadata.
1076
- if ! args.empty?
1077
- action_name = args.shift()
1078
- #; [!vl0zr] error when action not found.
1079
- metadata = INDEX.lookup_action(action_name) or
1080
- raise CommandError.new("#{action_name}: Unknown action.")
1081
- #; [!gucj7] if no action specified, finds default action instead.
1082
- elsif c.default_action
1083
- action_name = c.default_action
1084
- #; [!388rs] error when default action not found.
1085
- metadata = INDEX.lookup_action(action_name) or
1086
- raise CommandError.new("#{action_name}: Unknown default action.")
1087
- #; [!drmls] returns nil if no action specified but 'config.default_help' is set.
1088
- elsif c.default_help
1089
- #do_print_help_message([])
1090
- return nil
1091
- #; [!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...
1092
1940
  else
1093
- 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.")
1094
1943
  end
1095
- return metadata
1096
1944
  end
1097
1945
 
1098
- def do_run_action(metadata, args, _global_opts)
1099
- action_name = metadata.name
1100
- #; [!62gv9] parses action options even if specified after args.
1101
- options = metadata.parse_options(args, true)
1102
- #; [!6mlol] error if action requries argument but nothing specified.
1103
- #; [!72jla] error if action requires N args but specified less than N args.
1104
- #; [!zawxe] error if action requires N args but specified over than N args.
1105
- #; [!y97o3] action can take any much args if action has variable arg.
1106
- min, max = metadata.method_arity()
1107
- n = args.length
1108
- if n < min
1109
- raise CommandError.new("#{action_name}: Argument required.") if n == 0
1110
- raise CommandError.new("#{action_name}: Too less arguments (at least #{min}).")
1111
- elsif max && max < n
1112
- raise CommandError.new("#{action_name}: Too much arguments (at most #{max}).")
1113
- end
1114
- #; [!cf45e] runs action with arguments and options.
1115
- #; [!tsal4] detects looped action.
1116
- INDEX.action_doing(action_name)
1117
- ret = metadata.run_action(*args, **options)
1118
- INDEX.action_done(action_name, ret)
1119
- return ret
1120
- end
1121
-
1122
- def do_print_help_message(args, global_opts)
1123
- #; [!4qs7y] shows private (hidden) actions/options if '--all' option specified.
1124
- #; [!l4d6n] `all` flag should be true or false, not nil.
1125
- all = !! global_opts[:all]
1126
- #; [!eabis] prints help message of action if action name provided.
1127
- action_name = args[0]
1128
- if action_name
1129
- #; [!cgxkb] error if action for help option not found.
1130
- metadata = INDEX.lookup_action(action_name) or
1131
- raise CommandError.new("#{action_name}: Action not found.")
1132
- msg = metadata.help_message(@config.app_command, all)
1133
- #; [!nv0x3] prints help message of command if action name not provided.
1134
- else
1135
- msg = help_message(all)
1136
- end
1137
- #; [!efaws] prints colorized help message when stdout is a tty.
1138
- #; [!9vdy1] prints non-colorized help message when stdout is not a tty.
1139
- #; [!gsdcu] prints colorized help message when '--color[=on]' specified.
1140
- #; [!be8y2] prints non-colorized help message when '--color=off' specified.
1141
- msg = Util.del_escape_seq(msg) unless Util.colorize?
1142
- puts msg
1143
- end
1144
-
1145
- def do_validate_actions(_args, _global_opts)
1146
- #; [!6xhvt] reports warning at end of help message.
1147
- nl = "\n"
1148
- ActionScope::SUBCLASSES.each do |klass|
1149
- #; [!iy241] reports warning if `alias_of:` specified in action class but corresponding action not exist.
1150
- alias_of = klass.instance_variable_get(:@__aliasof__)
1151
- if alias_of
1152
- warn "#{nl}** [warning] in '#{klass.name}' class, `alias_of: #{alias_of.inspect}` specified but corresponding action not exist."
1153
- nl = ""
1946
+ def render_topic_list(topic, all: false)
1947
+ #; [!uzmml] renders topic list.
1948
+ #; [!vrzu0] topic 'category1' or 'categories2' is acceptable.
1949
+ #; [!xyn5g] global option '-L metadata' renders registry data in YAML format.
1950
+ builder = get_app_help_builder()
1951
+ return (
1952
+ case topic
1953
+ when "action", "actions"; builder.section_actions(false, all: all)
1954
+ when "alias" , "aliases"; builder.section_aliases(all: all)
1955
+ when "abbrev", "abbrevs"; builder.section_abbrevs(all: all)
1956
+ when /\Acategor(?:y|ies)(\d+)?\z/ ; builder.section_categories(($1 || 0).to_i, all: all)
1957
+ when "metadata"; MetadataRenderer.new(@_registry).render_metadata()
1958
+ else raise "** assertion failed: topic=#{topic.inspect}"
1154
1959
  end
1155
- #; [!h7lon] reports warning if `action:` specified in action class but corresponding action not exist.
1156
- default = klass.instance_variable_get(:@__default__)
1157
- if default
1158
- warn "#{nl}** [warning] in '#{klass.name}' class, `action: #{default.inspect}` specified but corresponding action not exist."
1159
- nl = ""
1160
- end
1161
- end
1960
+ )
1162
1961
  end
1163
1962
 
1164
- def do_print_candidates(args, _global_opts)
1165
- #; [!0e8vt] prints candidate action names including prefix name without tailing ':'.
1166
- prefix = args[0]
1167
- prefix2 = prefix.chomp(':')
1168
- pairs = []
1169
- aname2aliases = {}
1170
- INDEX.each_action do |ameta|
1171
- aname = ameta.name
1172
- next unless aname.start_with?(prefix) || aname == prefix2
1173
- #; [!k3lw0] private (hidden) action should not be printed as candidates.
1174
- next if ameta.hidden?
1175
- #
1176
- pairs << [aname, ameta.desc, ameta.important?]
1177
- aname2aliases[aname] = []
1178
- end
1179
- #; [!85i5m] candidate actions should include alias names.
1180
- INDEX.each_alias do |ali_obj|
1181
- ali_name = ali_obj.alias_name
1182
- next unless ali_name.start_with?(prefix) || ali_name == prefix2
1183
- pairs << [ali_name, ali_obj.desc(), ali_obj.important?]
1184
- end
1185
- #; [!i2azi] raises error when no candidate actions found.
1186
- ! pairs.empty? or
1187
- raise CommandError.new("No actions starting with '#{prefix}'.")
1188
- INDEX.each_alias do |alias_obj|
1189
- alias_ = alias_obj.alias_name
1190
- action_ = alias_obj.action_name
1191
- aname2aliases[action_] << alias_ if aname2aliases.key?(action_)
1963
+ def handle_blank_action(all: false)
1964
+ #; [!seba7] prints action list and returns `0`.
1965
+ print_str render_item_list(nil, all: all)
1966
+ return 0
1967
+ end
1968
+
1969
+ def handle_prefix(prefix, all: false)
1970
+ #; [!8w301] prints action list starting with prefix and returns `0`.
1971
+ print_str render_item_list(prefix, all: all)
1972
+ return 0
1973
+ end
1974
+
1975
+ def start_action(action_name, args)
1976
+ #; [!6htva] supports abbreviation of prefix.
1977
+ if ! @_registry.metadata_exist?(action_name)
1978
+ resolved = @_registry.abbrev_resolve(action_name)
1979
+ action_name = resolved if resolved
1192
1980
  end
1981
+ #; [!vbymd] runs action with args and returns `0`.
1982
+ @_registry.metadata_get(action_name) or
1983
+ raise CommandError.new("#{action_name}: Action not found.")
1984
+ new_context().start_action(action_name, args)
1985
+ return 0
1986
+ end
1987
+
1988
+ private
1989
+
1990
+ def get_app_help_builder()
1991
+ return @app_help_builder || APPLICATION_HELP_BUILDER_CLASS.new(@config)
1992
+ end
1993
+
1994
+ def get_action_help_builder()
1995
+ return @action_help_builder || ACTION_HELP_BUILDER_CLASS.new(@config)
1996
+ end
1997
+
1998
+ def new_context()
1999
+ #; [!9ddcl] creates new context object with config object.
2000
+ return CONTEXT_CLASS.new(@config)
2001
+ end
2002
+
2003
+ def print_str(str)
2004
+ #; [!yiabh] do nothing if str is nil.
2005
+ return nil unless str
2006
+ #; [!6kyv9] prints string as is if color mode is enabled.
2007
+ #; [!lxhvq] deletes escape characters from string and prints it if color mode is disabled.
2008
+ str = Util.delete_escape_chars(str) unless Util.color_mode?
2009
+ print str
2010
+ nil
2011
+ end
2012
+
2013
+ def print_error(exc)
2014
+ #; [!sdbj8] prints exception as error message.
2015
+ #; [!6z0mu] prints colored error message if stderr is a tty.
2016
+ #; [!k1s3o] prints non-colored error message if stderr is not a tty.
2017
+ prompt = "[ERROR]"
2018
+ prompt = @config.deco_error % prompt if $stderr.tty?
2019
+ $stderr.puts "#{prompt} #{exc.message}"
2020
+ nil
2021
+ end
2022
+
2023
+ def print_backtrace(exc)
2024
+ cache = {} # {filename => [line]}
2025
+ color_p = $stderr.tty?
1193
2026
  sb = []
1194
- sb << @config.format_heading % "Actions" << "\n"
1195
- format = @config.format_help
1196
- indent = " " * (Util.del_escape_seq(format) % ['', '']).length
1197
- pairs.sort_by {|aname, _, _| aname }.each do |aname, adesc, important|
1198
- #; [!j4b54] shows candidates in strong format if important.
1199
- #; [!q3819] shows candidates in weak format if not important.
1200
- sb << Util.format_help_line(format, aname, adesc, important) << "\n"
1201
- aliases = aname2aliases[aname]
1202
- if aliases && ! aliases.empty?
1203
- 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
1204
2044
  end
1205
2045
  end
1206
- s = sb.join()
1207
- s = Util.del_escape_seq(s) unless Util.colorize?
1208
- puts s
1209
- end
1210
-
1211
- def do_setup()
1212
- #; [!pkio4] sets config object to '$cmdapp_config'.
1213
- $cmdapp_config = @config
1214
- #; [!qwjjv] sets application object to '$cmdapp_application'.
1215
- $cmdapp_application = self
1216
- #; [!kqfn1] remove built-in 'help' action if `config.help_action == false`.
1217
- if ! @config.help_action
1218
- INDEX.delete_action("help") if INDEX.action_exist?("help")
1219
- end
2046
+ #; [!8wzxg] prints backtrace of exception.
2047
+ $stderr.print sb.join()
2048
+ cache.clear()
2049
+ nil
1220
2050
  end
1221
2051
 
1222
- def do_teardown(exc)
1223
- #; [!zxeo7] clears '$cmdapp_config'.
1224
- $cmdapp_config = nil
1225
- #; [!ufm1d] clears '$cmdapp_application'.
1226
- $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
1227
2057
  end
1228
2058
 
1229
- 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
1230
2065
 
1231
- def help_message(all=false, format=nil)
1232
- #; [!owg9y] returns help message.
1233
- @help_builder ||= do_create_help_message_builder(@config, @schema)
1234
- return @help_builder.build_help_message(all, format)
2066
+ def should_rescue?(exc)
2067
+ #; [!8lwyn] returns trueif exception is a BaseError.
2068
+ return exc.is_a?(BaseError)
1235
2069
  end
1236
2070
 
1237
2071
  end
1238
2072
 
1239
2073
 
1240
- class AppHelpBuilder < HelpBuilder
2074
+ class MetadataRenderer
1241
2075
 
1242
- def initialize(config, schema)
1243
- @config = config
1244
- @schema = schema
2076
+ def initialize(registry)
2077
+ @registry = registry
1245
2078
  end
1246
2079
 
1247
- def build_help_message(all=false, format=nil)
1248
- #; [!rvpdb] returns help message.
1249
- format ||= @config.format_help
2080
+ def render_metadata(all: false)
2081
+ #; [!gduge] renders registry data in YAML format.
1250
2082
  sb = []
1251
- sb << build_preamble(all)
1252
- sb << build_usage(all)
1253
- sb << build_options(all, format)
1254
- sb << build_actions(all, format)
1255
- #; [!oxpda] prints 'Aliases:' section only when 'config.help_aliases' is true.
1256
- sb << build_aliases(all, format) if @config.help_aliases
1257
- @config.help_sections.each do |title, content, desc|
1258
- #; [!kqnxl] array of section may have two or three elements.
1259
- sb << build_section(title, content, desc)
1260
- end if @config.help_sections
1261
- sb << build_postamble(all)
1262
- 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()
1263
2095
  end
1264
2096
 
1265
2097
  protected
1266
2098
 
1267
- def build_preamble(all=false)
1268
- #; [!34y8e] includes application name specified by config.
1269
- #; [!744lx] includes application description specified by config.
1270
- #; [!d1xz4] includes version number if specified by config.
1271
- c = @config
1272
- sb = []
1273
- if c.app_desc
1274
- app_name = c.format_appname % c.app_name
1275
- ver = c.app_version ? " (#{c.app_version})" : ""
1276
- sb << "#{app_name}#{ver} -- #{c.app_desc}\n"
1277
- end
1278
- #; [!775jb] includes detail text if specified by config.
1279
- #; [!t3tbi] adds '\n' before detail text only when app desc specified.
1280
- if c.app_detail
1281
- sb << "\n" unless sb.empty?
1282
- sb << c.app_detail
1283
- 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
1284
2126
  end
1285
- #; [!rvhzd] no preamble when neigher app desc nor detail specified.
1286
- return nil if sb.empty?
1287
- return sb.join()
1288
2127
  end
1289
2128
 
1290
- def build_usage(all=false)
1291
- c = @config
1292
- format = c.format_usage + "\n"
1293
- #; [!o176w] includes command name specified by config.
1294
- sb = []
1295
- sb << (format % [c.app_command, "[<options>] [<action> [<arguments>...]]"])
1296
- return build_section("Usage", sb.join(), nil)
2129
+ def render_parameters(metadata, all: false)
2130
+ obj = metadata.klass.new(nil)
2131
+ obj.method(metadata.meth).parameters.each do |ptype, pname|
2132
+ yield " - param: #{pname}\n"
2133
+ yield " type: #{ptype}\n"
2134
+ end
1297
2135
  end
1298
2136
 
1299
- def build_options(all=false, format=nil)
1300
- format ||= @config.format_help
1301
- format += "\n"
1302
- #; [!in3kf] ignores private (hidden) options.
1303
- #; [!ywarr] not ignore private (hidden) options if 'all' flag is true.
1304
- sb = []
1305
- @schema.each do |item|
1306
- if all || ! item.hidden?
1307
- #; [!p1tu9] prints option in weak format if option is hidden.
1308
- important = item.hidden? ? false : nil
1309
- sb << Util.format_help_line(format, item.optdef, item.desc, important)
1310
- 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
1311
2156
  end
1312
- #; [!bm71g] ignores 'Options:' section if no options exist.
1313
- return nil if sb.empty?
1314
- #; [!proa4] includes description of global options.
1315
- return build_section("Options", sb.join(), nil)
1316
2157
  end
1317
2158
 
1318
- def build_actions(all=false, format=nil)
1319
- c = @config
1320
- format ||= c.format_help
1321
- format += "\n"
1322
- sb = []
1323
- #; [!df13s] includes default action name if specified by config.
1324
- desc = c.default_action ? "(default: #{c.default_action})" : nil
1325
- #; [!jat15] includes action names ordered by name.
1326
- include_alias = ! @config.help_aliases
1327
- INDEX.each_action_name_and_desc(include_alias, all: all) do |name, desc, important|
1328
- #; [!b3l3m] not show private (hidden) action names in default.
1329
- #; [!yigf3] shows private (hidden) action names if 'all' flag is true.
1330
- if all || ! Util.hidden_name?(name)
1331
- #; [!5d9mc] shows hidden action in weak format.
1332
- #; [!awk3l] shows important action in strong format.
1333
- #; [!9k4dv] shows unimportant action in weak fomrat.
1334
- sb << Util.format_help_line(format, name, desc, important)
2159
+ def render_aliases(all: false)
2160
+ @registry.metadata_each(all: all) do |metadata|
2161
+ md = metadata
2162
+ if md.alias?
2163
+ yield " - alias: #{md.name}\n"
2164
+ yield " desc: #{qq(md.desc)}\n"
2165
+ yield " action: #{md.action}\n"
2166
+ yield " args: #{md.args.inspect}\n" if md.args && ! md.args.empty?
2167
+ yield " tag: #{md.tag}\n" if md.tag
2168
+ yield " important: #{md.important}\n" if md.important != nil
2169
+ yield " hidden: #{md.hidden?}\n" if md.hidden? != nil
1335
2170
  end
1336
2171
  end
1337
- return build_section("Actions", sb.join(), desc)
1338
2172
  end
1339
2173
 
1340
- def build_aliases(all=false, format=nil)
1341
- format ||= @config.format_help
1342
- format += "\n"
1343
- #; [!tri8x] includes alias names in order of registration.
1344
- sb = []
1345
- INDEX.each_alias do |alias_obj|
1346
- alias_name = alias_obj.alias_name
1347
- #; [!5g72a] not show hidden alias names in default.
1348
- #; [!ekuqm] shows all alias names including private ones if 'all' flag is true.
1349
- if all || ! Util.hidden_name?(alias_name)
1350
- #; [!aey2k] shows alias in strong or weak format according to action.
1351
- sb << Util.format_help_line(format, alias_name, alias_obj.desc(), alias_obj.important?)
1352
- end
2174
+ def render_categories(all: false)
2175
+ dict = @registry.category_count_actions(0, all: all)
2176
+ @registry.category_each do |prefix, desc|
2177
+ yield " - prefix: \"#{prefix}\"\n"
2178
+ yield " count: #{dict[prefix] || 0}\n"
2179
+ yield " desc: #{qq(desc)}\n" if desc
1353
2180
  end
1354
- #; [!p3oh6] now show 'Aliases:' section if no aliases defined.
1355
- return nil if sb.empty?
1356
- #; [!we1l8] shows 'Aliases:' section if any aliases defined.
1357
- return build_section("Aliases", sb.join(), nil)
1358
2181
  end
1359
2182
 
1360
- def build_postamble(all=false)
1361
- #; [!i04hh] includes postamble text if specified by config.
1362
- s = @config.help_postamble
1363
- if s
1364
- #; [!ckagw] adds '\n' at end of postamble text if it doesn't end with '\n'.
1365
- s += "\n" unless s.end_with?("\n")
2183
+ def render_abbrevs()
2184
+ @registry.abbrev_each do |abbrev, prefix|
2185
+ yield " - abbrev: \"#{abbrev}\"\n"
2186
+ yield " prefix: \"#{prefix}\"\n"
1366
2187
  end
1367
- return s
2188
+ end
2189
+
2190
+ private
2191
+
2192
+ def qq(s)
2193
+ return "" if s.nil?
2194
+ return '"' + s.gsub(/"/, '\\"') + '"'
1368
2195
  end
1369
2196
 
1370
2197
  end
1371
2198
 
1372
2199
 
1373
- 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
1374
2209
 
1375
2210
 
1376
2211
  end