toys-core 0.14.7 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/toys/cli.rb CHANGED
@@ -72,7 +72,7 @@ module Toys
72
72
  # * `preload_dir_name`: Name of preload directories in tool directories
73
73
  # * `data_dir_name`: Name of data directories in tool directories
74
74
  #
75
- # @param logger [Logger] A global logger to use for all tools. This may be
75
+ # @param logger [Logger] A global logger to use for all tools. This can be
76
76
  # set if the CLI will call at most one tool at a time. However, it will
77
77
  # behave incorrectly if CLI might run multiple tools at the same time
78
78
  # with different verbosity settings (since the logger cannot have
@@ -80,19 +80,21 @@ module Toys
80
80
  # global logger, but use the `logger_factory` parameter instead.
81
81
  # @param logger_factory [Proc] A proc that takes a {Toys::ToolDefinition}
82
82
  # as an argument, and returns a `Logger` to use when running that tool.
83
- # Optional. If not provided (and no global logger is set), CLI will use
84
- # a default factory that writes generates loggers writing formatted
85
- # output to `STDERR`, as defined by {Toys::CLI.default_logger_factory}.
83
+ # Optional. If not provided (and no global logger is set),
84
+ # {Toys::CLI.default_logger_factory} is called to get a basic default.
86
85
  # @param base_level [Integer] The logger level that should correspond
87
86
  # to zero verbosity.
88
87
  # Optional. If not provided, defaults to the current level of the
89
88
  # logger (which is often `Logger::WARN`).
90
- # @param error_handler [Proc,nil] A proc that is called when an error is
91
- # caught. The proc should take a {Toys::ContextualError} argument and
92
- # report the error. It should return an exit code (normally nonzero).
93
- # Optional. If not provided, defaults to an instance of
94
- # {Toys::CLI::DefaultErrorHandler}, which displays an error message to
95
- # `STDERR`.
89
+ # @param error_handler [Proc,nil] A proc that is called when an unhandled
90
+ # exception (a normal exception subclassing `StandardError`, an error
91
+ # loading a toys config file subclassing `SyntaxError`, or an unhandled
92
+ # signal subclassing `SignalException`) is detected. The proc should
93
+ # take a {Toys::ContextualError}, whose cause is the unhandled
94
+ # exception, as the sole argument, and report the error. It should
95
+ # return an exit code (normally nonzero) appropriate to the error.
96
+ # Optional. If not provided, {Toys::CLI.default_error_handler} is
97
+ # called to get a basic default handler.
96
98
  # @param executable_name [String] The executable name displayed in help
97
99
  # text. Optional. Defaults to the ruby program name.
98
100
  #
@@ -101,9 +103,8 @@ module Toys
101
103
  # characters are period, colon, and slash.
102
104
  # @param completion [Toys::Completion::Base] A specifier for shell tab
103
105
  # completion for the CLI as a whole.
104
- # Optional. If not provided, defaults to an instance of
105
- # {Toys::CLI::DefaultCompletion}, which delegates completion to the
106
- # relevant tool.
106
+ # Optional. If not provided, {Toys::CLI.default_completion} is called
107
+ # to get a default completion that delegates to the tool.
107
108
  #
108
109
  # @param middleware_stack [Array<Toys::Middleware::Spec>] An array of
109
110
  # middleware that will be used by default for all tools.
@@ -198,8 +199,8 @@ module Toys
198
199
  @mixin_lookup = mixin_lookup || CLI.default_mixin_lookup
199
200
  @middleware_lookup = middleware_lookup || CLI.default_middleware_lookup
200
201
  @template_lookup = template_lookup || CLI.default_template_lookup
201
- @error_handler = error_handler || DefaultErrorHandler.new
202
- @completion = completion || DefaultCompletion.new
202
+ @error_handler = error_handler || CLI.default_error_handler
203
+ @completion = completion || CLI.default_completion
203
204
  @logger = logger
204
205
  @logger_factory = logger ? proc { logger } : logger_factory || CLI.default_logger_factory
205
206
  @base_level = base_level
@@ -461,9 +462,10 @@ module Toys
461
462
  context = build_context(tool, remaining,
462
463
  verbosity: verbosity,
463
464
  delegated_from: delegated_from)
464
- execute_tool(tool, context, &:run)
465
+ run_handler = make_run_handler(tool)
466
+ execute_tool(tool, context, &run_handler)
465
467
  end
