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.
- 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)
|