toys-core 0.14.7 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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)