466
- rescue ContextualError, ::Interrupt => e
468
+ rescue ContextualError => e
467
469
  @error_handler.call(e).to_i
468
470
  end
469
471
 
@@ -487,102 +489,6 @@ module Toys
487
489
  end
488
490
  end
489
491
 
490
- ##
491
- # A basic error handler that prints out captured errors to a stream or
492
- # a logger.
493
- #
494
- class DefaultErrorHandler
495
- ##
496
- # Create an error handler.
497
- #
498
- # @param output [IO,nil] Where to write errors. Default is `$stderr`.
499
- #
500
- def initialize(output: $stderr)
501
- require "toys/utils/terminal"
502
- @terminal = Utils::Terminal.new(output: output)
503
- end
504
-
505
- ##
506
- # The error handler routine. Prints out the error message and backtrace,
507
- # and returns the correct result code.
508
- #
509
- # @param error [Exception] The error that occurred.
510
- # @return [Integer] The result code for the execution.
511
- #
512
- def call(error)
513
- cause = error
514
- case error
515
- when ContextualError
516
- cause = error.cause
517
- @terminal.puts(cause_string(cause))
518
- @terminal.puts(context_string(error), :bold)
519
- when ::Interrupt
520
- @terminal.puts
521
- @terminal.puts("INTERRUPTED", :bold)
522
- else
523
- @terminal.puts(cause_string(error))
524
- end
525
- exit_code_for(cause)
526
- end
527
-
528
- private
529
-
530
- def exit_code_for(error)
531
- case error
532
- when ArgParsingError
533
- 2
534
- when NotRunnableError
535
- 126
536
- when ::Interrupt
537
- 130
538
- else
539
- 1
540
- end
541
- end
542
-
543
- def cause_string(cause)
544
- lines = ["#{cause.class}: #{cause.message}"]
545
- cause.backtrace.each_with_index.reverse_each do |bt, i|
546
- lines << " #{(i + 1).to_s.rjust(3)}: #{bt}"
547
- end
548
- lines.join("\n")
549
- end
550
-
551
- def context_string(error)
552
- lines = [
553
- error.banner || "Unexpected error!",
554
- " #{error.cause.class}: #{error.cause.message}",
555
- ]
556
- if error.config_path
557
- lines << " in config file: #{error.config_path}:#{error.config_line}"
558
- end
559
- if error.tool_name
560
- lines << " while executing tool: #{error.tool_name.join(' ').inspect}"
561
- if error.tool_args
562
- lines << " with arguments: #{error.tool_args.inspect}"
563
- end
564
- end
565
- lines.join("\n")
566
- end
567
- end
568
-
569
- ##
570
- # A Completion that implements the default algorithm for a CLI. This
571
- # algorithm simply determines the tool and uses its completion.
572
- #
573
- class DefaultCompletion < Completion::Base
574
- ##
575
- # Returns candidates for the current completion.
576
- #
577
- # @param context [Toys::Completion::Context] the current completion
578
- # context including the string fragment.
579
- # @return [Array<Toys::Completion::Candidate>] an array of candidates
580
- #
581
- def call(context)
582
- context.tool.completion.call(context)
583
- end
584
- end
585
-
586
492
  class << self
587
493
  ##
588
494
  # Returns a default set of middleware that may be used as a starting
@@ -637,56 +543,43 @@ module Toys
637
543
  end
638
544
 
639
545
  ##
640
- # Returns a logger factory that generates loggers that write to stderr.
641
- # All loggers generated by this factory share a single
642
- # {Toys::Utils::Terminal}, so log entries may interleave but will not
643
- # interrupt one another.
546
+ # Returns a bare-bones error handler that takes simply reraises the
547
+ # error. If the original error (the cause of the {Toys::ContextualError})
548
+ # was a `SignalException` (or a subclass such as `Interrupted`), that
549
+ # `SignalException` itself is reraised so that the Ruby VM has a chance
550
+ # to handle it. Otherwise, for any other error, the
551
+ # {Toys::ContextualError} is reraised.
552
+ #
553
+ # @return [Proc]
554
+ #
555
+ def default_error_handler
556
+ proc do |error|
557
+ cause = error.cause
558
+ raise cause.is_a?(::SignalException) ? cause : error
559
+ end
560
+ end
561
+
562
+ ##
563
+ # Returns a default logger factory that generates simple loggers that
564
+ # write to STDERR.
644
565
  #
