Class: Toys::CLI
- Inherits:
-
Object
- Object
- Toys::CLI
- Defined in:
- lib/toys/cli.rb
Overview
A Toys-based CLI.
This is the entry point for command line execution, and the stable public interface to the framework. A CLI owns the configuration: it gathers all the settings in one place, constructs the Loader that finds and loads tool definitions, and constructs the Runner that runs them. It also provides #child, which clones the configuration so a tool can be run under modified settings.
Running a tool is delegated to the Runner; #run and #load_tool are thin wrappers around it that supply the CLI's configuration.
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_source do
def run
puts "Hello, world!"
end
end
exit(cli.run(*ARGV))
The currently running CLI is also available at runtime, as Toys::Context#cli. Use it when a tool needs the CLI configuration itself, most often to build a modified copy with #child. For example:
# My .toys.rb
tool "bar" do
def run
# Run "some-tool" with the tools from the "my-tools" gem also
# available.
child = cli.child(copy_sources: true) do |c|
c.add_source(Toys::SourceSpec.gem("my-tools"), high_priority: true)
end
child.run("some-tool")
end
end
A tool that simply wants to invoke another tool should instead use the runner, as described in Runner.
Instance Attribute Summary collapse
-
#base_level ⇒ Integer?
readonly
The initial logger level in this CLI, used as the level for verbosity 0.
-
#completion ⇒ Toys::Completion::Base, Proc
readonly
The overall completion strategy for this CLI.
-
#executable_name ⇒ String
readonly
The effective executable name used for usage text in this CLI.
-
#extra_delimiters ⇒ String
readonly
The string of tool name delimiter characters (besides space).
-
#logger ⇒ Logger?
readonly
The global logger, if any.
-
#logger_factory ⇒ Proc
readonly
The logger factory.
-
#tool_name_splitter ⇒ Toys::ToolNameSplitter
readonly
The splitter that interprets delimiters in tool names, reflecting this CLI's #extra_delimiters.
Class Method Summary collapse
-
.default_completion ⇒ Object
Returns a default Completion that simply uses the tool's completion.
-
.default_error_handler ⇒ Proc
Returns a bare-bones error handler that simply reraises the error it is given.
-
.default_logger_factory ⇒ Proc
Returns a default logger factory that generates simple loggers that write to the current stderr.
-
.default_middleware_lookup ⇒ Toys::ModuleLookup
Returns a default ModuleLookup for middleware that points at the StandardMiddleware module.
-
.default_middleware_stack ⇒ Array<Toys::Middleware::Spec>
Returns a default set of middleware that may be used as a starting point for a typical CLI.
-
.default_mixin_lookup ⇒ Toys::ModuleLookup
Returns a default ModuleLookup for mixins that points at the StandardMixins module.
-
.default_template_lookup ⇒ Toys::ModuleLookup
Returns a default empty ModuleLookup for templates.
Instance Method Summary collapse
-
#add_config_block(high_priority: false, source_name: nil, context_directory: nil, &block) ⇒ self
deprecated
Deprecated.
Prefer #add_source.
-
#add_config_path(path, high_priority: false, source_name: nil, context_directory: :parent) ⇒ self
deprecated
Deprecated.
Prefer #add_source.
-
#add_search_path(search_path, high_priority: false, context_directory: :path) ⇒ self
Checks the given search directory.
-
#add_search_path_hierarchy(start: nil, terminate: [], high_priority: false, context_directory: :path) ⇒ self
Walk up the directory hierarchy from the given start location, searching for toplevel tool files and directories, and add any found.
-
#add_source(spec = nil, high_priority: false, &block) ⇒ self
Add a source to the source list, described by the given source spec.
-
#child(copy_sources: false, **opts) {|cli| ... } ⇒ Toys::CLI
Make a clone of this CLI with the same settings.
-
#finalize_sources! ⇒ self
Finalize the source list.
-
#initialize(executable_name: nil, middleware_stack: nil, extra_delimiters: "", toplevel_tool_dir_name: nil, toplevel_tool_file_name: nil, mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil, logger_factory: nil, logger: nil, base_level: nil, error_handler: nil, completion: nil, source_list: nil, git_cache: nil, gems_util: nil) ⇒ CLI
constructor
Create a CLI.
-
#load_tool(*args, verbosity: 0) {|context| ... } ⇒ Object
Prepare a tool to be run, but just execute the given block rather than performing a full run of the tool.
-
#loader ⇒ Toys::Loader
The current loader for this CLI.
-
#run(*args, verbosity: 0) ⇒ Integer
Run the CLI with the given command line arguments.
-
#runner ⇒ Toys::Runner
The runner this CLI uses to run tools, configured with this CLI's settings.
Constructor Details
#initialize(executable_name: nil, middleware_stack: nil, extra_delimiters: "", toplevel_tool_dir_name: nil, toplevel_tool_file_name: nil, mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil, logger_factory: nil, logger: nil, base_level: nil, error_handler: nil, completion: nil, source_list: nil, git_cache: nil, gems_util: 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_delimiters: 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 sources
-
toplevel_tool_dir_name: Directory name containing tool files -
toplevel_tool_file_name: File name for tools -
source_list: Initial sources to populate -
git_cache: How to resolve git sources -
gems_util: How to resolve gem sources
-
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/toys/cli.rb', line 159 def initialize(executable_name: nil, middleware_stack: nil, extra_delimiters: "", toplevel_tool_dir_name: nil, toplevel_tool_file_name: nil, mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil, logger_factory: nil, logger: nil, base_level: nil, error_handler: nil, completion: nil, source_list: nil, git_cache: nil, gems_util: 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 @param_logger_factory = logger_factory @logger_factory = logger ? proc { |_tool| logger } : logger_factory || CLI.default_logger_factory @base_level = base_level @extra_delimiters = extra_delimiters @tool_name_splitter = ToolNameSplitter.new(extra_delimiters) @toplevel_tool_dir_name = toplevel_tool_dir_name @toplevel_tool_file_name = toplevel_tool_file_name @source_list = source_list&.dup || SourceList.new @git_cache = git_cache @gems_util = gems_util @loader = @runner = nil @source_definition_mutex = ::Monitor.new end |
Instance Attribute Details
#base_level ⇒ Integer? (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.
290 291 292 |
# File 'lib/toys/cli.rb', line 290 def base_level @base_level end |
#completion ⇒ Toys::Completion::Base, Proc (readonly)
The overall completion strategy for this CLI.
296 297 298 |
# File 'lib/toys/cli.rb', line 296 def completion @completion end |
#executable_name ⇒ String (readonly)
The effective executable name used for usage text in this CLI.
258 259 260 |
# File 'lib/toys/cli.rb', line 258 def executable_name @executable_name end |
#extra_delimiters ⇒ String (readonly)
The string of tool name delimiter characters (besides space).
264 265 266 |
# File 'lib/toys/cli.rb', line 264 def extra_delimiters @extra_delimiters end |
#logger ⇒ Logger? (readonly)
The global logger, if any.
277 278 279 |
# File 'lib/toys/cli.rb', line 277 def logger @logger end |
#logger_factory ⇒ Proc (readonly)
The logger factory.
283 284 285 |
# File 'lib/toys/cli.rb', line 283 def logger_factory @logger_factory end |
#tool_name_splitter ⇒ Toys::ToolNameSplitter (readonly)
The splitter that interprets delimiters in tool names, reflecting this CLI's #extra_delimiters.
271 272 273 |
# File 'lib/toys/cli.rb', line 271 def tool_name_splitter @tool_name_splitter end |
Class Method Details
.default_completion ⇒ Object
Returns a default Completion that simply uses the tool's completion.
717 718 719 720 721 |
# File 'lib/toys/cli.rb', line 717 def default_completion proc do |context| context.tool.completion.call(context) end end |
.default_error_handler ⇒ Proc
Returns a bare-bones error handler that simply reraises the error it is
given. A Toys::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.
700 701 702 |
# File 'lib/toys/cli.rb', line 700 def default_error_handler Runner::DEFAULT_ERROR_HANDLER end |
.default_logger_factory ⇒ Proc
Returns a default logger factory that generates simple loggers that write to the current stderr.
710 711 712 |
# File 'lib/toys/cli.rb', line 710 def default_logger_factory Runner::DEFAULT_LOGGER_FACTORY end |
.default_middleware_lookup ⇒ Toys::ModuleLookup
Returns a default ModuleLookup for middleware that points at the StandardMiddleware module.
678 679 680 |
# File 'lib/toys/cli.rb', line 678 def default_middleware_lookup ModuleLookup.new.add_path("toys/standard_middleware") end |
.default_middleware_stack ⇒ Array<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:
- StandardMiddleware::SetDefaultDescriptions providing defaults for description fields.
- StandardMiddleware::ShowHelp adding the
--helpflag and providing default behavior for namespaces. - StandardMiddleware::HandleUsageErrors
- StandardMiddleware::AddVerbosityFlags adding the
--verboseand--quietflags for managing the logger level.
653 654 655 656 657 658 659 660 |
# File 'lib/toys/cli.rb', line 653 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_lookup ⇒ Toys::ModuleLookup
Returns a default ModuleLookup for mixins that points at the StandardMixins module.
668 669 670 |
# File 'lib/toys/cli.rb', line 668 def default_mixin_lookup ModuleLookup.new.add_path("toys/standard_mixins") end |
.default_template_lookup ⇒ Toys::ModuleLookup
Returns a default empty ModuleLookup for templates.
687 688 689 |
# File 'lib/toys/cli.rb', line 687 def default_template_lookup ModuleLookup.new end |
Instance Method Details
#add_config_block(high_priority: false, source_name: nil, context_directory: nil, &block) ⇒ self
Prefer #add_source.
Add a block to the source list.
This is a deprecated legacy method that has been superseded by #add_source. Instead of:
cli.add_config_block do
...
end
You should now:
cli.add_source do
...
end
Or, if you need to configure the source name or context directory:
source = Toys::SourceSpec.block(context_directory: "/var/project") do
...
end
cli.add_source(source)
630 631 632 633 634 635 636 |
# File 'lib/toys/cli.rb', line 630 def add_config_block(high_priority: false, source_name: nil, context_directory: nil, &block) spec = SourceSpec.block(source_name: source_name, context_directory: context_directory, &block) add_source(spec, high_priority: high_priority) end |
#add_config_path(path, high_priority: false, source_name: nil, context_directory: :parent) ⇒ self
Prefer #add_source.
Add a specific tool file or directory to the source list.
This is a deprecated legacy method that has been superseded by
#add_source. However, note that while add_config_path sets a
particular context directory by default, #add_source does not. So the
equivalent of:
cli.add_config_path("/path/to/tools")
is technically:
source = Toys::SourceSpec.path("/path/to/tools",
context_directory: "/path/to")
cli.add_source(source)
580 581 582 583 584 585 586 587 588 |
# File 'lib/toys/cli.rb', line 580 def add_config_path(path, high_priority: false, source_name: nil, context_directory: :parent) path = SourceSpec.check_and_normalize_path(path) context_directory = resolve_context_directory(context_directory, path) spec = SourceSpec.path(path, source_name: source_name, context_directory: context_directory) add_source(spec, high_priority: high_priority) end |
#add_search_path(search_path, high_priority: false, context_directory: :path) ⇒ self
Checks the given search directory. If it contains a tool file and/or
tool directory (identified by the toplevel_tool_file_name and
toplevel_tool_dir_name constructor arguments), those are added to the
source list. If the given search directory path does not exist or does
not contain either the file or directory, nothing is added.
The main Toys executable uses this method to load tools from directories
in the TOYS_PATH.
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/toys/cli.rb', line 377 def add_search_path(search_path, high_priority: false, context_directory: :path) # Pre-check for source list finalization. Do this so that we error out # (instead of just silently do nothing) if the source list is finalized # and the "empty" checks below result in no attempt to add a source. check_source_list_open search_path = SourceSpec.check_and_normalize_path(search_path, name: "search_path") context_directory = resolve_context_directory(context_directory, search_path) paths = [] if @toplevel_tool_file_name file_path = ::File.join(search_path, @toplevel_tool_file_name) paths << @toplevel_tool_file_name if !::File.directory?(file_path) && ::File.readable?(file_path) end if @toplevel_tool_dir_name dir_path = ::File.join(search_path, @toplevel_tool_dir_name) paths << @toplevel_tool_dir_name if ::File.directory?(dir_path) && ::File.readable?(dir_path) end unless paths.empty? spec = SourceSpec.path(search_path, relative_paths: paths, context_directory: context_directory) add_source(spec, high_priority: high_priority) end self end |
#add_search_path_hierarchy(start: nil, terminate: [], high_priority: false, context_directory: :path) ⇒ self
Walk up the directory hierarchy from the given start location, searching for toplevel tool files and directories, and add any found. Starts at the given directory and works up through parent directories until it reaches the file system root or it encounters one of the "terminate" directories.
The main Toys executable uses this method to load tools from the current directory and its ancestors.
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# File 'lib/toys/cli.rb', line 440 def add_search_path_hierarchy(start: nil, terminate: [], high_priority: false, context_directory: :path) start = SourceSpec.check_and_normalize_path(start || ::Dir.getwd, name: "start path") terminate = terminate.map { |path| SourceSpec.check_and_normalize_path(path, name: "terminate path") } path = start 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, context_directory: context_directory) end self end |
#add_source(spec = nil, high_priority: false, &block) ⇒ self
Add a source to the source list, described by the given source spec.
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.
The source can be specified in one of three ways:
- A source spec built using one of the SourceSpec module methods. If you need to configure the context directory or name of the source, you must use a full SourceSpec object.
- A string (or other object convertible to a path, such as a
Pathname) interpreted as a file system path, which will be passed to SourceSpec.path to get the source spec. - A block, which will be passed to SourceSpec.block to get the source spec. (Do not include an argument if passing a block.)
The spec is not resolved here. The loader resolves it, at most once, the first time it looks up a tool, so a source that cannot be read, fetched, or activated fails then rather than now.
332 333 334 335 336 337 338 339 340 341 342 343 344 345 |
# File 'lib/toys/cli.rb', line 332 def add_source(spec = nil, high_priority: false, &block) if block raise ::ArgumentError, "Ambiguous source: both spec and block passed" if spec spec = SourceSpec.block(&block) elsif !spec.is_a?(SourceSpec::Base) raise ::ArgumentError, "No source spec, path, or block given" if spec.nil? # Anything else is taken as a path. spec = SourceSpec.path(spec) end ensure_source_list_open do @source_list.add(spec, high_priority: high_priority) end self end |
#child(copy_sources: false, **opts) {|cli| ... } ⇒ Toys::CLI
Make a clone of this CLI with the same settings.
By default, the new CLI has no tool sources, which is sometimes useful
for calling another tool that has to be loaded from a different source
configuration. Alternately, you can pass copy_sources: true to start
with the same sources as the original (to which you can add additional
sources before starting to load tools). Sources are copied before the
block (if any) is called, so any sources the block adds at high priority
will take priority over the originals.
218 219 220 221 222 |
# File 'lib/toys/cli.rb', line 218 def child(copy_sources: false, **opts) cli = CLI.new(**current_settings(copy_sources), **opts) yield cli if block_given? cli end |
#finalize_sources! ⇒ self
Finalize the source list. Any subsequent attempt to add a source will raise SourceListFinalizedError.
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 |
# File 'lib/toys/cli.rb', line 523 def finalize_sources! @source_definition_mutex.synchronize do unless @loader loader = Loader.new(@source_list, middleware_stack: @middleware_stack, tool_name_splitter: @tool_name_splitter, mixin_lookup: @mixin_lookup, template_lookup: @template_lookup, middleware_lookup: @middleware_lookup, git_cache: @git_cache, gems_util: @gems_util) runner = Runner.new(loader, logger_factory: @logger_factory, base_level: @base_level, error_handler: @error_handler, executable_name: @executable_name, external_data: {Context::Key::CLI => self}) @loader = loader @runner = runner end end self end |
#load_tool(*args, verbosity: 0) {|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 neither wraps errors nor passes them to the error handler. An error such as a failure to parse arguments or to load the requested tool is raised out of this method as-is, so the block does not execute and this method does not return.
Note that calling this finalizes this CLI's source list if not already finalized. Any subsequent attempt to add a source raises SourceListFinalizedError.
508 509 510 511 512 513 514 515 |
# File 'lib/toys/cli.rb', line 508 def load_tool(*args, verbosity: 0) result = nil runner.run(args.flatten, verbosity: verbosity, wrap_errors: false, handle_errors: false) do |ctx| result = yield ctx end result end |
#loader ⇒ Toys::Loader
The current loader for this CLI.
Note that calling this finalizes this CLI's source list if not already finalized. Any subsequent attempt to add a source raises SourceListFinalizedError.
233 234 235 236 |
# File 'lib/toys/cli.rb', line 233 def loader finalize_sources! @loader end |
#run(*args, verbosity: 0) ⇒ Integer
Run the CLI with the given command line arguments. Handles exceptions using the error handler.
Any error that is not handled by the tool itself is passed to this CLI's
error handler, and this method returns the exit code that the handler
produces. Ordinary errors arrive as a Toys::ContextualError wrapper,
but a signal that no tool intercepted arrives as the SignalException
itself, unwrapped. See the error_handler argument to #initialize.
Note that calling this finalizes this CLI's source list if not already finalized. Any subsequent attempt to add a source raises SourceListFinalizedError.
483 484 485 |
# File 'lib/toys/cli.rb', line 483 def run(*args, verbosity: 0) runner.run(args.flatten, verbosity: verbosity) end |
#runner ⇒ Toys::Runner
The runner this CLI uses to run tools, configured with this CLI's settings. Use it directly when you need more control over a single run than #run provides, such as turning off error handling.
Note that calling this finalizes this CLI's source list if not already finalized. Any subsequent attempt to add a source raises SourceListFinalizedError.
249 250 251 252 |
# File 'lib/toys/cli.rb', line 249 def runner finalize_sources! @runner end |