Class: Toys::CLI

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

Overview

A Toys-based CLI.

This is the entry point for command line execution. It includes the set of tool definitions (and/or information on how to load them from the file system), configuration parameters such as logging and error handling, and a method to call to invoke a command.

This is the class to instantiate to create a Toys-based command line executable. For example:

#!/usr/bin/env ruby
require "toys-core"
cli = Toys::CLI.new
cli.add_config_block do
  def run
    puts "Hello, world!"
  end
end
exit(cli.run(*ARGV))

The currently running CLI is also available at runtime, and can be used by tools that want to invoke other tools. For example:

# My .toys.rb
tool "foo" do
  def run
    puts "in foo"
  end
end
tool "bar" do
  def run
    puts "in bar"
    cli.run "foo"
  end
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(executable_name: nil, middleware_stack: nil, extra_delimiters: "", config_dir_name: nil, config_file_name: nil, index_file_name: nil, preload_file_name: nil, preload_dir_name: nil, data_dir_name: nil, lib_dir_name: nil, mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil, logger_factory: nil, logger: nil, base_level: nil, error_handler: nil, completion: nil) ⇒ CLI

Create a CLI.

Most configuration parameters (besides tool definitions and tool lookup paths) are set as options passed to the constructor. These options fall roughly into four categories:

  • Options affecting output behavior:
    • logger: A global logger for all tools to use
    • logger_factory: A proc that returns a logger to use
    • base_level: The default log level
    • error_handler: Callback for handling exceptions
    • executable_name: The name of the executable
  • Options affecting tool specification
    • extra_delimibers: Tool name delimiters besides space
    • completion: Tab completion handler
  • Options affecting tool definition
    • middleware_stack: The middleware applied to all tools
    • mixin_lookup: Where to find well-known mixins
    • middleware_lookup: Where to find well-known middleware
    • template_lookup: Where to find well-known templates
  • Options affecting tool files and directories
    • config_dir_name: Directory name containing tool files
    • config_file_name: File name for tools
    • index_file_name: Name of index files in tool directories
    • preload_file_name: Name of preload files in tool directories
    • preload_dir_name: Name of preload directories in tool directories
    • data_dir_name: Name of data directories in tool directories

