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/docs/guide.md CHANGED
@@ -4,10 +4,13 @@
4
4
 
5
5
  # Toys-Core User Guide
6
6
 
7
- Toys-Core is the command line framework underlying Toys. It implements most of
7
+ Toys-Core is the command line framework underlying
8
+ [Toys](https://dazuma.github.io/toys/gems/toys/latest). It implements most of
8
9
  the core functionality of Toys, including the tool DSL, argument parsing,
9
- loading Toys files, online help, subprocess control, and so forth. It can be
10
- used to create custom command line executables using the same facilities.
10
+ loading Toys files, online help, subprocess control, and so forth. Toys-Core
11
+ can be used to create custom command line executables, or it can be used to
12
+ provide mixins or templates in your gem to help your users define tools related
13
+ to your gem's functionality.
11
14
 
12
15
  If this is your first time using Toys-Core, we recommend starting with the
13
16
  [README](https://dazuma.github.io/toys/gems/toys-core/latest), which includes a
@@ -30,10 +33,10 @@ sophisticated command line tools.
30
33
  ## Conceptual overview
31
34
 
32
35
  Toys-Core is a **command line framework** in the traditional sense. It is
33
- intended to be used to write custom command line executables in Ruby. The
34
- framework provides common facilities such as argument parsing and online help,
35
- while your executable chooses and configures those facilities, and implements
36
- the actual behavior.
36
+ intended as the core component of the Toys gem, but is designed generically for
37
+ writing custom command line executables in Ruby. The framework provides common
38
+ facilities such as argument parsing and online help. Your executable can then
39
+ choose and configure those facilities, and implement the actual behavior.
37
40
 
38
41
  The entry point for Toys-Core is the **cli object**. Typically your executable
39
42
  script instantiates a CLI, configures it with the desired tool implementations,
@@ -49,25 +52,32 @@ An executable can customize its own facilities for writing tools by providing
49
52
  behavior across all tools by providing **middleware**.
50
53
 
51
54
  Most executables will provide a set of **static tools**, but it is possible to
52
- support user-provided tools as Toys does. Executables can customize how tool
53
- definitions are searched and loaded from the file system.
55
+ support user-provided tools as Toys does. Executables can customize how such
56
+ tool definitions are searched and loaded from the file system.
54
57
 
55
- Finally, an executable can customize many aspects of its behavior, such as the
58
+ An executable can customize many aspects of its behavior, such as the
56
59
  **logging output**, **error handling**, and even shell **tab completion**.
57
60
 
61
+ Finally, Toys-Core can also be used to publish **Toys extensions**, collections
62
+ of mixins, templates, and predefined tools that can be distributed as gems to
63
+ enhance Toys for other users.
64
+
58
65
  ## Using the CLI object
59
66
 
60
- The CLI object is the main entry point for Toys-Core. Most command line
67
+ The {Toys::CLI} object is the main entry point for Toys-Core. Most command line
61
68
  executables based on Toys-Core use it as follows:
62
69
 
63
70
  * Instantiate a CLI object, passing configuration parameters to the
64
- constructor.
71
+ {Toys::CLI#initialize constructor}.
65
72
  * Define the functionality of the CLI, either inline by passing it blocks, or
66
73
  by providing paths to tool files.
67
74
  * Call the {Toys::CLI#run} method, passing it the command line arguments
68
75
  (e.g. from `ARGV`).
69
76
  * Handle the result code, normally by passing it to `Kernel#exit`.
70
77
 
78
+ To get access to the CLI object, or any other Toys-Core classes, you first need
79
+ to ensure that the `toys-core` gem is loaded, and `require "toys-core"`.
80
+
71
81
  Following is a simple "hello world" example using the CLI:
72
82
 
73
83
  #!/usr/bin/env ruby
@@ -101,29 +111,29 @@ When you call {Toys::CLI#run}, the CLI runs through three phases:
101
111
  * **Loading** in which the CLI identifies which tool to run, and loads the
102
112
  tool from a tool **source**, which could be a block passed to the CLI, or a
103
113
  file loaded from the file system, git, or other location.
104
- * **Building context**, in which the CLI parses the command-line arguments
114
+ * **Context building**, in which the CLI parses the command-line arguments
105
115
  according to the flags and arguments declared by the tool, instantiates the
106
116
  tool, and populates the {Toys::Context} object (which is `self` when the
107
117
  tool is executed)
108
118
  * **Execution**, which involves running any initializers defined on the tool,
109
- applying middleware, running the tool, and handling errors.
119
+ applying middleware, running the tool's code, and handling errors.
110
120
 
111
121
  #### The Loader
112
122
 
113
123
  When the CLI needs the definition of a tool, it queries the {Toys::Loader}. The
114
124
  loader object is configured with a set of tool _sources_ representing ways to
115
- define a tool. These sources may be blocks passed directly to the CLI,
116
- directories and files in the file system, and remote git repositories. When a
117
- tool is requested by name, the loader is responsible for locating the tool
118
- definition in those sources, and constructing the tool definition object,
119
- represented by {Toys::ToolDefinition}.
125
+ define a tool. These sources may be blocks passed directly to the CLI, or
126
+ directories and files loaded from the file system or even remote git
127
+ repositories. When a tool is requested by name, the loader is responsible for
128
+ locating the tool definition in those sources, and constructing the tool
129
+ definition object, represented by {Toys::ToolDefinition}.
120
130
 
121
131
  One important property of the loader is that it is _lazy_. It queries tool
122
- sources only if it has reason to believe that a tool it is looking for may be
132
+ sources only when it has reason to believe that a tool it is looking for may be
123
133
  defined there. For example, if your tools are defined in a directory structure,
124
- the `foo bar` tool might live in the file `foo/bar.rb`. The loader will open
125
- that file, if it exists, only when the `foo bar` tool is requested. If instead
126
- `foo qux` is requested, the `foo/bar.rb` file is never even opened.
134
+ a tool named `foo bar` might live in the file `foo/bar.rb`. The loader will
135
+ open that file, if it exists, only when the `foo bar` tool is requested. If
136
+ instead `foo qux` is requested, the `foo/bar.rb` file is never even opened.
127
137
 
128
138
  Perhaps more subtly, if you call {Toys::CLI#add_config_block} to define tools,
129
139
  the block is stored in the loader object _but not called immediately_. Only
@@ -181,16 +191,9 @@ The execution phase involves:
181
191
  forgo or replace the main functionality, similar to Rack middleware.
182
192
  * Executing the tool itself by calling its `run` method.
183
193
 
184
- During execution, exceptions are caught and reported along with the location in
185
- the tool source where it was triggered. This logic is handled by the
186
- {Toys::ContextualError} class.
187
-
188
- The CLI can be configured with an error handler that responds to any exceptions
189
- raised during execution. An error handler is simply a callable object (such as
190
- a `Proc`) that takes an exception as an argument. The provided
191
- {Toys::CLI::DefaultErrorHandler} class provides the default behavior of the
192
- normal `toys` CLI, but you can provide any object that duck types the `call`
193
- method.
194
+ The CLI also implements error and signal handling, directing control either to
195
+ the tool's callbacks or to fallback handlers that can be configured into the
196
+ CLI itself. More on this later.
194
197
 
195
198
  #### Multiple runs
196
199
 
@@ -206,12 +209,12 @@ Generally, you control CLI features by passing arguments to its constructor.
206
209
  These features include:
207
210
 
208
211
  * How to find toys files and related code and data. See the section on
209
- [writing tool files](#Writing_tool_files).
212
+ [defining functionality](#Defining_functionality).
210
213
  * Middleware, providing common behavior for all tools. See the section on
211
214
  [customizing the middleware stack](#Customizing_default_behavior).
212
215
  * Common mixins and templates available to all tools. See the section on
213
216
  [how to define mixins and templates](#Defining_mixins_and_templates).
214
- * How logs and errors are reported. See the section on
217
+ * How logs, errors, and signals are reported. See the section on
215
218
  [customizing tool output](#Customizing_tool_output).
216
219
  * How the executable interacts with the shell, including setting up tab
217
220
  completion. See the
@@ -225,34 +228,526 @@ request.
225
228
 
226
229
  ## Defining functionality
227
230
 
231
+ Toys-Core uses (and indeed, provides the underlying implementation of) the
232
+ familiar Toys DSL that you can read about in the
233
+ [Toys README](https://dazuma.github.io/toys/gems/toys/latest) and
234
+ [Toys User's Guide](https://dazuma.github.io/toys/gems/toys/latest/file.guide.html).
235
+ This section assumes familiarity with those techniques for defining tools.
236
+
237
+ Here we will cover how to use the Toys-Core interfaces to point to specific
238
+ tool definition files or to load tool definitions programmatically. We'll also
239
+ look more closely at how tool definition works, providing insights into lazy
240
+ loading and the tool prioritization system.
241
+
228
242
  ### Writing tools in blocks
229
243
 
244
+ If you are writing your own command line executable using Toys-Core, often the
245
+ easiest way to define your tools is to use a block. The "hello world" example
246
+ at the start of this guide uses this technique:
247
+
248
+ #!/usr/bin/env ruby
249
+
250
+ require "toys-core"
251
+
252
+ cli = Toys::CLI.new
253
+
254
+ # Define the functionality by passing a block to the CLI
255
+ cli.add_config_block do
256
+ desc "My first executable!"
257
+ flag :whom, default: "world"
258
+ def run
259
+ puts "Hello, #{whom}!"
260
+ end
261
+ end
262
+
263
+ result = cli.run(*ARGV)
264
+ exit(result)
265
+
266
+ The block simply contains Toys DSL syntax. The above example configures the
267
+ "root tool", that is, the functionality of the program if you do not pass a
268
+ tool name on the command line. You can also include "tool" blocks to define
269
+ named tools, just as you would in a normal Toys file.
270
+
271
+ The reference documentation for {Toys::CLI#add_config_block} lists several
272
+ options that can be passed in. `:context_directory` lets you select a context
273
+ directory for tools defined in the block. Normally, this is the directory
274
+ containing the Toys files in which the tool is defined, but when tools are
275
+ defined in a block, it must be set explicitly. (Otherwise, calling the
276
+ `context_directory` from within the tool will return `nil`.) Similarly, the
277
+ `:source_name`, normally the path to the Toys file that appears in error
278
+ messages and documentation, can also be set explicitly.
279
+
230
280
  ### Writing tool files
231
281
 
282
+ If you want to define tools in separate files, you can do so and pass the file
283
+ paths to the CLI using {Toys::CLI#add_config_path}.
284
+
285
+ #!/usr/bin/env ruby
286
+
287
+ require "toys-core"
288
+
289
+ cli = Toys::CLI.new
290
+
291
+ # Load a file defining the functionality
292
+ cli.add_config_path("/usr/local/share/my_tool.rb)
293
+
294
+ result = cli.run(*ARGV)
295
+ exit(result)
296
+
297
+ The contents of `/usr/local/share/my_tool.rb` could then be:
298
+
299
+ desc "My first executable!"
300
+ flag :whom, default: "world"
301
+ def run
302
+ puts "Hello, #{whom}!"
303
+ end
304
+
305
+ You can point to a specific file to load, or to a Toys directory, whose
306
+ contents will be loaded similarly to how a `.toys` directory is loaded.
307
+
308
+ The CLI also provides high-level lookup methods that search for files named
309
+ `.toys.rb` or directories named `.toys`. (These names can also be configured
310
+ by passing appropriate options to the CLI constructor.) These methods,
311
+ {Toys::CLI#add_search_path} and {Toys::CLI#add_search_path_hierarchy},
312
+ implement the actual behavior of Toys in which it looks for any available files
313
+ in the current directory or its parents.
314
+
232
315
  ### Tool priority
233
316
 
234
- ### Defining mixins and templates
317
+ It is possible to configure a CLI with multiple files, directories, and/or
318
+ blocks with tool definitions. Indeed, this is how the `toys` gem itself is
319
+ configured: loading tools from the current directory and its ancestry, from
320
+ global directories, and from builtins. When a CLI is configured to load tools
321
+ from multiple sources, it combines them. However, if multiple sources define a
322
+ tool of the same name, only one definition will "win", the one from the source
323
+ with the highest priority.
324
+
325
+ Each time a tool source is added to a CLI using {Toys::CLI#add_config_block},
326
+ {Toys::CLI#add_config_path}, or similar, that new source is added to a
327
+ prioritized list. By default it is added to the end of the list, at a lower
328
+ priority level than previously added sources. Thus, any tools defined in the
329
+ new source would be overridden by tools of the same name defined in previously
330
+ added sources.
331
+
332
+ #!/usr/bin/env ruby
333
+
334
+ require "toys-core"
335
+
336
+ cli = Toys::CLI.new
337
+
338
+ # Add a block defining a tool called "hello"
339
+ cli.add_config_block do
340
+ tool "hello" do
341
+ def run
342
+ puts "Hello from the first config block!"
343
+ end
344
+ end
345
+ end
346
+
347
+ # Add a lower-priority block defining a tool with the same name
348
+ cli.add_config_block do
349
+ tool "hello" do
350
+ def run
351
+ puts "Hello from the second config block!"
352
+ end
353
+ end
354
+ end
355
+
356
+ # Runs the tool defined in the first block
357
+ result = cli.run("hello")
358
+ exit(result)
359
+
360
+ When defining tool blocks or loading tools from files, you can also add the new
361
+ source at the *front* of the priority list by passing an argument:
362
+
363
+ # Add tools with the highest priority
364
+ cli.add_config_block high_priority: true do
365
+ tool "hello" do
366
+ def run
367
+ puts "Hello from the second config block!"
368
+ end
369
+ end
370
+ end
235
371
 
236
- ## Customizing tool output
372
+ Priorities are used by the `toys` gem when loading tools from different
373
+ directories. Any `.toys.rb` file or `.toys` directory is added to the CLI at
374
+ the front of the list, with the highest priority. Parent directories are added
375
+ at subsequently lower priorities, and common directories such as the home
376
+ directory are loaded at the lowest priority.
237
377
 
238
- ### Logging and verbosity
378
+ ### Changing built-in mixins and templates
379
+
380
+ (TODO)
381
+
382
+ ## Customizing diagnostic output
383
+
384
+ Toys provides diagnostic logging and error reporting that can be customized by
385
+ the CLI. This section explains how to control logging output and levels, and
386
+ how to customize signal handling and exception reporting.
387
+
388
+ Toys-Core provides a class called {Toys::Utils::StandardUI} that implements the
389
+ diagnostic output format used by the `toys` gem. We'll look at how to use the
390
+ StandardUI after discussing each type of diagnostic output.
391
+
392
+ ### Logging
393
+
394
+ Toys provides a Logger for each tool execution. Tools can access this Logger by
395
+ calling the `logger` method, or by getting the `Toys::Context::Key::LOGGER`
396
+ context object.
397
+
398
+ #!/usr/bin/env ruby
399
+
400
+ require "toys-core"
401
+
402
+ cli = Toys::CLI.new
403
+
404
+ cli.add_config_block do
405
+ tool "hello" do
406
+ def run
407
+ logger.info "This log entry is displayed in verbose mode."
408
+ end
409
+ end
410
+ end
411
+
412
+ result = cli.run(*ARGV)
413
+ exit(result)
414
+
415
+ #### Log level and verbosity
416
+
417
+ The logging level is controlled by the *verbosity* setting when the tool is
418
+ invoked. This built-in attribute starts at 0, and by convention can be
419
+ increased or decreased by the user by passing the `--verbose` or `--quiet`
420
+ flags. (These flags are not provided by the CLI itself, but are implemented by
421
+ *middleware*, which we will cover later.) Its final setting is then mapped to a
422
+ Logger level threshold.
423
+
424
+ By default, a verbosity of 0 maps to log level `Logger::WARN`. Entries logged
425
+ at level `Logger::WARN` or higher are displayed, whereas entries logged at
426
+ `Logger::INFO` or `Logger::DEBUG` are suppressed. If the user increases the
427
+ verbosity by passing `--verbose` or `-v`, a verbosity of 1 will move the log
428
+ level threshold down to `Logger::INFO`.
429
+
430
+ You can modify the *starting* verbosity value by passing it to {Toys::CLI#run}.
431
+ Passing `verbosity: 1` will set the starting verbosity to 1, meaning
432
+ `Logger::INFO` entries will display but `Logger::DEBUG` entries will not. If
433
+ the invoker then provides an extra `--verbose` flag, the verbosity will further
434
+ increase to 2, allowing `Logger::DEBUG` entries to appear.
435
+
436
+ # ...
437
+ result = cli.run(*ARGV, verbosity: 1)
438
+ exit(result)
439
+
440
+ You can also modify the log level that verbosity 0 maps to by passing the
441
+ `base_level` argument to the CLI constructor. The following causes verbosity 0
442
+ to map to `Logger::INFO` rather than `Logger::WARN`.
443
+
444
+ cli = Toys::CLI.new(base_level: Logger::INFO)
445
+
446
+ #### Customizing the logger
447
+
448
+ Toys-Core configures its default logger with the default logging formatter, and
449
+ configures it to log to STDERR. If you want to change any of these settings,
450
+ you can provide your own logger by passing a `logger` to the CLI constructor
451
+ constructor.
452
+
453
+ my_logger = Logger.new("my_logfile.log")
454
+ cli = Toys::CLI.new(logger: my_logger)
455
+
456
+ A logger passed directly to the CLI is *global*. The CLI will attempt to use it
457
+ for every execution, even if multiple executions are happening concurrently. In
458
+ the concurrent case, this might cause problems if those executions attempt to
459
+ use different verbosity settings, as the log level thresholds will conflict. If
460
+ your CLI might be run multiple times concurrently, we recommend instead passing
461
+ a `logger_factory` to the CLI constructor. This is a Proc that will be invoked
462
+ to create a new logger for each execution.
463
+
464
+ my_logger_factory = Proc.new do
465
+ Logger.new("my_logfile.log")
466
+ end
467
+ cli = Toys::CLI.new(logger_factory: my_logger_factory)
468
+
469
+ #### StandardUI logging
470
+
471
+ {Toys::Utils::StandardUI} implements the logger used by the `toys` gem, which
472
+ formats log entries with the severity and timestamp using ANSI coloring.
473
+
474
+ You can use this logger by passing {Toys::Utils::StandardUI#logger_factory} to
475
+ the CLI constructor:
476
+
477
+ standard_ui = Toys::Utils::StandardUI.new
478
+ cli = Toys::CLI.new(logger_factory: standard_ui.logger_factory)
479
+
480
+ You can also customize the logger by subclassing StandardUI and overriding its
481
+ methods or adjusting its parameters. In particular, you can alter the
482
+ {Toys::Utils::StandardUI#log_header_severity_styles} mapping to adjust styling,
483
+ or override {Toys::Utils::StandardUI#logger_factory_impl} or
484
+ {Toys::Utils::StandardUI#logger_formatter_impl} to adjust content and
485
+ formatting.
239
486
 
240
487
  ### Handling errors
241
488
 
489
+ If an unhandled exception (specifically an exception represented by a subclass
490
+ of `StandardError` or `ScriptError`) occurs, or a signal such as an interrupt
491
+ (represented by a `SignalException`) is received, during tool execution,
492
+ Toys-Core first wraps the exception in a {Toys::ContextualError}. This error
493
+ type provides various context fields such as an estimate of where in the tool
494
+ source the error may have occurred. It also provides the original exception in
495
+ the `cause` field.
496
+
497
+ Then, Toys-Core invokes the error handler, a Proc that you can set as a
498
+ configuration argument when constructing a CLI. An error handler takes the
499
+ {Toys::ContextualError} wrapper as an argument and should perform any desired
500
+ final handling of an unhandled exception, such as displaying the error to the
501
+ terminal, or reraising the exception. The handler should then return the
502
+ desired result code for the execution.
503
+
504
+ my_error_handler = Proc.new |wrapped_error| do
505
+ # Propagate signals out and let the Ruby VM handle them.
506
+ raise wrapped_error.cause if wrapped_error.cause.is_a?(SignalException)
507
+ # Handle any other exception types by printing a message.
508
+ $stderr.puts "An error occurred. Please contact your administrator."
509
+ # Return the result code
510
+ 255
511
+ end
512
+ cli = Toys::CLI.new(error_handler: my_error_handler)
513
+
514
+ If you do not set an error handler, the exception is raised out of the
515
+ {Toys::CLI#run} call. In the case of signals, the *cause*, represented by a
516
+ `SignalException`, is raised directly so that the Ruby VM can handle it
517
+ normally. For other exceptions, however, the {Toys::ContextualError} wrapper
518
+ will be raised so that a rescue block has access to the context information.
519
+
520
+ #### StandardUI error handling
521
+
522
+ {Toys::Utils::StandardUI} provides the error handler used by the `toys` gem.
523
+ For normal exceptions, this standard handler displays the exception to STDERR,
524
+ along with some contextual information such as the tool name and arguments and
525
+ the location in the tool source where the error occurred, and returns an
526
+ appropriate result code, typically 1. For signals, this standard handler
527
+ displays a brief message noting the signal or interrupt, and returns the
528
+ conventional result code of `128 + signo` (e.g. 130 for interrupts).
529
+
530
+ You can use this error handler by passing
531
+ {Toys::Utils::StandardUI#error_handler} to the CLI constructor:
532
+
533
+ standard_ui = Toys::Utils::StandardUI.new
534
+ cli = Toys::CLI.new(error_handler: standard_ui.error_handler)
535
+
536
+ You can also customize the error handler by subclassing StandardUI and
537
+ overriding its methods. In particular, you can alter what is displayed in
538
+ response to errors or signals by overriding
539
+ {Toys::Utils::StandardUI#display_error_notice} or
540
+ {Toys::Utils::StandardUI#display_signal_notice}, respectively, and you can
541
+ alter how exit codes are generated by overriding
542
+ {Toys::Utils::StandardUI#exit_code_for}.
543
+
544
+ #### Nonstandard exceptions
545
+
546
+ Toys-Core error handling handles normal exceptions that are subclasses of
547
+ `StandardError`, errors coming from Ruby file loading and parsing that are
548
+ subclasses of `ScriptError`, and signals that are subclasses of
549
+ `SignalException`.
550
+
551
+ Other exceptions such as `NoMemoryError` or `SystemStackError` are not handled
552
+ by Toys, and are raised directly out of the {Toys::CLI#run}.
553
+
242
554
  ## Customizing default behavior
243
555
 
556
+ Command line tools often have a set of common behaviors, such as online help,
557
+ flags that control verbosity, and handlers for option parsing errors and corner
558
+ cases. In Toys-Core, a few of these common behaviors are built into the CLI
559
+ class as described above, but others are implemented and configured using
560
+ **middleware**.
561
+
562
+ Toys Middleware is analogous to middleware in other frameworks. It is code that
563
+ "wraps" tools defined in a Toys CLI and makes modifications. Middleware can,
564
+ for example, modify the tool's properties such as its description or settings,
565
+ modify the arguments accepted by the tool, and/or modify the execution of the
566
+ tool, by injecting code before and/or after the tool's execution, or even
567
+ replacing the execution altogether.
568
+
244
569
  ### Introducing middleware
245
570
 
571
+ A middleware object must duck-type {Toys::Middleware}, although it does not
572
+ necessarily need to include the module itself. {Toys::Middleware} defines two
573
+ methods, {Toys::Middleware#config} and {Toys::Middleware#run}. The first is
574
+ is called after a tool is defined, and lets the middleware modify the tool's
575
+ definition, e.g. to modify or provide defaults for properties such as
576
+ description and common flags. The second is called when a tool is executed, and
577
+ lets the middleware modify the tool's execution.
578
+
579
+ Middleware is arranged in a stack, where each middleware object "wraps" the
580
+ objects below it. Each middleware object's methods can implement its own
581
+ functionality, and then either pass control to the next middleware in the
582
+ stack, or stop processing and disable the rest of the stack. In particular, if
583
+ a middleware stops processing during the {Toys::Middleware#run} call, the
584
+ normal tool execution is also canceled; hence, middleware can even be used to
585
+ replace normal tool execution.
586
+
587
+ ### Configuring middleware
588
+
589
+ Middleware is normally configured as part of the CLI object. Each CLI includes
590
+ an ordered list, a _stack_, of middleware specifications, each represented by
591
+ {Toys::Middleware::Spec}. A middleware spec can be a specific middleware
592
+ object, a class to instantiate, or a name that can be looked up from a
593
+ directory of middleware class files. You can pass an array of these specs to a
594
+ CLI object when you instantiate it.
595
+
596
+ A useful example can be seen in the default Toys CLI behavior. If you do not
597
+ provide a middleware stack when instantiating {Toys::CLI}, the class uses a
598
+ default stack that looks approximately like this:
599
+
600
+ [
601
+ Toys::Middleware.spec(:set_default_descriptions),
602
+ Toys::Middleware.spec(:show_help, help_flags: true, fallback_execution: true),
603
+ Toys::Middleware.spec(:handle_usage_errors),
604
+ Toys::Middleware.spec(:add_verbosity_flags),
605
+ ]
606
+
607
+ Each of the names, e.g. `:set_default_descriptions`, is the name of a Ruby
608
+ file in the `toys-core` gem under `toys/standard_middleware`. You can configure
609
+ the middleware system to recognize middleware by name, by providing a
610
+ middleware lookup object, of type {Toys::ModuleLookup}. This object is
611
+ configured with one or more directories, and if you provide a name, it looks
612
+ for an appropriate module of that name in a ruby file in those directories. By
613
+ default, the middleware lookup in {Toys::CLI} looks for middleware in the
614
+ `toys/standard_middleware` directory in the `toys-core` gem, but you can
615
+ configure it to look elsewhere.
616
+
617
+ Note also that, in the case of `:show_help`, the stack above also includes some
618
+ options that are passed to the {Toys::StandardMiddleware::ShowHelp} middleware
619
+ constructor when it is instantiated.
620
+
621
+ You can also look at the middleware stack in the `Toys::StandardCLI` class in
622
+ the `toys` gem to see the middleware as the `toys` executable configures it.
623
+
246
624
  ### Built-in middlewares
247
625
 
626
+ The `toys-core` gem provides several useful middleware classes that you can use
627
+ when configuring your own CLI. These live in the `toys/standard_middlware`
628
+ directory, and are available by name if you keep the default middleware lookup.
629
+ These built-in middlewares include:
630
+
631
+ * {Toys::StandardMiddleware::AddVerbosityFlags} which adds the `--verbose`
632
+ and `--quiet` flags that control verbosity.
633
+ * {Toys::StandardMiddleware::ApplyConfig} which is instantiated with a block,
634
+ and includes that block when configuring all tools.
635
+ * {Toys::StandardMiddleware::HandleUsageErrors} which provides a standard
636
+ behavior for handling usage errors. That is, it catches
637
+ {Toys::ArgParsingError} and outputs the error along with usage info.
638
+ * {Toys::StandardMiddleware::SetDefaultDescriptions} which provides defaults
639
+ for tool description and long description fields. It can handle various
640
+ kinds of tools, including normal tools, namespaces, the root tool, and
641
+ delegates.
642
+ * {Toys::StandardMiddleware::ShowHelp} which adds help flags (e.g. `--help`)
643
+ to tools, and responds by showing the help page.
644
+ * {Toys::StandardMiddleware::ShowRootVersion} which displays a version string
645
+ when the root tool is invoked with `--version`.
646
+
248
647
  ### Writing your own middleware
249
648
 
649
+ Writing your own middleware is as simple as writing a class that implements the
650
+ {Toys::Middleware#config} and/or {Toys::Middleware#run} methods. The middleware
651
+ class need not include the {Toys::Middleware} module; it merely needs to
652
+ duck-type at least one of its methods. Your class can then be used in the stack
653
+ of middleware specifications.
654
+
655
+ #### Example: TimingMiddleware
656
+
657
+ An example would probably do best to illustrate how to write middleware. The
658
+ following is a simple middleware that adds the `--show-timing` flag to every
659
+ tool. When the flag is set, the middleware displays how long the tool took to
660
+ execute.
661
+
662
+ class TimingMiddleware
663
+ # This is a context key that will be used to store the "--show-timing"
664
+ # flag state. We can use `Object.new` to ensure that the key is unique
665
+ # across other middlewares and tool definitions.
666
+ KEY = Object.new
667
+
668
+ # This method intercepts tool configuration. We use it to add a flag that
669
+ # enables timing display.
670
+ def config(tool, _loader)
671
+ # Add a flag to control this functionality. Suppress collisions, i.e.
672
+ # just silently do nothing if the tool has already added a flag called
673
+ # "--show-timing".
674
+ tool.add_flag(KEY, "--show-timing", report_collisions: false)
675
+
676
+ # Calling yield passes control to the rest of the middleware stack.
677
+ # Normally you should call yield, to ensure that the remaining
678
+ # middleware can run. If you omit this, no additional middleware will
679
+ # be able to run tool configuration. Note you can also perform
680
+ # additional processing after the yield call, i.e. after the rest of
681
+ # the middleware stack has run.
682
+ yield
683
+ end
684
+
685
+ # This method intercepts tool execution. We use it to collect timing
686
+ # information, and display it if the flag has been provided in the
687
+ # command line arguments.
688
+ def run(context)
689
+ # Read monotonic time at the start of execution.
690
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
691
+
692
+ # Call yield to run the rest of the middleware stack, including the
693
+ # actual tool execution. If you omit this, you will prevent the rest of
694
+ # the middleware stack, AND the actual tool execution, from running.
695
+ # So you could omit the yield call if your goal is to replace tool
696
+ # execution with your own code.
697
+ yield
698
+
699
+ # Read monotonic time again after execution.
700
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
701
+
702
+ # Display the elapsed time, if the tool was passed the "--show-timing"
703
+ # flag.
704
+ puts "Tool took #{end_time - start_time} secs" if context[KEY]
705
+ end
706
+ end
707
+
708
+ We can now insert our middleware into the stack when we create a CLI. Here
709
+ we'll take that "default" stack we saw earlier and add our timing middleware at
710
+ the top of the stack. We put it here so that its execution "wraps" all the
711
+ other middleware, and thus its timing measurement includes the latency incurred
712
+ by other middleware (including middleware that replaces execution such as
713
+ `:show_help`).
714
+
715
+ my_middleware_stack = [
716
+ Toys::Middleware.spec(TimingMiddleware),
717
+ Toys::Middleware.spec(:set_default_descriptions),
718
+ Toys::Middleware.spec(:show_help, help_flags: true, fallback_execution: true),
719
+ Toys::Middleware.spec(:handle_usage_errors),
720
+ Toys::Middleware.spec(:add_verbosity_flags),
721
+ ]
722
+ cli = Toys::CLI.new(middleware_stack: my_middleware_stack)
723
+
724
+ Now, every tool run by this CLI wil have the `--show-timing` flag and
725
+ associated functionality.
726
+
727
+ #### Changing built-in middleware
728
+
729
+ (TODO)
730
+
250
731
  ## Shell and command line integration
251
732
 
733
+ (TODO)
734
+
252
735
  ### Interpreting tool names
253
736
 
737
+ (TODO)
738
+
254
739
  ### Tab completion
255
740
 
741
+ (TODO)
742
+
256
743
  ## Packaging your executable
257
744
 
258
- ## Overview of Toys-Core classes
745
+ (TODO)
746
+
747
+ ## Extending Toys
748
+
749
+ (TODO)
750
+
751
+ ## Overview of Toys-Core classes
752
+
753
+ (TODO)