benry-cmdapp 0.1.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.
@@ -0,0 +1,1376 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ ###
5
+ ### $Release: 0.1.0 $
6
+ ### $Copyright: copyright(c) 2023 kwatch@gmail.com $
7
+ ### $License: MIT License $
8
+ ###
9
+
10
+
11
+ require 'benry/cmdopt'
12
+
13
+
14
+ module Benry::CmdApp
15
+
16
+
17
+ SCHEMA_CLASS = Benry::CmdOpt::Schema
18
+ PARSER_CLASS = Benry::CmdOpt::Parser
19
+
20
+
21
+ class BaseError < StandardError; end
22
+
23
+ class DefinitionError < BaseError; end
24
+ class ActionDefError < DefinitionError; end
25
+ class OptionDefError < DefinitionError; end
26
+ class AliasDefError < DefinitionError; end
27
+
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
33
+
34
+
35
+ module Util
36
+ module_function
37
+
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
46
+ end
47
+
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
53
+ end
54
+
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
63
+ end
64
+
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?
71
+ end
72
+
73
+ def del_escape_seq(str)
74
+ #; [!wgp2b] deletes escape sequence.
75
+ return str.gsub(/\e\[.*?m/, '')
76
+ end
77
+
78
+ class Doing # :nodoc:
79
+ def inspect(); "<DOING>"; end
80
+ alias to_s inspect
81
+ end
82
+
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
101
+ end
102
+ end
103
+
104
+ def str_strong(s)
105
+ return "\e[4m#{s}\e[0m"
106
+ end
107
+
108
+ def str_weak(s)
109
+ return "\e[2m#{s}\e[0m"
110
+ end
111
+
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
138
+ end
139
+
140
+ end
141
+
142
+
143
+ class ActionIndex
144
+
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
169
+ end
170
+
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)
186
+ end
187
+
188
+ def get_action(action_name)
189
+ return @actions[action_name.to_s]
190
+ end
191
+
192
+ def register_action(action_name, action_metadata)
193
+ @actions[action_name.to_s] = action_metadata
194
+ action_metadata
195
+ end
196
+
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.")
202
+ end
203
+
204
+ def action_exist?(action_name)
205
+ return @actions.key?(action_name.to_s)
206
+ end
207
+
208
+ def each_action(&block)
209
+ @actions.values().each(&block)
210
+ nil
211
+ end
212
+
213
+ def action_result(action_name)
214
+ return @done[action_name.to_s]
215
+ end
216
+
217
+ def action_done(action_name, val)
218
+ @done[action_name.to_s] = val
219
+ val
220
+ end
221
+
222
+ def action_done?(action_name)
223
+ return @done.key?(action_name.to_s) && ! action_doing?(action_name)
224
+ end
225
+
226
+ def action_doing(action_name)
227
+ @done[action_name.to_s] = Util::DOING
228
+ nil
229
+ end
230
+
231
+ def action_doing?(action_name)
232
+ return action_result(action_name) == Util::DOING
233
+ end
234
+
235
+ def register_alias(alias_name, alias_obj)
236
+ @aliases[alias_name.to_s] = alias_obj
237
+ alias_obj
238
+ end
239
+
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.")
245
+ end
246
+
247
+ def get_alias(alias_name)
248
+ return @aliases[alias_name.to_s]
249
+ end
250
+
251
+ def alias_exist?(alias_name)
252
+ return @aliases.key?(alias_name)
253
+ end
254
+
255
+ def each_alias(&block)
256
+ @aliases.values().each(&block)
257
+ end
258
+
259
+ end
260
+
261
+
262
+ INDEX = ActionIndex.new
263
+
264
+
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
270
+
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
+ end
276
+
277
+
278
+ class ActionMetadata
279
+
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
290
+ end
291
+
292
+ attr_reader :name, :method, :klass, :schema, :desc, :detail, :postamble, :important, :tag
293
+
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?()
307
+ return nil
308
+ end
309
+
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)
316
+ end
317
+
318
+ def run_action(*args, **kwargs)
319
+ if ! $TRACE_MODE
320
+ __run_action(*args, **kwargs)
321
+ 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
331
+ end
332
+ nil
333
+ end
334
+
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
340
+ else
341
+ action_obj.__send__(@method, *args, **kwargs)
342
+ end
343
+ end
344
+ private :__run_action
345
+
346
+ def _new_action_object()
347
+ return @klass.new
348
+ end
349
+ protected :_new_action_object
350
+
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]
368
+ end
369
+
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."
380
+ end
381
+
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)
386
+ end
387
+
388
+ end
389
+
390
+
391
+ ACTION_METADATA_CLASS = ActionMetadata
392
+
393
+
394
+ class ActionWithArgs
395
+
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
401
+ end
402
+
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
412
+ end
413
+
414
+ def method()
415
+ return @action_metadata.method
416
+ end
417
+
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)
423
+ end
424
+
425
+ end
426
+
427
+
428
+ class HelpBuilder
429
+
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"
438
+ end
439
+ sb << content
440
+ sb << "\n" unless content.end_with?("\n")
441
+ return sb.join()
442
+ end
443
+
444
+ def config()
445
+ nil
446
+ end
447
+
448
+ def heading(title)
449
+ c = config()
450
+ format = c ? c.format_heading : Config::FORMAT_HEADING
451
+ return format % title
452
+ end
453
+
454
+ end
455
+
456
+
457
+ class ActionHelpBuilder < HelpBuilder
458
+
459
+ def initialize(action_metadata)
460
+ @am = action_metadata
461
+ end
462
+
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")
470
+ end
471
+
472
+ protected
473
+
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")
482
+ end
483
+ return sb.join()
484
+ end
485
+
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
519
+ end
520
+ #; [!pvu56] ignores 'Options:' section when no options exist.
521
+ return nil if sb.empty?
522
+ return build_section("Options", sb.join(), nil)
523
+ end
524
+
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")
531
+ end
532
+ return s
533
+ end
534
+
535
+ def config()
536
+ return @cmdapp_config
537
+ end
538
+
539
+ private
540
+
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
556
+ end
557
+ end
558
+ sb << ("]" * n)
559
+ return sb.join()
560
+ end
561
+
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
569
+ end
570
+
571
+ end
572
+
573
+
574
+ ACTION_HELP_BUILDER_CLASS = ActionHelpBuilder
575
+
576
+
577
+ class ActionScope
578
+
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
583
+
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)
587
+ end
588
+
589
+ private
590
+
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
611
+ end
612
+
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
624
+ end
625
+
626
+ SUBCLASSES = []
627
+
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)
662
+ end
663
+ end
664
+ end
665
+
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}"
712
+ end
713
+ 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
727
+ end
728
+ end
729
+
730
+ end
731
+
732
+
733
+ Action = ActionScope
734
+
735
+
736
+ class BuiltInAction < ActionScope
737
+
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)
753
+ 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
758
+ end
759
+
760
+ end
761
+
762
+
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
782
+
783
+
784
+ class Alias
785
+
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
792
+ end
793
+
794
+ attr_reader :alias_name, :action_name, :args, :important, :tag
795
+
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"
801
+ end
802
+ end
803
+
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?
811
+ end
812
+
813
+ end
814
+
815
+
816
+ class Config #< BasicObject
817
+
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
821
+
822
+ FORMAT_APPNAME = "\e[1m%s\e[0m"
823
+
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
827
+
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
833
+
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
877
+
878
+ end
879
+
880
+
881
+ class AppOptionSchema < Benry::CmdOpt::Schema
882
+
883
+ def initialize(config=nil)
884
+ super()
885
+ #; [!3ihzx] do nothing when config is nil.
886
+ 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 }
911
+ nil
912
+ end
913
+
914
+ end
915
+
916
+
917
+ APP_OPTION_SCHEMA_CLASS = AppOptionSchema
918
+
919
+
920
+ class Application
921
+
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
930
+ end
931
+
932
+ attr_reader :config, :schema, :help_builder, :callback
933
+
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})"
953
+ end
954
+ #; [!qk5q5] returns 1 as exit code when error occurred.
955
+ return 1
956
+ else
957
+ #; [!5oypr] returns 0 as exit code when no errors occurred.
958
+ return 0
959
+ end
960
+ end
961
+
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)
1007
+ end
1008
+
1009
+ protected
1010
+
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)
1014
+ end
1015
+
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)
1019
+ end
1020
+
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)
1025
+ 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
1046
+ end
1047
+ end
1048
+
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.
1052
+ if global_opts[:help]
1053
+ do_print_help_message(args, global_opts)
1054
+ do_validate_actions(args, global_opts)
1055
+ throw :SKIP # done
1056
+ end
1057
+ #; [!fslsy] prints version if '-V' or '--version' specified.
1058
+ if global_opts[:version]
1059
+ puts @config.app_version
1060
+ throw :SKIP # done
1061
+ 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)
1070
+ 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.
1092
+ else
1093
+ raise CommandError.new("#{c.app_command}: Action name required (run `#{c.app_command} -h` for details).")
1094
+ end
1095
+ return metadata
1096
+ end
1097
+
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 = ""
1154
+ 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
1162
+ end
1163
+
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_)
1192
+ end
1193
+ 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"
1204
+ end
1205
+ 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
1220
+ end
1221
+
1222
+ def do_teardown(exc)
1223
+ #; [!zxeo7] clears '$cmdapp_config'.
1224
+ $cmdapp_config = nil
1225
+ #; [!ufm1d] clears '$cmdapp_application'.
1226
+ $cmdapp_application = nil
1227
+ end
1228
+
1229
+ public
1230
+
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)
1235
+ end
1236
+
1237
+ end
1238
+
1239
+
1240
+ class AppHelpBuilder < HelpBuilder
1241
+
1242
+ def initialize(config, schema)
1243
+ @config = config
1244
+ @schema = schema
1245
+ end
1246
+
1247
+ def build_help_message(all=false, format=nil)
1248
+ #; [!rvpdb] returns help message.
1249
+ format ||= @config.format_help
1250
+ 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")
1263
+ end
1264
+
1265
+ protected
1266
+
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")
1284
+ end
1285
+ #; [!rvhzd] no preamble when neigher app desc nor detail specified.
1286
+ return nil if sb.empty?
1287
+ return sb.join()
1288
+ end
1289
+
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)
1297
+ end
1298
+
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
1311
+ 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
+ end
1317
+
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)
1335
+ end
1336
+ end
1337
+ return build_section("Actions", sb.join(), desc)
1338
+ end
1339
+
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
1353
+ 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
+ end
1359
+
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")
1366
+ end
1367
+ return s
1368
+ end
1369
+
1370
+ end
1371
+
1372
+
1373
+ APP_HELP_BUILDER_CLASS = AppHelpBuilder
1374
+
1375
+
1376
+ end