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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/LICENSE.md +1 -1
- data/README.md +10 -5
- data/docs/guide.md +534 -39
- data/lib/toys/cli.rb +76 -172
- data/lib/toys/compat.rb +15 -30
- data/lib/toys/context.rb +51 -0
- data/lib/toys/core.rb +1 -1
- data/lib/toys/dsl/flag.rb +40 -2
- data/lib/toys/dsl/flag_group.rb +15 -5
- data/lib/toys/dsl/internal.rb +23 -8
- data/lib/toys/dsl/positional_arg.rb +32 -1
- data/lib/toys/dsl/tool.rb +174 -68
- data/lib/toys/errors.rb +2 -2
- data/lib/toys/middleware.rb +3 -2
- data/lib/toys/settings.rb +1 -1
- data/lib/toys/standard_mixins/bundler.rb +16 -1
- data/lib/toys/standard_mixins/exec.rb +29 -8
- data/lib/toys/standard_mixins/gems.rb +17 -3
- data/lib/toys/standard_mixins/highline.rb +12 -12
- data/lib/toys/standard_mixins/terminal.rb +7 -7
- data/lib/toys/tool_definition.rb +153 -50
- data/lib/toys/utils/exec.rb +22 -1
- data/lib/toys/utils/gems.rb +3 -0
- data/lib/toys/utils/standard_ui.rb +261 -0
- data/lib/toys-core.rb +51 -3
- metadata +6 -5
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
|
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.
|
10
|
-
used to create custom command line executables
|
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
|
34
|
-
|
35
|
-
|
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
|
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
|
-
|
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
|
-
* **
|
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
|
117
|
-
tool is requested by name, the loader is responsible for
|
118
|
-
definition in those sources, and constructing the tool
|
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
|
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
|
-
|
125
|
-
that file, if it exists, only when the `foo bar` tool is requested. If
|
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
|
-
|
185
|
-
the tool
|
186
|
-
|
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
|
-
[
|
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
|
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
|
-
|
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
|
-
|
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
|
-
###
|
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
|
-
|
745
|
+
(TODO)
|
746
|
+
|
747
|
+
## Extending Toys
|
748
|
+
|
749
|
+
(TODO)
|
750
|
+
|
751
|
+
## Overview of Toys-Core classes
|
752
|
+
|
753
|
+
(TODO)
|