Class: Toys::CLI

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

Overview

A Toys-based CLI.

Use this class to implement a CLI using Toys.

Defined Under Namespace

Classes: DefaultErrorHandler

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(binary_name: nil, middleware_stack: nil, config_dir_name: nil, config_file_name: nil, index_file_name: nil, mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil, logger: nil, base_level: nil, error_handler: nil) ⇒ CLI

Create a CLI.

Parameters:

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

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

  • config_dir_name (String, nil) (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. The default toys CLI sets this to ".toys".

  • config_file_name (String, nil) (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. Optional. If not provided, toplevel configuration files are disabled. The default toys CLI sets this to ".toys.rb".

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

    A file with this name that appears in any configuration directory (not just a toplevel directory) is loaded first as a standalone configuration file. If not provided, standalone configuration files are disabled. The default toys CLI sets this to ".toys.rb".

  • middleware_stack (Array) (defaults to: nil)

    An array of middleware that will be used by default for all tools loaded by this CLI. If not provided, uses default_middleware_stack.

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

    A lookup for well-known mixin modules. If not provided, uses default_mixin_lookup.

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

    A lookup for well-known middleware classes. If not provided, uses default_middleware_lookup.

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

    A lookup for well-known template classes. If not provided, uses default_template_lookup.

  • logger (Logger, nil) (defaults to: nil)

    The logger to use. If not provided, a default logger that writes to STDERR is used.

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

    The logger level that should correspond to zero verbosity. If not provided, will default to the current level of the logger.

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

    A proc that is called when an error is caught. The proc should take a Toys::ContextualError argument and report the error. It should return an exit code (normally nonzero). Default is a DefaultErrorHandler writing to the logger.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/toys/cli.rb', line 83

def initialize(
  binary_name: nil, middleware_stack: nil,
  config_dir_name: nil, config_file_name: nil, index_file_name: nil,
  mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil,
  logger: nil, base_level: nil, error_handler: nil
)
  @logger = logger || self.class.default_logger
  @base_level = base_level || @logger.level
  @middleware_stack = middleware_stack || self.class.default_middleware_stack
  @binary_name = binary_name || ::File.basename($PROGRAM_NAME)
  @config_dir_name = config_dir_name
  @config_file_name = config_file_name
  @index_file_name = index_file_name
  @mixin_lookup = mixin_lookup || self.class.default_mixin_lookup
  @middleware_lookup = middleware_lookup || self.class.default_middleware_lookup
  @template_lookup = template_lookup || self.class.default_template_lookup
  @loader = Loader.new(
    index_file_name: index_file_name,
    mixin_lookup: @mixin_lookup, template_lookup: @template_lookup,
    middleware_lookup: @middleware_lookup, middleware_stack: @middleware_stack
  )
  @error_handler = error_handler || DefaultErrorHandler.new
end

Instance Attribute Details

#base_levelInteger (readonly)

Return the initial logger level in this CLI, used as the level for verbosity 0.

Returns:

  • (Integer)


130
131
132
# File 'lib/toys/cli.rb', line 130

def base_level
  @base_level
end

#binary_nameString (readonly)

Return the effective binary name used for usage text in this CLI

Returns:

  • (String)


117
118
119
# File 'lib/toys/cli.rb', line 117

def binary_name
  @binary_name
end

#loaderToys::Loader (readonly)

Return the current loader for this CLI

Returns:



111
112
113
# File 'lib/toys/cli.rb', line 111

def loader
  @loader
end

#loggerLogger (readonly)

Return the logger used by this CLI

Returns:

  • (Logger)


123
124
125
# File 'lib/toys/cli.rb', line 123

def logger
  @logger
end

Class Method Details

.default_logger(stream = $stderr) ⇒ Logger

Returns a default logger that logs to $stderr.

Parameters:

  • stream (IO) (defaults to: $stderr)

    Stream to write to. Defaults to $stderr.

Returns:

  • (Logger)


379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/toys/cli.rb', line 379

def default_logger(stream = $stderr)
  logger = ::Logger.new(stream)
  terminal = Utils::Terminal.new(output: stream)
  logger.formatter = proc do |severity, time, _progname, msg|
    msg_str =
      case msg
      when ::String
        msg
      when ::Exception
        "#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).join("\n")
      else
        msg.inspect
      end
    format_log(terminal, time, severity, msg_str)
  end
  logger.level = ::Logger::WARN
  logger
end

.default_middleware_lookupToys::Utils::ModuleLookup

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



360
361
362
# File 'lib/toys/cli.rb', line 360

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

.default_middleware_stackArray

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:

  • (Array)


334
335
336
337
338
339
340
341
342
# File 'lib/toys/cli.rb', line 334

def default_middleware_stack
  [
    [:set_default_descriptions],
    [:show_help, help_flags: true],
    [:handle_usage_errors],
    [:show_help, fallback_execution: true],
    [:add_verbosity_flags]
  ]
end

.default_mixin_lookupToys::Utils::ModuleLookup

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



350
351
352
# File 'lib/toys/cli.rb', line 350

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

.default_template_lookupToys::Utils::ModuleLookup

Returns a default empty ModuleLookup for templates.



369
370
371
# File 'lib/toys/cli.rb', line 369

def default_template_lookup
  Utils::ModuleLookup.new
end

Instance Method Details

#add_config_block(high_priority: false, path: nil, &block) ⇒ Object

Add a configuration block to the loader.

Parameters:

  • high_priority (Boolean) (defaults to: false)

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

  • path (String) (defaults to: nil)

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



157
158
159
160
# File 'lib/toys/cli.rb', line 157

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

#add_config_path(path, high_priority: false) ⇒ Object

Add a configuration file or directory to the loader.

If a CLI has a default tool set, it might use this to point to the directory that defines those tools. For example, the default Toys CLI uses this to load the builtin tools from the "builtins" directory.

Parameters:

  • path (String)

    A path to add.

  • high_priority (Boolean) (defaults to: false)

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



143
144
145
146
# File 'lib/toys/cli.rb', line 143

def add_config_path(path, high_priority: false)
  @loader.add_path(path, high_priority: high_priority)
  self
end

#add_search_path(search_path, high_priority: false) ⇒ Object

Searches the given directory for a well-known config directory and/or config file. If found, these are added to the loader.

Typically, a CLI will use this to find toys configs in the current working directory, the user's home directory, or some other well-known general configuration-oriented directory such as "/etc".

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.



174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/toys/cli.rb', line 174

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

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

A convenience method that searches the current working directory, and all ancestor directories, for configs to add to the loader.

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.

  • high_priority (Boolean) (defaults to: false)

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



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/toys/cli.rb', line 199

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 paths in the loader. This is sometimes useful for running sub-tools.

Parameters:

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

    Unused options that can be used by subclasses.

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:



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/toys/cli.rb', line 249

def child(_opts = {})
  cli = CLI.new(binary_name: @binary_name,
                config_dir_name: @config_dir_name,
                config_file_name: @config_file_name,
                index_file_name: @index_file_name,
                middleware_stack: @middleware_stack,
                mixin_lookup: @mixin_lookup,
                middleware_lookup: @middleware_lookup,
                template_lookup: @template_lookup,
                logger: @logger,
                base_level: @base_level,
                error_handler: @error_handler)
  yield cli if block_given?
  cli
end

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

Run the CLI with the given command line arguments.

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.

Returns:

  • (Integer)

    The resulting status code



226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/toys/cli.rb', line 226

def run(*args, verbosity: 0)
  tool_definition, remaining = ContextualError.capture("Error finding tool definition") do
    @loader.lookup(args.flatten)
  end
  ContextualError.capture_path(
    "Error during tool execution!", tool_definition.source_path,
    tool_name: tool_definition.full_name, tool_args: remaining
  ) do
    Runner.new(self, tool_definition).run(remaining, verbosity: verbosity)
  end
rescue ContextualError => e
  @error_handler.call(e)
end