645
566
  # @return [Proc]
646
567
  #
647
568
  def default_logger_factory
648
- require "toys/utils/terminal"
649
- shared_terminal = Utils::Terminal.new(output: $stderr)
650
569
  proc do
651
- logger = ::Logger.new(shared_terminal)
652
- logger.formatter = proc do |severity, time, _progname, msg|
653
- msg_str =
654
- case msg
655
- when ::String
656
- msg
657
- when ::Exception
658
- "#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).join("\n")
659
- else
660
- msg.inspect
661
- end
662
- format_log(shared_terminal, time, severity, msg_str)
663
- end
570
+ logger = ::Logger.new($stderr)
664
571
  logger.level = ::Logger::WARN
665
572
  logger
666
573
  end
667
574
  end
668
575
 
669
- private
670
-
671
- def format_log(terminal, time, severity, msg)
672
- timestr = time.strftime("%Y-%m-%d %H:%M:%S")
673
- header = format("[%<time>s %<sev>5s]", time: timestr, sev: severity)
674
- styled_header =
675
- case severity
676
- when "FATAL"
677
- terminal.apply_styles(header, :bright_magenta, :bold, :underline)
678
- when "ERROR"
679
- terminal.apply_styles(header, :bright_red, :bold)
680
- when "WARN"
681
- terminal.apply_styles(header, :bright_yellow)
682
- when "INFO"
683
- terminal.apply_styles(header, :bright_cyan)
684
- when "DEBUG"
685
- terminal.apply_styles(header, :white)
686
- else
687
- header
688
- end
689
- "#{styled_header} #{msg}\n"
576
+ ##
577
+ # Returns a default Completion that simply uses the tool's completion.
578
+ #
579
+ def default_completion
580
+ proc do |context|
581
+ context.tool.completion.call(context)
582
+ end
690
583
  end
691
584
  end
692
585
 
@@ -704,7 +597,20 @@ module Toys
704
597
  tool.tool_class.new(arg_parser.data)
705
598
  end
706
599
 
707
- def execute_tool(tool, context)
600
+ def make_run_handler(tool)
601
+ run_handler = tool.run_handler
602
+ if run_handler.is_a?(::Symbol)
603
+ proc do |context|
604
+ context.send(run_handler)
605
+ end
606
+ else
607
+ proc do |context|
608
+ context.instance_exec(&run_handler)
609
+ end
610
+ end
611
+ end
612
+
613
+ def execute_tool(tool, context, &block)
708
614
  tool.source_info&.apply_lib_paths
709
615
  tool.run_initializers(context)
710
616
  cur_logger = context[Context::Key::LOGGER]
@@ -713,9 +619,7 @@ module Toys
713
619
  cur_logger.level = (base_level || original_level) - context[Context::Key::VERBOSITY].to_i
714
620
  end
715
621
  begin
716
- executor = build_executor(tool, context) do
717
- yield context
718
- end
622
+ executor = build_executor(tool, context, &block)
719
623
  catch(:result) do
720
624
  executor.call
721
625
  0
@@ -733,11 +637,10 @@ module Toys
733
637
  elsif !tool.runnable?
734
638
  raise NotRunnableError, "No implementation for tool #{tool.display_name.inspect}"
735
639
  else
736
- yield
640
+ yield context
737
641
  end
738
- rescue ::Interrupt => e
739
- raise e unless tool.handles_interrupts?
740
- handle_interrupt(context, tool.interrupt_handler, e)
642
+ rescue ::SignalException => e
643
+ handle_signal_by_tool(context, tool, e)
741
644
  end
742
645
  end
743
646
  tool.built_middleware.reverse_each do |middleware|
@@ -750,24 +653,25 @@ module Toys
750
653
  usage_errors = context[Context::Key::USAGE_ERRORS]
751
654
  handler = tool.usage_error_handler
752
655
  raise ArgParsingError, usage_errors if handler.nil?
753
- handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
754
- if handler.arity.zero?
755
- context.instance_exec(&handler)
756
- else
757
- context.instance_exec(usage_errors, &handler)
758
- end
656
+ call_handler(context, handler, usage_errors)
759
657
  end
760
658
 