Parameters:

  • logger (Logger) (defaults to: nil)

    A global logger to use for all tools. This can be set if the CLI will call at most one tool at a time. However, it will behave incorrectly if CLI might run multiple tools at the same time with different verbosity settings (since the logger cannot have multiple level settings simultaneously). In that case, do not set a global logger, but use the logger_factory parameter instead.

  • logger_factory (Proc) (defaults to: nil)

    A proc that takes a ToolDefinition as an argument, and returns a Logger to use when running that tool. Optional. If not provided (and no global logger is set), default_logger_factory is called to get a basic default.

  • base_level (Integer) (defaults to: nil)

    The logger level that should correspond to zero verbosity. Optional. If not provided, defaults to the current level of the logger (which is often Logger::WARN).

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

    A proc that is called when an unhandled exception (a normal exception subclassing StandardError, an error loading a toys config file subclassing SyntaxError, or an unhandled signal subclassing SignalException) is detected. The proc should take a Toys::ContextualError, whose cause is the unhandled exception, as the sole argument, and report the error. It should return an exit code (normally nonzero) appropriate to the error. Optional. If not provided, default_error_handler is called to get a basic default handler.

  • executable_name (String) (defaults to: nil)

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

  • extra_delimiters (String) (defaults to: "")

    A string containing characters that can function as delimiters in a tool name. Defaults to empty. Allowed characters are period, colon, and slash.

  • completion (Toys::Completion::Base) (defaults to: nil)

    A specifier for shell tab completion for the CLI as a whole. Optional. If not provided, default_completion is called to get a default completion that delegates to the tool.

  • middleware_stack (Array<Toys::Middleware::Spec>) (defaults to: nil)

    An array of middleware that will be used by default for all tools. Optional. If not provided, uses a default set of middleware defined in default_middleware_stack. To include no middleware, pass the empty array explicitly.

  • mixin_lookup (Toys::ModuleLookup) (defaults to: nil)

    A lookup for well-known mixin modules (i.e. with symbol names). Optional. If not provided, defaults to the set of standard mixins provided by toys-core, as defined by default_mixin_lookup. If you explicitly want no standard mixins, pass an empty instance of ModuleLookup.

  • middleware_lookup (Toys::ModuleLookup) (defaults to: nil)

    A lookup for well-known middleware classes. Optional. If not provided, defaults to the set of standard middleware classes provided by toys-core, as defined by default_middleware_lookup. If you explicitly want no standard middleware, pass an empty instance of ModuleLookup.

  • template_lookup (Toys::ModuleLookup) (defaults to: nil)

    A lookup for well-known template classes. Optional. If not provided, defaults to the set of standard template classes provided by toys core, as defined by default_template_lookup. If you explicitly want no standard tenokates, pass an empty instance of ModuleLookup.

  • config_dir_name (String) (defaults to: nil)

    A directory with this name that appears in the loader path, is treated as a configuration directory whose contents are loaded into the toys configuration. Optional. If not provided, toplevel configuration directories are disabled. Note: the standard toys executable sets this to ".toys".

  • config_file_name (String) (defaults to: nil)

    A file with this name that appears in the loader path, is treated as a toplevel configuration file whose contents are loaded into the toys configuration. This does not include "index" configuration files located within a configuration directory. Optional. If not provided, toplevel configuration files are disabled. Note: the standard toys executable sets this to ".toys.rb".

  • index_file_name (String) (defaults to: nil)

    A file with this name that appears in any configuration directory is loaded first as a standalone configuration file. This does not include "toplevel" configuration files outside configuration directories. Optional. If not provided, index configuration files are disabled. Note: the standard toys executable sets this to ".toys.rb".

  • preload_file_name (String) (defaults to: nil)

    A file with this name that appears in any configuration directory is preloaded using require before any tools in that configuration directory are defined. A preload file includes normal Ruby code, rather than Toys DSL definitions. The preload file is loaded before any files in a preload directory. Optional. If not provided, preload files are disabled. Note: the standard toys executable sets this to ".preload.rb".

  • preload_dir_name (String) (defaults to: nil)

    A directory with this name that appears in any configuration directory is searched for Ruby files, which are preloaded using require before any tools in that configuration directory are defined. Files in a preload directory include normal Ruby code, rather than Toys DSL definitions. Files in a preload directory are loaded after any standalone preload file. Optional. If not provided, preload directories are disabled. Note: the standard toys executable sets this to ".preload".

  • data_dir_name (String) (defaults to: nil)

    A directory with this name that appears in any configuration directory is added to the data directory search path for any tool file in that directory. Optional. If not provided, data directories are disabled. Note: the standard toys executable sets this to ".data".

  • lib_dir_name (String) (defaults to: nil)

    A directory with this name that appears in any configuration directory is added to the Ruby load path when executing any tool file in that directory. Optional. If not provided, lib directories are disabled. Note: the standard toys executable sets this to ".lib".



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/toys/cli.rb', line 175

def initialize(executable_name: nil, # rubocop:disable Metrics/MethodLength
               middleware_stack: nil,
               extra_delimiters: "",
               config_dir_name: nil,
               config_file_name: nil,
               index_file_name: nil,
               preload_file_name: nil,
               preload_dir_name: nil,
               data_dir_name: nil,
               lib_dir_name: nil,
               mixin_lookup: nil,
               middleware_lookup: nil,
               template_lookup: nil,
               logger_factory: nil,
               logger: nil,
               base_level: nil,
               error_handler: nil,
               completion: nil)
  @executable_name = executable_name || ::File.basename($PROGRAM_NAME)
  @middleware_stack = middleware_stack || CLI.default_middleware_stack
  @mixin_lookup = mixin_lookup || CLI.default_mixin_lookup
  @middleware_lookup = middleware_lookup || CLI.default_middleware_lookup
  @template_lookup = template_lookup || CLI.default_template_lookup
  @error_handler = error_handler || CLI.default_error_handler
  @completion = completion || CLI.default_completion
  @logger = logger
  @logger_factory = logger ? proc { logger } : logger_factory || CLI.default_logger_factory
  @base_level = base_level
  @extra_delimiters = extra_delimiters
  @config_dir_name = config_dir_name
  @config_file_name = config_file_name
  @index_file_name = index_file_name
  @preload_file_name = preload_file_name
  @preload_dir_name = preload_dir_name
  @data_dir_name = data_dir_name
  @lib_dir_name = lib_dir_name
  @loader = Loader.new(
    index_file_name: @index_file_name,
    preload_dir_name: @preload_dir_name,
    preload_file_name: @preload_file_name,
    data_dir_name: @data_dir_name,
    lib_dir_name: @lib_dir_name,
    middleware_stack: @middleware_stack,
    extra_delimiters: @extra_delimiters,
    mixin_lookup: @mixin_lookup,
    template_lookup: @template_lookup,
    middleware_lookup: @middleware_lookup
  )
