Class: Toys::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/toys/runner.rb

Overview

An object that runs tools.

A Runner holds the environment in which tools run. This includes the Loader that resolves a tool name to a tool definition, along with run policy such as how to obtain a logger for a tool and what to do with an error the tool did not handle. This environment is fixed when the Runner is constructed.

Everything specific to a single invocation, such as which tool to run and the arguments to pass to it, is passed to #run. A Runner is thus itself immutable and holds no per-run state, so one Runner can run any number of tools and is safe to share. This says nothing about the tools it runs, however; running two tools at the same time is safe only if the tools themselves are, since a tool can modify global state such as the load path or a shared logger. See the logger parameter of CLI#initialize for one such caveat.

The Runner that is running a tool is available to that tool as Context#runner, so a tool can use it to run a sibling tool in the same process. For example:

# My .toys.rb
tool "foo" do
  def run
    puts "in foo"
  end
end
tool "bar" do
  def run
    puts "in bar"
    runner.run(["foo"])
  end
end

Constant Summary collapse

DEFAULT_LOGGER_FACTORY =

The singleton default logger_factory Proc, which simply returns a new logger writing to the current stderr.

Returns:

  • (Proc)
proc { |_tool|
  logger = ::Logger.new($stderr)
  logger.level = ::Logger::WARN
  logger
}.freeze
DEFAULT_ERROR_HANDLER =

The singleton default error_handler Proc, which simply reraises the error it is given. A ContextualError is reraised as itself, so that a rescue block has access to the context information. An unhandled SignalException (or a subclass such as Interrupt) is also reraised as itself, so that the Ruby VM has a chance to handle it normally.

Returns:

  • (Proc)
proc { |error|
  raise(error)
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(loader, logger_factory: nil, base_level: nil, error_handler: nil, executable_name: nil, external_data: {}) ⇒ Runner

Create a Runner.

This performs no I/O and raises nothing. Tools are looked up, loaded, and run only when #run is called.

Parameters:

  • loader (Toys::Loader)

    The loader used to look up tools.

  • logger_factory (Proc, nil) (defaults to: nil)

    A proc that takes a ToolDefinition as an argument, and returns a logger to use when running that tool. If not given, DEFAULT_LOGGER_FACTORY is used.

  • base_level (Integer, nil) (defaults to: nil)

    The logger level that corresponds to zero verbosity. If not provided, the level the logger has before a run adjusts it is used (typically Logger::WARN). A run nested inside another run that shares the same logger uses the base level already in effect for that logger, so that verbosity does not compound.

  • error_handler (Proc, nil) (defaults to: nil)

    A proc that is called when an unhandled exception is detected during a run that has error handling enabled. The proc takes the error as its sole argument, and should report it. It could simply reraise the exception, or it could display an error message and/or return an exit code (normally nonzero) appropriate to the error. The error is one of the following:

    • a ContextualError, wrapping a StandardError, a ScriptError, or a nested ContextualError
    • a bare StandardError or ScriptError, if the run disabled error wrapping
    • a bare SignalException, which is never wrapped

    Optional. If not given, DEFAULT_ERROR_HANDLER is used.

  • executable_name (String, nil) (defaults to: nil)

    The executable name displayed in help text. Optional. Defaults to the ruby program name.

  • external_data (Hash) (defaults to: {})

    Additional context data provided by the caller. It is merged underneath the data the Runner provides itself, so it cannot override runtime-owned keys.



104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/toys/runner.rb', line 104

def initialize(loader,
               logger_factory: nil,
               base_level: nil,
               error_handler: nil,
               executable_name: nil,
               external_data: {})
  @loader = loader
  @logger_factory = logger_factory || DEFAULT_LOGGER_FACTORY
  @base_level = base_level
  @error_handler = error_handler || DEFAULT_ERROR_HANDLER
  @executable_name = executable_name || ::File.basename($PROGRAM_NAME)
  @external_data = external_data
end

Instance Method Details

#run(args, verbosity: 0, wrap_errors: true, handle_errors: true) {|context| ... } ⇒ Integer

Run a tool.

The tool is looked up by matching a tool name at the beginning of the given arguments. The remaining arguments are then parsed into a Context, the tool's middleware is applied, and the tool is run.

If a block is passed, the runtime context is simply yielded to it in place of the tool's run handler, with the tool's middleware still applied. This is useful for testing parts of the tool runtime in isolation.

If the tool raises a SignalException that it does not handle itself, that exception propagates unwrapped, even when wrap_errors is enabled. This lets each tool in a nested execution dispatch it to its own on_interrupt or on_signal handler, and lets the error handler, and ultimately the Ruby VM, recognize it as a signal.

Parameters:

  • args (Array<String>)

    The command line arguments, including the name of the tool to look up. This must be an array of strings; it is an error to pass anything else.

  • verbosity (Integer) (defaults to: 0)

    Initial verbosity. Default is 0.

  • wrap_errors (boolean) (defaults to: true)

    If true (the default), wrap errors in ContextualError, including errors during the tool lookup, argument parsing, and tool execution. If false, propagate errors as-is and do not wrap them. A SignalException is never wrapped regardless of this setting; see above.

  • handle_errors (boolean) (defaults to: true)

    If true (the default), pass any error that reaches the end of the run to this Runner's error handler, and return the exit code it produces. If false, let the error propagate out of this method. A SystemExit is never passed to the error handler regardless of this setting, so Kernel.exit in a tool still exits the process.

Yield Parameters:

  • context (Toys::Context)

    If a block is given, it is invoked in place of the tool's run handler, with the tool's middleware still applied. This is intended for testing tools.

Returns:

  • (Integer)

    The resulting process status code (i.e. 0 for success).

Raises:

  • (ArgumentError)

    if args is not an array. Note that CLI#run, unlike this method, takes its arguments as a splat.



160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/toys/runner.rb', line 160

def run(args, verbosity: 0, wrap_errors: true, handle_errors: true, &block)
  unless args.is_a?(::Array)
    raise ::ArgumentError, "Tool arguments must be an array of strings: #{args.inspect}"
  end
  Invocation.new(runner: self,
                 args: args,
                 verbosity: verbosity,
                 wrap_errors: wrap_errors,
                 handle_errors: handle_errors,
                 delegated_from: nil,
                 block: block).run
end