761
- def handle_interrupt(context, handler, exception)
659
+ def handle_signal_by_tool(context, tool, exception)
660
+ handler = tool.signal_handler(exception.signo)
661
+ raise exception unless handler
662
+ call_handler(context, handler, exception)
663
+ rescue ::SignalException => e
664
+ raise e if e.equal?(exception)
665
+ handle_signal_by_tool(context, tool, e)
666
+ end
667
+
668
+ def call_handler(context, handler, argument)
762
669
  handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
763
670
  if handler.arity.zero?
764
671
  context.instance_exec(&handler)
765
672
  else
766
- context.instance_exec(exception, &handler)
673
+ context.instance_exec(argument, &handler)
767
674
  end
768
- rescue ::Interrupt => e
769
- raise e if e.equal?(exception)
770
- handle_interrupt(context, handler, e)
771
675
  end
772
676
 
773
677
  def make_executor(middleware, context, next_executor)
data/lib/toys/compat.rb CHANGED
@@ -12,37 +12,27 @@ module Toys
12
12
  parts = ::RUBY_VERSION.split(".")
13
13
  ruby_version = parts[0].to_i * 10000 + parts[1].to_i * 100 + parts[2].to_i
14
14
 
15
- ##
16
15
  # @private
17
- #
18
16
  def self.jruby?
19
17
  ::RUBY_ENGINE == "jruby"
20
18
  end
21
19
 
22
- ##
23
20
  # @private
24
- #
25
21
  def self.truffleruby?
26
22
  ::RUBY_ENGINE == "truffleruby"
27
23
  end
28
24
 
29
- ##
30
25
  # @private
31
- #
32
26
  def self.windows?
33
27
  ::RbConfig::CONFIG["host_os"] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
34
28
  end
35
29
 
36
- ##
37
30
  # @private
38
- #
39
31
  def self.allow_fork?
40
32
  !jruby? && !truffleruby? && !windows?
41
33
  end
42
34
 
43
- ##
44
35
  # @private
45
- #
46
36
  def self.supports_suggestions?
47
37
  unless defined?(@supports_suggestions)
48
38
  begin
@@ -60,9 +50,7 @@ module Toys
60
50
  @supports_suggestions
61
51
  end
62
52
 
63
- ##
64
53
  # @private
65
- #
66
54
  def self.suggestions(word, list)
67
55
  if supports_suggestions?
68
56
  ::DidYouMean::SpellChecker.new(dictionary: list).correct(word)
@@ -73,16 +61,12 @@ module Toys
73
61
 
74
62
  # The :base argument to Dir.glob requires Ruby 2.5 or later.
75
63
  if ruby_version >= 20500
76
- ##
77
64
  # @private
78
- #
79
65
  def self.glob_in_dir(glob, dir)
80
66
  ::Dir.glob(glob, base: dir)
81
67
  end
82
68
  else
83
- ##
84
69
  # @private
85
- #
86
70
  def self.glob_in_dir(glob, dir)
87
71
  ::Dir.chdir(dir) { ::Dir.glob(glob) }
88
72
  end
@@ -90,16 +74,12 @@ module Toys
90
74
 
91
75
  # Dir.children requires Ruby 2.5 or later.
92
76
  if ruby_version >= 20500
93
- ##
94
77
  # @private
95
- #
96
78
  def self.dir_children(dir)
97
79
  ::Dir.children(dir)
98
80
  end
99
81
  else
100
- ##
101
82
  # @private
102
- #
103
83
  def self.dir_children(dir)
104
84
  ::Dir.entries(dir) - [".", ".."]
105
85
  end
@@ -110,16 +90,12 @@ module Toys
110
90
  # This also hits TruffleRuby
111
91
  # (see https://github.com/oracle/truffleruby/issues/2567)
112
92
  if ruby_version >= 20700 && !truffleruby?
113
- ##
114
93
  # @private
115
- #
116
94
  def self.instantiate(klass, args, kwargs, block)
117
95
  klass.new(*args, **kwargs, &block)
118
96
  end
119
97
  else
120
- ##
121
98
  # @private
122
- #
123
99
  def self.instantiate(klass, args, kwargs, block)
124
100
  formals = klass.instance_method(:initialize).parameters
125
101
  if kwargs.empty? && formals.all? { |arg| arg.first != :key && arg.first != :keyrest }
@@ -133,26 +109,35 @@ module Toys
133
109
  # File.absolute_path? requires Ruby 2.7 or later. For earlier Rubies, use
134
110
  # an ad-hoc mechanism.
135
111
  if ruby_version >= 20700