end

Instance Attribute Details

#base_levelInteger? (readonly)

The initial logger level in this CLI, used as the level for verbosity 0. May be nil, indicating it will use the initial logger setting.

Returns:

  • (Integer, nil)


298
299
300
# File 'lib/toys/cli.rb', line 298

def base_level
  @base_level
end

#completionToys::Completion::Base, Proc (readonly)

The overall completion strategy for this CLI.

Returns:



304
305
306
# File 'lib/toys/cli.rb', line 304

def completion
  @completion
end

#executable_nameString (readonly)

The effective executable name used for usage text in this CLI.

Returns:

  • (String)


273
274
275
# File 'lib/toys/cli.rb', line 273

def executable_name
  @executable_name
end

#extra_delimitersString (readonly)

The string of tool name delimiter characters (besides space).

Returns:

  • (String)


279
280
281
# File 'lib/toys/cli.rb', line 279

def extra_delimiters
  @extra_delimiters
end

#loaderToys::Loader (readonly)

The current loader for this CLI.

Returns:



267
268
269
# File 'lib/toys/cli.rb', line 267

def loader
  @loader
end

#loggerLogger? (readonly)

The global logger, if any.

Returns:

  • (Logger, nil)


285
286
287
# File 'lib/toys/cli.rb', line 285

def logger
  @logger
end

#logger_factoryProc (readonly)

The logger factory.

Returns:

  • (Proc)


291
292
293
# File 'lib/toys/cli.rb', line 291

def logger_factory
  @logger_factory
end

Class Method Details

.default_completionObject

Returns a default Completion that simply uses the tool's completion.



576
577
578
579
580
# File 'lib/toys/cli.rb', line 576

def default_completion
  proc do |context|
    context.tool.completion.call(context)
  end
end

.default_error_handlerProc

Returns a bare-bones error handler that takes simply reraises the error. If the original error (the cause of the Toys::ContextualError) was a SignalException (or a subclass such as Interrupted), that SignalException itself is reraised so that the Ruby VM has a chance to handle it. Otherwise, for any other error, the Toys::ContextualError is reraised.

Returns:

  • (Proc)


551
552
553
554
555
556
# File 'lib/toys/cli.rb', line 551

def default_error_handler
  proc do |error|
    cause = error.cause
    raise cause.is_a?(::SignalException) ? cause : error
  end
end

.default_logger_factoryProc

Returns a default logger factory that generates simple loggers that write to STDERR.

Returns:

  • (Proc)


564
565
566
567
568
569
570
571
# File 'lib/toys/cli.rb', line 564

def default_logger_factory
  require "logger"
  proc do
    logger = ::Logger.new($stderr)
    logger.level = ::Logger::WARN
    logger
  end
end

.default_middleware_lookupToys::ModuleLookup

Returns a default ModuleLookup for middleware that points at the StandardMiddleware module.

Returns:



528
529
530
# File 'lib/toys/cli.rb', line 528

def default_middleware_lookup
  ModuleLookup.new.add_path("toys/standard_middleware")
end

.default_middleware_stackArray<Toys::Middleware::Spec>

Returns a default set of middleware that may be used as a starting point for a typical CLI. This set includes the following in order:

Returns:



503
504
505
506
507
508
509
510
# File 'lib/toys/cli.rb', line 503

def default_middleware_stack
  [
    Middleware.spec(:set_default_descriptions),
    Middleware.spec(:show_help, help_flags: true, fallback_execution: true),
    Middleware.spec(:handle_usage_errors),
    Middleware.spec(:add_verbosity_flags),
  ]
end

.default_mixin_lookupToys::ModuleLookup

Returns a default ModuleLookup for mixins that points at the StandardMixins module.

