benry-cmdapp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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