136
- ##
137
112
  # @private
138
- #
139
113
  def self.absolute_path?(path)
140
114
  ::File.absolute_path?(path)
141
115
  end
142
116
  elsif ::Dir.getwd =~ /^[a-zA-Z]:/
143
- ##
144
117
  # @private
145
- #
146
118
  def self.absolute_path?(path)
147
119
  /^[a-zA-Z]:/.match?(path)
148
120
  end
149
121
  else
150
- ##
151
122
  # @private
152
- #
153
123
  def self.absolute_path?(path)
154
124
  path.start_with?("/")
155
125
  end
156
126
  end
127
+
128
+ # The second argument to method_defined? and private_method_defined?
129
+ # requires Ruby 2.6 or later.
130
+ if ruby_version >= 20600
131
+ # @private
132
+ def self.method_defined_without_ancestors?(klass, name)
133
+ klass.method_defined?(name, false) || klass.private_method_defined?(name, false)
134
+ end
135
+ else
136
+ # @private
137
+ def self.method_defined_without_ancestors?(klass, name)
138
+ klass.instance_methods(false).include?(name) ||
139
+ klass.private_instance_methods(false).include?(name)
140
+ end
141
+ end
157
142
  end
158
143
  end
data/lib/toys/context.rb CHANGED
@@ -139,22 +139,30 @@ module Toys
139
139
  #
140
140
  # This is a convenience getter for {Toys::Context::Key::ARGS}.
141
141
  #
142
+ # If the `args` method is overridden by the tool, you can still access it
143
+ # using the name `__args`.
144
+ #
142
145
  # @return [Array<String>]
143
146
  #
144
147
  def args
145
148
  @__data[Key::ARGS]
146
149
  end
150
+ alias __args args
147
151
 
148
152
  ##
149
153
  # The currently running CLI.
150
154
  #
151
155
  # This is a convenience getter for {Toys::Context::Key::CLI}.
152
156
  #
157
+ # If the `cli` method is overridden by the tool, you can still access it
158
+ # using the name `__cli`.
159
+ #
153
160
  # @return [Toys::CLI]
154
161
  #
155
162
  def cli
156
163
  @__data[Key::CLI]
157
164
  end
165
+ alias __cli cli
158
166
 
159
167
  ##
160
168
  # Return the context directory for this tool. Generally, this defaults
@@ -164,71 +172,98 @@ module Toys
164
172
  #
165
173
  # This is a convenience getter for {Toys::Context::Key::CONTEXT_DIRECTORY}.
166
174
  #
175
+ # If the `context_directory` method is overridden by the tool, you can
176
+ # still access it using the name `__context_directory`.
177
+ #
167
178
  # @return [String] Context directory path
168
179
  # @return [nil] if there is no context.
169
180
  #
170
181
  def context_directory
171
182
  @__data[Key::CONTEXT_DIRECTORY]
172
183
  end
184
+ alias __context_directory context_directory
173
185
 
174
186
  ##
175
187
  # The logger for this execution.
176
188
  #
177
189
  # This is a convenience getter for {Toys::Context::Key::LOGGER}.
178
190
  #
191
+ # If the `logger` method is overridden by the tool, you can still access it
192
+ # using the name `__logger`.
193
+ #
179
194
  # @return [Logger]
180
195
  #
181
196
  def logger
182
197
  @__data[Key::LOGGER]
183
198
  end
199
+ alias __logger logger
184
200
 
185
201
  ##
186
202
  # The full name of the tool being executed, as an array of strings.
187
203
  #
188
204
  # This is a convenience getter for {Toys::Context::Key::TOOL_NAME}.
189
205
  #
206
+ # If the `tool_name` method is overridden by the tool, you can still access
207
+ # it using the name `__tool_name`.
208
+ #
190
209
  # @return [Array<String>]
191
210
  #
192
211
  def tool_name
193
212
  @__data[Key::TOOL_NAME]
194
213
  end
214
+ alias __tool_name tool_name
195
215
 
196
216
  ##
197
217
  # The source of the tool being executed.
198
218
  #
199
219
  # This is a convenience getter for {Toys::Context::Key::TOOL_SOURCE}.
200
220
  #
221
+ # If the `tool_source` method is overridden by the tool, you can still
222
+ # access it using the name `__tool_source`.
223
+ #
201
224
  # @return [Toys::SourceInfo]
202
225
  #
203
226
  def tool_source