Returns:



518
519
520
# File 'lib/toys/cli.rb', line 518

def default_mixin_lookup
  ModuleLookup.new.add_path("toys/standard_mixins")
end

.default_template_lookupToys::ModuleLookup

Returns a default empty ModuleLookup for templates.

Returns:



537
538
539
# File 'lib/toys/cli.rb', line 537

def default_template_lookup
  ModuleLookup.new
end

Instance Method Details

#add_config_block(high_priority: false, source_name: nil, context_directory: nil, &block) ⇒ self

Add a configuration block to the loader.

This is used to create tools "inline", and is useful for simple command line executables based on Toys.

Parameters:

  • high_priority (Boolean) (defaults to: false)

    Add the config at the head of the priority list rather than the tail.

  • source_name (String) (defaults to: nil)

    The source name that will be shown in documentation for tools defined in this block. If omitted, a default unique string will be generated.

  • block (Proc)

    The block of configuration, executed in the context of the tool DSL DSL::Tool.

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

    The context directory for tools loaded from this block. You can pass a directory path as a string, or nil to denote no context. Defaults to nil.

Returns:

  • (self)


356
357
358
359
360
361
362
363
364
365
# File 'lib/toys/cli.rb', line 356

def add_config_block(high_priority: false,
                     source_name: nil,
                     context_directory: nil,
                     &block)
  @loader.add_block(high_priority: high_priority,
                    source_name: source_name,
                    context_directory: context_directory,
                    &block)
  self
end

#add_config_path(path, high_priority: false, source_name: nil, context_directory: :parent) ⇒ self

Add a specific configuration file or directory to the loader.

This is generally used to load a static or "built-in" set of tools, either for a standalone command line executable based on Toys, or to provide a "default" set of tools for a dynamic executable. For example, the main Toys executable uses this to load the builtin tools from its "builtins" directory.

Parameters:

  • path (String)

    A path to add. May reference a single Toys file or a Toys directory.

  • high_priority (Boolean) (defaults to: false)

    Add the config at the head of the priority list rather than the tail.

  • source_name (String) (defaults to: nil)

    A custom name for the root source. Optional.

  • context_directory (String, nil, :path, :parent) (defaults to: :parent)

    The context directory for tools loaded from this path. You can pass a directory path as a string, :path to denote the given path, :parent to denote the given path's parent directory, or nil to denote no context. Defaults to :parent.

Returns:

  • (self)


327
328
329
330
331
332
333
334
335
336
# File 'lib/toys/cli.rb', line 327

def add_config_path(path,
                    high_priority: false,
                    source_name: nil,
                    context_directory: :parent)
  @loader.add_path(path,
                   high_priority: high_priority,
                   source_name: source_name,
                   context_directory: context_directory)
  self
end

#add_search_path(search_path, high_priority: false, context_directory: :path) ⇒ self

Checks the given directory path. If it contains a config file and/or config directory, those are added to the loader.

The main Toys executable uses this method to load tools from directories in the TOYS_PATH.

Parameters:

  • search_path (String)

    A path to search for configs.

  • high_priority (Boolean) (defaults to: false)

    Add the configs at the head of the priority list rather than the tail.

  • context_directory (String, nil, :path, :parent) (defaults to: :path)

    The context directory for tools loaded from this path. You can pass a directory path as a string, :path to denote the given path, :parent to denote the given path's parent directory, or nil to denote no context. Defaults to :path.

Returns:

  • (self)


384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/toys/cli.rb', line 384

def add_search_path(search_path,
                    high_priority: false,
                    context_directory: :path)
  paths = []
  if @config_file_name
    file_path = ::File.join(search_path, @config_file_name)
    paths << @config_file_name if !::File.directory?(file_path) && ::File.readable?(file_path)
  end
  if @config_dir_name
    dir_path = ::File.join(search_path, @config_dir_name)
    paths << @config_dir_name if ::File.directory?(dir_path) && ::File.readable?(dir_path)
  end
  @loader.add_path_set(search_path, paths,
                       high_priority: high_priority,
                       context_directory: context_directory)
  self
end

#add_search_path_hierarchy(start: nil, terminate: [], high_priority: false) ⇒ self

Walk up the directory hierarchy from the given start location, and add to the loader any config files and directories found.