204
227
  @__data[Key::TOOL_SOURCE]
205
228
  end
229
+ alias __tool_source tool_source
206
230
 
207
231
  ##
208
232
  # The (possibly empty) array of errors detected during argument parsing.
209
233
  #
210
234
  # This is a convenience getter for {Toys::Context::Key::USAGE_ERRORS}.
211
235
  #
236
+ # If the `usage_errors` method is overridden by the tool, you can still
237
+ # access it using the name `__usage_errors`.
238
+ #
212
239
  # @return [Array<Toys::ArgParser::UsageError>]
213
240
  #
214
241
  def usage_errors
215
242
  @__data[Key::USAGE_ERRORS]
216
243
  end
244
+ alias __usage_errors usage_errors
217
245
 
218
246
  ##
219
247
  # The current verbosity setting as an integer.
220
248
  #
221
249
  # This is a convenience getter for {Toys::Context::Key::VERBOSITY}.
222
250
  #
251
+ # If the `verbosity` method is overridden by the tool, you can still access
252
+ # it using the name `__verbosity`.
253
+ #
223
254
  # @return [Integer]
224
255
  #
225
256
  def verbosity
226
257
  @__data[Key::VERBOSITY]
227
258
  end
259
+ alias __verbosity verbosity
228
260
 
229
261
  ##
230
262
  # Fetch an option or other piece of data by key.
231
263
  #
264
+ # If the `get` method is overridden by the tool, you can still access it
265
+ # using the name `__get` or the `[]` operator.
266
+ #
232
267
  # @param key [Symbol]
233
268
  # @return [Object]
234
269
  #
@@ -251,6 +286,9 @@ module Toys
251
286
  ##
252
287
  # Set one or more options or other context data by key.
253
288
  #
289
+ # If the `set` method is overridden by the tool, you can still access it
290
+ # using the name `__set`.
291
+ #
254
292
  # @return [self]
255
293
  #
256
294
  # @overload set(key, value)
@@ -272,6 +310,7 @@ module Toys
272
310
  end
273
311
  self
274
312
  end
313
+ alias __set set
275
314
 
276
315
  ##
277
316
  # The subset of the context that uses string or symbol keys. By convention,
@@ -279,6 +318,9 @@ module Toys
279
318
  # include well-known context values such as verbosity or private context
280
319
  # values used by middleware or mixins.
281
320
  #
321
+ # If the `options` method is overridden by the tool, you can still access
322
+ # it using the name `__options`.
323
+ #
282
324
  # @return [Hash]
283
325
  #
284
326
  def options
@@ -286,10 +328,14 @@ module Toys
286
328
  k.is_a?(::Symbol) || k.is_a?(::String)
287
329
  end
288
330
  end
331
+ alias __options options
289
332
 
290
333
  ##
291
334
  # Find the given data file or directory in this tool's search path.
292
335
  #
336
+ # If the `find_data` method is overridden by the tool, you can still access
337
+ # it using the name `__find_data`.
338
+ #
293
339
  # @param path [String] The path to find
294
340
  # @param type [nil,:file,:directory] Type of file system object to find,
295
341
  # or nil to return any type.
@@ -300,10 +346,14 @@ module Toys
300
346
  def find_data(path, type: nil)
301
347
  @__data[Key::TOOL_SOURCE].find_data(path, type: type)
302
348
  end
349
+ alias __find_data find_data
303
350
 
304
351
  ##
305
352
  # Exit immediately with the given status code.
306
353
  #
354
+ # If the `exit` method is overridden by the tool, you can still access it
355
+ # using the name `__exit` or by calling {Context.exit}.
356
+ #
307
357
  # @param code [Integer] The status code, which should be 0 for no error,
308
358
  # or nonzero for an error condition. Default is 0.
309
359
  # @return [void]
@@ -311,6 +361,7 @@ module Toys
311
361
  def exit(code = 0)
312
362
  Context.exit(code)
313
363
  end
364
+ alias __exit exit
314
365
 
315
366
  ##
316
367
  # Exit immediately with the given status code. This class method can be
data/lib/toys/core.rb CHANGED
@@ -9,7 +9,7 @@ module Toys
9
9
  # Current version of Toys core.
10
10
  # @return [String]
11
11
  #
12
- VERSION = "0.14.7"
12
+ VERSION = "0.15.0"
13
13
  end
14
14
 
15
15
  ##