The main Toys executable uses this method to load tools from the current directory and its ancestors.

Parameters:

  • start (String) (defaults to: nil)

    The first directory to add. Defaults to the current working directory.

  • terminate (Array<String>) (defaults to: [])

    Optional list of directories that should terminate the search. If the walk up the directory tree encounters one of these directories, the search is halted without checking the terminating directory.

  • high_priority (Boolean) (defaults to: false)

    Add the configs at the head of the priority list rather than the tail.

Returns:

  • (self)


419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/toys/cli.rb', line 419

def add_search_path_hierarchy(start: nil, terminate: [], high_priority: false)
  path = start || ::Dir.pwd
  paths = []
  loop do
    break if terminate.include?(path)
    paths << path
    next_path = ::File.dirname(path)
    break if next_path == path
    path = next_path
  end
  paths.reverse! if high_priority
  paths.each do |p|
    add_search_path(p, high_priority: high_priority)
  end
  self
end

#child(**opts) {|cli| ... } ⇒ Toys::CLI

Make a clone with the same settings but no config blocks and no paths in the loader. This is sometimes useful for calling another tool that has to be loaded from a different configuration.

Parameters:

  • opts (keywords)

    Any configuration arguments that should be modified from the original. See #initialize for a list of recognized keywords.

Yield Parameters:

  • cli (Toys::CLI)

    If you pass a block, the new CLI is yielded to it so you can add paths and make other modifications.

Returns:



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/toys/cli.rb', line 237

def child(**opts)
  args = {
    executable_name: @executable_name,
    config_dir_name: @config_dir_name,
    config_file_name: @config_file_name,
    index_file_name: @index_file_name,
    preload_dir_name: @preload_dir_name,
    preload_file_name: @preload_file_name,
    data_dir_name: @data_dir_name,
    lib_dir_name: @lib_dir_name,
    middleware_stack: @middleware_stack,
    extra_delimiters: @extra_delimiters,
    mixin_lookup: @mixin_lookup,
    middleware_lookup: @middleware_lookup,
    template_lookup: @template_lookup,
    logger: @logger,
    logger_factory: @logger_factory,
    base_level: @base_level,
    error_handler: @error_handler,
    completion: @completion,
  }.merge(opts)
  cli = CLI.new(**args)
  yield cli if block_given?
  cli
end

#load_tool(*args) {|context| ... } ⇒ Object

Prepare a tool to be run, but just execute the given block rather than performing a full run of the tool. This is intended for testing tools. Unlike #run, this does not catch errors and perform error handling.

Parameters:

  • args (String...)

    Command line arguments specifying which tool to run and what arguments to pass to it. You may pass either a single array of strings, or a series of string arguments.

Yield Parameters:

Returns:

  • (Object)

    The value returned from the block.



480
481
482
483
484
485
486
# File 'lib/toys/cli.rb', line 480

def load_tool(*args)
  tool, remaining = @loader.lookup(args.flatten)
  context = build_context(tool, remaining)
  execute_tool(tool, context) do |ctx|
    ctx.exit(yield ctx)
  end
end

#run(*args, verbosity: 0, delegated_from: nil) ⇒ Integer

Run the CLI with the given command line arguments. Handles exceptions using the error handler.

Parameters:

  • args (String...)

    Command line arguments specifying which tool to run and what arguments to pass to it. You may pass either a single array of strings, or a series of string arguments.

  • verbosity (Integer) (defaults to: 0)

    Initial verbosity. Default is 0.

  • delegated_from (Toys::Context) (defaults to: nil)

    The context from which this execution is delegated. Optional. Should be set only if this is a delegated execution.

Returns:

  • (Integer)

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



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/toys/cli.rb', line 450

def run(*args, verbosity: 0, delegated_from: nil)
  tool, remaining = ContextualError.capture("Error finding tool definition") do
    @loader.lookup(args.flatten)
  end
  ContextualError.capture_path(
    "Error during tool execution!", tool.source_info&.source_path,
    tool_name: tool.full_name, tool_args: remaining
  ) do
    context = build_context(tool, remaining,
                            verbosity: verbosity,
                            delegated_from: delegated_from)
    run_handler = make_run_handler(tool)
    execute_tool(tool, context, &run_handler)
  end
rescue ContextualError => e
  @error_handler.call(e).to_i
end