toys-core 0.7.0 → 0.8.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 +98 -0
- data/LICENSE.md +16 -24
- data/README.md +307 -59
- data/docs/guide.md +44 -4
- data/lib/toys-core.rb +58 -49
- data/lib/toys/acceptor.rb +672 -0
- data/lib/toys/alias.rb +106 -0
- data/lib/toys/arg_parser.rb +624 -0
- data/lib/toys/cli.rb +422 -181
- data/lib/toys/compat.rb +83 -0
- data/lib/toys/completion.rb +442 -0
- data/lib/toys/context.rb +354 -0
- data/lib/toys/core_version.rb +18 -26
- data/lib/toys/dsl/flag.rb +213 -56
- data/lib/toys/dsl/flag_group.rb +237 -51
- data/lib/toys/dsl/positional_arg.rb +210 -0
- data/lib/toys/dsl/tool.rb +968 -317
- data/lib/toys/errors.rb +46 -28
- data/lib/toys/flag.rb +821 -0
- data/lib/toys/flag_group.rb +282 -0
- data/lib/toys/input_file.rb +18 -26
- data/lib/toys/loader.rb +110 -100
- data/lib/toys/middleware.rb +24 -31
- data/lib/toys/mixin.rb +90 -59
- data/lib/toys/module_lookup.rb +125 -0
- data/lib/toys/positional_arg.rb +184 -0
- data/lib/toys/source_info.rb +192 -0
- data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
- data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
- data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
- data/lib/toys/standard_middleware/show_help.rb +130 -113
- data/lib/toys/standard_middleware/show_root_version.rb +29 -35
- data/lib/toys/standard_mixins/exec.rb +116 -78
- data/lib/toys/standard_mixins/fileutils.rb +16 -24
- data/lib/toys/standard_mixins/gems.rb +29 -30
- data/lib/toys/standard_mixins/highline.rb +34 -41
- data/lib/toys/standard_mixins/terminal.rb +72 -26
- data/lib/toys/template.rb +51 -35
- data/lib/toys/tool.rb +1161 -206
- data/lib/toys/utils/completion_engine.rb +171 -0
- data/lib/toys/utils/exec.rb +279 -182
- data/lib/toys/utils/gems.rb +58 -49
- data/lib/toys/utils/help_text.rb +117 -111
- data/lib/toys/utils/terminal.rb +69 -62
- data/lib/toys/wrappable_string.rb +162 -0
- metadata +24 -22
- data/lib/toys/definition/acceptor.rb +0 -191
- data/lib/toys/definition/alias.rb +0 -112
- data/lib/toys/definition/arg.rb +0 -140
- data/lib/toys/definition/flag.rb +0 -370
- data/lib/toys/definition/flag_group.rb +0 -205
- data/lib/toys/definition/source_info.rb +0 -190
- data/lib/toys/definition/tool.rb +0 -842
- data/lib/toys/dsl/arg.rb +0 -132
- data/lib/toys/runner.rb +0 -188
- data/lib/toys/standard_middleware.rb +0 -47
- data/lib/toys/utils/module_lookup.rb +0 -135
- data/lib/toys/utils/wrappable_string.rb +0 -165
data/lib/toys/cli.rb
CHANGED
@@ -1,167 +1,300 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
6
11
|
#
|
7
|
-
#
|
8
|
-
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
9
14
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# derived from this software without specific prior written permission.
|
18
|
-
#
|
19
|
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
-
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
23
|
-
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
24
|
-
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
25
|
-
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
26
|
-
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
27
|
-
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
28
|
-
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
29
|
-
# POSSIBILITY OF SUCH DAMAGE.
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
21
|
+
# IN THE SOFTWARE.
|
30
22
|
;
|
31
23
|
|
32
24
|
require "logger"
|
25
|
+
require "toys/completion"
|
33
26
|
|
34
27
|
module Toys
|
35
28
|
##
|
36
29
|
# A Toys-based CLI.
|
37
30
|
#
|
38
|
-
#
|
31
|
+
# This is the entry point for command line execution. It includes the set of
|
32
|
+
# tool definitions (and/or information on how to load them from the file
|
33
|
+
# system), configuration parameters such as logging and error handling, and a
|
34
|
+
# method to call to invoke a command.
|
35
|
+
#
|
36
|
+
# This is the class to instantiate to create a Toys-based command line
|
37
|
+
# executable. For example:
|
38
|
+
#
|
39
|
+
# #!/usr/bin/env ruby
|
40
|
+
# require "toys-core"
|
41
|
+
# cli = Toys::CLI.new
|
42
|
+
# cli.add_config_block do
|
43
|
+
# def run
|
44
|
+
# puts "Hello, world!"
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# exit(cli.run(*ARGV))
|
48
|
+
#
|
49
|
+
# The currently running CLI is also available at runtime, and can be used by
|
50
|
+
# tools that want to invoke other tools. For example:
|
51
|
+
#
|
52
|
+
# # My .toys.rb
|
53
|
+
# tool "foo" do
|
54
|
+
# def run
|
55
|
+
# puts "in foo"
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
# tool "bar" do
|
59
|
+
# def run
|
60
|
+
# puts "in bar"
|
61
|
+
# cli.run "foo"
|
62
|
+
# end
|
63
|
+
# end
|
39
64
|
#
|
40
65
|
class CLI
|
41
66
|
##
|
42
67
|
# Create a CLI.
|
43
68
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
# @param [
|
78
|
-
# function as delimiters in a tool name. Defaults to empty. Allowed
|
79
|
-
# characters are period, colon, and slash.
|
80
|
-
# @param [Toys::Utils::ModuleLookup] mixin_lookup A lookup for well-known
|
81
|
-
# mixin modules. If not provided, uses
|
82
|
-
# {Toys::CLI.default_mixin_lookup}.
|
83
|
-
# @param [Toys::Utils::ModuleLookup] middleware_lookup A lookup for
|
84
|
-
# well-known middleware classes. If not provided, uses
|
85
|
-
# {Toys::CLI.default_middleware_lookup}.
|
86
|
-
# @param [Toys::Utils::ModuleLookup] template_lookup A lookup for
|
87
|
-
# well-known template classes. If not provided, uses
|
88
|
-
# {Toys::CLI.default_template_lookup}.
|
89
|
-
# @param [Logger,nil] logger The logger to use. If not provided, a default
|
90
|
-
# logger that writes to `STDERR` is used.
|
91
|
-
# @param [Integer,nil] base_level The logger level that should correspond
|
92
|
-
# to zero verbosity. If not provided, will default to the current level
|
93
|
-
# of the logger.
|
94
|
-
# @param [Proc,nil] error_handler A proc that is called when an error is
|
69
|
+
# Most configuration parameters (besides tool definitions and tool lookup
|
70
|
+
# paths) are set as options passed to the constructor. These options fall
|
71
|
+
# roughly into four categories:
|
72
|
+
#
|
73
|
+
# * Options affecting output behavior:
|
74
|
+
# * `logger`: The logger
|
75
|
+
# * `base_level`: The default log level
|
76
|
+
# * `error_handler`: Callback for handling exceptions
|
77
|
+
# * `executable_name`: The name of the executable
|
78
|
+
# * Options affecting tool specification
|
79
|
+
# * `extra_delimibers`: Tool name delimiters besides space
|
80
|
+
# * `completion`: Tab completion handler
|
81
|
+
# * Options affecting tool definition
|
82
|
+
# * `middleware_stack`: The middleware applied to all tools
|
83
|
+
# * `mixin_lookup`: Where to find well-known mixins
|
84
|
+
# * `middleware_lookup`: Where to find well-known middleware
|
85
|
+
# * `template_lookup`: Where to find well-known templates
|
86
|
+
# * Options affecting tool files and directories
|
87
|
+
# * `config_dir_name`: Directory name containing tool files
|
88
|
+
# * `config_file_name`: File name for tools
|
89
|
+
# * `index_file_name`: Name of index files in tool directories
|
90
|
+
# * `preload_file_name`: Name of preload files in tool directories
|
91
|
+
# * `preload_dir_name`: Name of preload directories in tool directories
|
92
|
+
# * `data_dir_name`: Name of data directories in tool directories
|
93
|
+
#
|
94
|
+
# @param logger [Logger] The logger to use.
|
95
|
+
# Optional. If not provided, will use a default logger that writes
|
96
|
+
# formatted output to `STDERR`, as defined by
|
97
|
+
# {Toys::CLI.default_logger}.
|
98
|
+
# @param base_level [Integer] The logger level that should correspond
|
99
|
+
# to zero verbosity.
|
100
|
+
# Optional. If not provided, defaults to the current level of the
|
101
|
+
# logger (which is often `Logger::WARN`).
|
102
|
+
# @param error_handler [Proc,nil] A proc that is called when an error is
|
95
103
|
# caught. The proc should take a {Toys::ContextualError} argument and
|
96
104
|
# report the error. It should return an exit code (normally nonzero).
|
97
|
-
#
|
105
|
+
# Optional. If not provided, defaults to an instance of
|
106
|
+
# {Toys::CLI::DefaultErrorHandler}, which displays an error message to
|
107
|
+
# `STDERR`.
|
108
|
+
# @param executable_name [String] The executable name displayed in help
|
109
|
+
# text. Optional. Defaults to the ruby program name.
|
110
|
+
#
|
111
|
+
# @param extra_delimiters [String] A string containing characters that can
|
112
|
+
# function as delimiters in a tool name. Defaults to empty. Allowed
|
113
|
+
# characters are period, colon, and slash.
|
114
|
+
# @param completion [Toys::Completion::Base] A specifier for shell tab
|
115
|
+
# completion for the CLI as a whole.
|
116
|
+
# Optional. If not provided, defaults to an instance of
|
117
|
+
# {Toys::CLI::DefaultCompletion}, which delegates completion to the
|
118
|
+
# relevant tool.
|
119
|
+
#
|
120
|
+
# @param middleware_stack [Array] An array of middleware that will be used
|
121
|
+
# by default for all tools loaded by this CLI.
|
122
|
+
# Optional. If not provided, uses a default set of middleware defined
|
123
|
+
# in {Toys::CLI.default_middleware_stack}. To include no middleware,
|
124
|
+
# pass the empty array explicitly.
|
125
|
+
# @param mixin_lookup [Toys::ModuleLookup] A lookup for well-known mixin
|
126
|
+
# modules (i.e. with symbol names).
|
127
|
+
# Optional. If not provided, defaults to the set of standard mixins
|
128
|
+
# provided by toys-core, as defined by
|
129
|
+
# {Toys::CLI.default_mixin_lookup}. If you explicitly want no standard
|
130
|
+
# mixins, pass an empty instance of {Toys::ModuleLookup}.
|
131
|
+
# @param middleware_lookup [Toys::ModuleLookup] A lookup for well-known
|
132
|
+
# middleware classes.
|
133
|
+
# Optional. If not provided, defaults to the set of standard middleware
|
134
|
+
# classes provided by toys-core, as defined by
|
135
|
+
# {Toys::CLI.default_middleware_lookup}. If you explicitly want no
|
136
|
+
# standard middleware, pass an empty instance of
|
137
|
+
# {Toys::ModuleLookup}.
|
138
|
+
# @param template_lookup [Toys::ModuleLookup] A lookup for well-known
|
139
|
+
# template classes.
|
140
|
+
# Optional. If not provided, defaults to the set of standard template
|
141
|
+
# classes provided by toys core, as defined by
|
142
|
+
# {Toys::CLI.default_template_lookup}. If you explicitly want no
|
143
|
+
# standard tenokates, pass an empty instance of {Toys::ModuleLookup}.
|
144
|
+
#
|
145
|
+
# @param config_dir_name [String] A directory with this name that appears
|
146
|
+
# in the loader path, is treated as a configuration directory whose
|
147
|
+
# contents are loaded into the toys configuration.
|
148
|
+
# Optional. If not provided, toplevel configuration directories are
|
149
|
+
# disabled.
|
150
|
+
# Note: the standard toys executable sets this to `".toys"`.
|
151
|
+
# @param config_file_name [String] A file with this name that appears in
|
152
|
+
# the loader path, is treated as a toplevel configuration file whose
|
153
|
+
# contents are loaded into the toys configuration. This does not
|
154
|
+
# include "index" configuration files located within a configuration
|
155
|
+
# directory.
|
156
|
+
# Optional. If not provided, toplevel configuration files are disabled.
|
157
|
+
# Note: the standard toys executable sets this to `".toys.rb"`.
|
158
|
+
# @param index_file_name [String] A file with this name that appears in any
|
159
|
+
# configuration directory is loaded first as a standalone configuration
|
160
|
+
# file. This does not include "toplevel" configuration files outside
|
161
|
+
# configuration directories.
|
162
|
+
# Optional. If not provided, index configuration files are disabled.
|
163
|
+
# Note: the standard toys executable sets this to `".toys.rb"`.
|
164
|
+
# @param preload_file_name [String] A file with this name that appears
|
165
|
+
# in any configuration directory is preloaded using `require` before
|
166
|
+
# any tools in that configuration directory are defined. A preload file
|
167
|
+
# includes normal Ruby code, rather than Toys DSL definitions. The
|
168
|
+
# preload file is loaded before any files in a preload directory.
|
169
|
+
# Optional. If not provided, preload files are disabled.
|
170
|
+
# Note: the standard toys executable sets this to `".preload.rb"`.
|
171
|
+
# @param preload_dir_name [String] A directory with this name that appears
|
172
|
+
# in any configuration directory is searched for Ruby files, which are
|
173
|
+
# preloaded using `require` before any tools in that configuration
|
174
|
+
# directory are defined. Files in a preload directory include normal
|
175
|
+
# Ruby code, rather than Toys DSL definitions. Files in a preload
|
176
|
+
# directory are loaded after any standalone preload file.
|
177
|
+
# Optional. If not provided, preload directories are disabled.
|
178
|
+
# Note: the standard toys executable sets this to `".preload"`.
|
179
|
+
# @param data_dir_name [String] A directory with this name that appears in
|
180
|
+
# any configuration directory is added to the data directory search
|
181
|
+
# path for any tool file in that directory.
|
182
|
+
# Optional. If not provided, data directories are disabled.
|
183
|
+
# Note: the standard toys executable sets this to `".data"`.
|
98
184
|
#
|
99
185
|
def initialize(
|
100
|
-
|
186
|
+
executable_name: nil, middleware_stack: nil, extra_delimiters: "",
|
101
187
|
config_dir_name: nil, config_file_name: nil, index_file_name: nil,
|
102
|
-
preload_file_name: nil,
|
188
|
+
preload_file_name: nil, preload_dir_name: nil, data_dir_name: nil,
|
103
189
|
mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil,
|
104
|
-
logger: nil, base_level: nil, error_handler: nil
|
190
|
+
logger: nil, base_level: nil, error_handler: nil, completion: nil
|
105
191
|
)
|
106
|
-
@
|
192
|
+
@executable_name = executable_name || ::File.basename($PROGRAM_NAME)
|
193
|
+
@middleware_stack = middleware_stack || CLI.default_middleware_stack
|
194
|
+
@mixin_lookup = mixin_lookup || CLI.default_mixin_lookup
|
195
|
+
@middleware_lookup = middleware_lookup || CLI.default_middleware_lookup
|
196
|
+
@template_lookup = template_lookup || CLI.default_template_lookup
|
197
|
+
@error_handler = error_handler || DefaultErrorHandler.new
|
198
|
+
@completion = completion || DefaultCompletion.new
|
199
|
+
@logger = logger || CLI.default_logger
|
107
200
|
@base_level = base_level || @logger.level
|
108
|
-
@middleware_stack = middleware_stack || self.class.default_middleware_stack
|
109
201
|
@extra_delimiters = extra_delimiters
|
110
|
-
@binary_name = binary_name || ::File.basename($PROGRAM_NAME)
|
111
202
|
@config_dir_name = config_dir_name
|
112
203
|
@config_file_name = config_file_name
|
113
204
|
@index_file_name = index_file_name
|
114
205
|
@preload_file_name = preload_file_name
|
115
|
-
@
|
116
|
-
@
|
117
|
-
@mixin_lookup = mixin_lookup || self.class.default_mixin_lookup
|
118
|
-
@middleware_lookup = middleware_lookup || self.class.default_middleware_lookup
|
119
|
-
@template_lookup = template_lookup || self.class.default_template_lookup
|
206
|
+
@preload_dir_name = preload_dir_name
|
207
|
+
@data_dir_name = data_dir_name
|
120
208
|
@loader = Loader.new(
|
121
|
-
index_file_name: index_file_name, extra_delimiters: @extra_delimiters,
|
122
|
-
|
123
|
-
|
209
|
+
index_file_name: @index_file_name, extra_delimiters: @extra_delimiters,
|
210
|
+
preload_dir_name: @preload_dir_name, preload_file_name: @preload_file_name,
|
211
|
+
data_dir_name: @data_dir_name,
|
124
212
|
mixin_lookup: @mixin_lookup, template_lookup: @template_lookup,
|
125
213
|
middleware_lookup: @middleware_lookup, middleware_stack: @middleware_stack
|
126
214
|
)
|
127
|
-
@error_handler = error_handler || DefaultErrorHandler.new
|
128
215
|
end
|
129
216
|
|
130
217
|
##
|
131
|
-
#
|
218
|
+
# Make a clone with the same settings but no paths in the loader.
|
219
|
+
# This is sometimes useful for running sub-tools that have to be loaded
|
220
|
+
# from a different configuration.
|
221
|
+
#
|
222
|
+
# @param _opts [Hash] Unused options that can be used by subclasses.
|
223
|
+
# @return [Toys::CLI]
|
224
|
+
# @yieldparam cli [Toys::CLI] If you pass a block, the new CLI is yielded
|
225
|
+
# to it so you can add paths and make other modifications.
|
226
|
+
#
|
227
|
+
def child(_opts = {})
|
228
|
+
cli = CLI.new(executable_name: @executable_name,
|
229
|
+
config_dir_name: @config_dir_name,
|
230
|
+
config_file_name: @config_file_name,
|
231
|
+
index_file_name: @index_file_name,
|
232
|
+
preload_dir_name: @preload_dir_name,
|
233
|
+
preload_file_name: @preload_file_name,
|
234
|
+
data_dir_name: @data_dir_name,
|
235
|
+
middleware_stack: @middleware_stack,
|
236
|
+
extra_delimiters: @extra_delimiters,
|
237
|
+
mixin_lookup: @mixin_lookup,
|
238
|
+
middleware_lookup: @middleware_lookup,
|
239
|
+
template_lookup: @template_lookup,
|
240
|
+
logger: @logger,
|
241
|
+
base_level: @base_level,
|
242
|
+
error_handler: @error_handler,
|
243
|
+
completion: @completion)
|
244
|
+
yield cli if block_given?
|
245
|
+
cli
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# The current loader for this CLI.
|
132
250
|
# @return [Toys::Loader]
|
133
251
|
#
|
134
252
|
attr_reader :loader
|
135
253
|
|
136
254
|
##
|
137
|
-
#
|
255
|
+
# The effective executable name used for usage text in this CLI.
|
256
|
+
# @return [String]
|
257
|
+
#
|
258
|
+
attr_reader :executable_name
|
259
|
+
|
260
|
+
##
|
261
|
+
# The string of tool name delimiter characters (besides space).
|
138
262
|
# @return [String]
|
139
263
|
#
|
140
|
-
attr_reader :
|
264
|
+
attr_reader :extra_delimiters
|
141
265
|
|
142
266
|
##
|
143
|
-
#
|
267
|
+
# The logger used by this CLI.
|
144
268
|
# @return [Logger]
|
145
269
|
#
|
146
270
|
attr_reader :logger
|
147
271
|
|
148
272
|
##
|
149
|
-
#
|
150
|
-
# verbosity 0.
|
273
|
+
# The initial logger level in this CLI, used as the level for verbosity 0.
|
151
274
|
# @return [Integer]
|
152
275
|
#
|
153
276
|
attr_reader :base_level
|
154
277
|
|
155
278
|
##
|
156
|
-
#
|
279
|
+
# The overall completion strategy for this CLI.
|
280
|
+
# @return [Toys::Completion::Base,Proc]
|
281
|
+
#
|
282
|
+
attr_reader :completion
|
283
|
+
|
284
|
+
##
|
285
|
+
# Add a specific configuration file or directory to the loader.
|
157
286
|
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
#
|
287
|
+
# This is generally used to load a static or "built-in" set of tools,
|
288
|
+
# either for a standalone command line executable based on Toys, or to
|
289
|
+
# provide a "default" set of tools for a dynamic executable. For example,
|
290
|
+
# the main Toys executable uses this to load the builtin tools from its
|
291
|
+
# "builtins" directory.
|
161
292
|
#
|
162
|
-
# @param [String]
|
163
|
-
#
|
293
|
+
# @param path [String] A path to add. May reference a single Toys file or
|
294
|
+
# a Toys directory.
|
295
|
+
# @param high_priority [Boolean] Add the config at the head of the priority
|
164
296
|
# list rather than the tail.
|
297
|
+
# @return [self]
|
165
298
|
#
|
166
299
|
def add_config_path(path, high_priority: false)
|
167
300
|
@loader.add_path(path, high_priority: high_priority)
|
@@ -171,11 +304,17 @@ module Toys
|
|
171
304
|
##
|
172
305
|
# Add a configuration block to the loader.
|
173
306
|
#
|
174
|
-
#
|
307
|
+
# This is used to create tools "inline", and is useful for simple command
|
308
|
+
# line executables based on Toys.
|
309
|
+
#
|
310
|
+
# @param high_priority [Boolean] Add the config at the head of the priority
|
175
311
|
# list rather than the tail.
|
176
|
-
# @param [String]
|
312
|
+
# @param name [String] The source name that will be shown in documentation
|
177
313
|
# for tools defined in this block. If omitted, a default unique string
|
178
314
|
# will be generated.
|
315
|
+
# @param block [Proc] The block of configuration, executed in the context
|
316
|
+
# of the tool DSL {Toys::DSL::Tool}.
|
317
|
+
# @return [self]
|
179
318
|
#
|
180
319
|
def add_config_block(high_priority: false, name: nil, &block)
|
181
320
|
@loader.add_block(high_priority: high_priority, name: name, &block)
|
@@ -183,16 +322,16 @@ module Toys
|
|
183
322
|
end
|
184
323
|
|
185
324
|
##
|
186
|
-
#
|
187
|
-
# config
|
325
|
+
# Checks the given directory path. If it contains a config file and/or
|
326
|
+
# config directory, those are added to the loader.
|
188
327
|
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
# general configuration-oriented directory such as "/etc".
|
328
|
+
# The main Toys executable uses this method to load tools from directories
|
329
|
+
# in the `TOYS_PATH`.
|
192
330
|
#
|
193
|
-
# @param [String]
|
194
|
-
# @param [Boolean]
|
331
|
+
# @param search_path [String] A path to search for configs.
|
332
|
+
# @param high_priority [Boolean] Add the configs at the head of the
|
195
333
|
# priority list rather than the tail.
|
334
|
+
# @return [self]
|
196
335
|
#
|
197
336
|
def add_search_path(search_path, high_priority: false)
|
198
337
|
paths = []
|
@@ -209,15 +348,21 @@ module Toys
|
|
209
348
|
end
|
210
349
|
|
211
350
|
##
|
212
|
-
#
|
213
|
-
#
|
351
|
+
# Walk up the directory hierarchy from the given start location, and add to
|
352
|
+
# the loader any config files and directories found.
|
214
353
|
#
|
215
|
-
#
|
354
|
+
# The main Toys executable uses this method to load tools from the current
|
355
|
+
# directory and its ancestors.
|
356
|
+
#
|
357
|
+
# @param start [String] The first directory to add. Defaults to the current
|
216
358
|
# working directory.
|
217
|
-
# @param [Array<String>]
|
218
|
-
# terminate the search.
|
219
|
-
#
|
359
|
+
# @param terminate [Array<String>] Optional list of directories that should
|
360
|
+
# terminate the search. If the walk up the directory tree encounters
|
361
|
+
# one of these directories, the search is halted without checking the
|
362
|
+
# terminating directory.
|
363
|
+
# @param high_priority [Boolean] Add the configs at the head of the
|
220
364
|
# priority list rather than the tail.
|
365
|
+
# @return [self]
|
221
366
|
#
|
222
367
|
def add_search_path_hierarchy(start: nil, terminate: [], high_priority: false)
|
223
368
|
path = start || ::Dir.pwd
|
@@ -238,55 +383,108 @@ module Toys
|
|
238
383
|
|
239
384
|
##
|
240
385
|
# Run the CLI with the given command line arguments.
|
386
|
+
# Handles exceptions using the error handler.
|
241
387
|
#
|
242
|
-
# @param [String...]
|
388
|
+
# @param args [String...] Command line arguments specifying which tool to
|
243
389
|
# run and what arguments to pass to it. You may pass either a single
|
244
390
|
# array of strings, or a series of string arguments.
|
245
|
-
# @param [Integer]
|
391
|
+
# @param verbosity [Integer] Initial verbosity. Default is 0.
|
246
392
|
#
|
247
|
-
# @return [Integer] The resulting status code
|
393
|
+
# @return [Integer] The resulting process status code (i.e. 0 for success).
|
248
394
|
#
|
249
395
|
def run(*args, verbosity: 0)
|
250
|
-
|
396
|
+
tool, remaining = ContextualError.capture("Error finding tool definition") do
|
251
397
|
@loader.lookup(args.flatten)
|
252
398
|
end
|
253
399
|
ContextualError.capture_path(
|
254
|
-
"Error during tool execution!",
|
255
|
-
tool_name:
|
400
|
+
"Error during tool execution!", tool.source_info&.source_path,
|
401
|
+
tool_name: tool.full_name, tool_args: remaining
|
256
402
|
) do
|
257
|
-
|
403
|
+
run_tool(tool, remaining, verbosity: verbosity)
|
258
404
|
end
|
259
|
-
rescue ContextualError => e
|
260
|
-
@error_handler.call(e)
|
405
|
+
rescue ContextualError, ::Interrupt => e
|
406
|
+
@error_handler.call(e).to_i
|
261
407
|
end
|
262
408
|
|
409
|
+
private
|
410
|
+
|
263
411
|
##
|
264
|
-
#
|
265
|
-
#
|
412
|
+
# Run the given tool with the given arguments.
|
413
|
+
# Does not handle exceptions.
|
266
414
|
#
|
267
|
-
# @param [
|
268
|
-
# @
|
269
|
-
# @
|
270
|
-
#
|
415
|
+
# @param tool [Toys::Tool] The tool to run.
|
416
|
+
# @param args [Array<String>] Command line arguments passed to the tool.
|
417
|
+
# @param verbosity [Integer] Initial verbosity. Default is 0.
|
418
|
+
# @return [Integer] The resulting status code
|
271
419
|
#
|
272
|
-
def
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
420
|
+
def run_tool(tool, args, verbosity: 0)
|
421
|
+
arg_parser = ArgParser.new(self, tool,
|
422
|
+
verbosity: verbosity,
|
423
|
+
require_exact_flag_match: tool.exact_flag_match_required?)
|
424
|
+
arg_parser.parse(args).finish
|
425
|
+
context = tool.tool_class.new(arg_parser.data)
|
426
|
+
tool.run_initializers(context)
|
427
|
+
|
428
|
+
cur_logger = logger
|
429
|
+
original_level = cur_logger.level
|
430
|
+
cur_logger.level = base_level - context[Context::Key::VERBOSITY]
|
431
|
+
begin
|
432
|
+
perform_execution(context, tool)
|
433
|
+
ensure
|
434
|
+
cur_logger.level = original_level
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def perform_execution(context, tool)
|
439
|
+
executor = proc do
|
440
|
+
begin
|
441
|
+
if !context[Context::Key::USAGE_ERRORS].empty?
|
442
|
+
handle_usage_errors(context, tool)
|
443
|
+
elsif !tool.runnable?
|
444
|
+
raise NotRunnableError, "No implementation for tool #{tool.display_name.inspect}"
|
445
|
+
else
|
446
|
+
context.run
|
447
|
+
end
|
448
|
+
rescue ::Interrupt => e
|
449
|
+
raise e unless tool.handles_interrupts?
|
450
|
+
handle_interrupt(context, tool.interrupt_handler, e)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
tool.middleware_stack.reverse_each do |middleware|
|
454
|
+
executor = make_executor(middleware, context, executor)
|
455
|
+
end
|
456
|
+
catch(:result) do
|
457
|
+
executor.call
|
458
|
+
0
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
def handle_usage_errors(context, tool)
|
463
|
+
usage_errors = context[Context::Key::USAGE_ERRORS]
|
464
|
+
handler = tool.usage_error_handler
|
465
|
+
raise ArgParsingError, usage_errors if handler.nil?
|
466
|
+
handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
|
467
|
+
if handler.arity.zero?
|
468
|
+
context.instance_exec(&handler)
|
469
|
+
else
|
470
|
+
context.instance_exec(usage_errors, &handler)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def handle_interrupt(context, handler, exception)
|
475
|
+
handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
|
476
|
+
if handler.arity.zero?
|
477
|
+
context.instance_exec(&handler)
|
478
|
+
else
|
479
|
+
context.instance_exec(exception, &handler)
|
480
|
+
end
|
481
|
+
rescue ::Interrupt => e
|
482
|
+
raise e if e.equal?(exception)
|
483
|
+
handle_interrupt(context, handler, e)
|
484
|
+
end
|
485
|
+
|
486
|
+
def make_executor(middleware, context, next_executor)
|
487
|
+
proc { middleware.run(context, &next_executor) }
|
290
488
|
end
|
291
489
|
|
292
490
|
##
|
@@ -297,25 +495,51 @@ module Toys
|
|
297
495
|
##
|
298
496
|
# Create an error handler.
|
299
497
|
#
|
300
|
-
# @param [IO]
|
498
|
+
# @param output [IO,nil] Where to write errors. Default is `$stderr`.
|
301
499
|
#
|
302
|
-
def initialize(output
|
500
|
+
def initialize(output: $stderr)
|
501
|
+
require "toys/utils/terminal"
|
303
502
|
@terminal = Utils::Terminal.new(output: output)
|
304
503
|
end
|
305
504
|
|
306
505
|
##
|
307
|
-
# The error handler routine. Prints out the error message and backtrace
|
506
|
+
# The error handler routine. Prints out the error message and backtrace,
|
507
|
+
# and returns the correct result code.
|
308
508
|
#
|
309
|
-
# @param [Exception]
|
509
|
+
# @param error [Exception] The error that occurred.
|
510
|
+
# @return [Integer] The result code for the execution.
|
310
511
|
#
|
311
512
|
def call(error)
|
312
|
-
|
313
|
-
|
314
|
-
|
513
|
+
cause = error
|
514
|
+
case error
|
515
|
+
when ContextualError
|
516
|
+
cause = error.cause
|
517
|
+
@terminal.puts(cause_string(cause))
|
518
|
+
@terminal.puts(context_string(error), :bold)
|
519
|
+
when ::Interrupt
|
520
|
+
@terminal.puts
|
521
|
+
@terminal.puts("INTERRUPTED", :bold)
|
522
|
+
else
|
523
|
+
@terminal.puts(cause_string(error))
|
524
|
+
end
|
525
|
+
exit_code_for(cause)
|
315
526
|
end
|
316
527
|
|
317
528
|
private
|
318
529
|
|
530
|
+
def exit_code_for(error)
|
531
|
+
case error
|
532
|
+
when ArgParsingError
|
533
|
+
2
|
534
|
+
when NotRunnableError
|
535
|
+
126
|
536
|
+
when ::Interrupt
|
537
|
+
130
|
538
|
+
else
|
539
|
+
1
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
319
543
|
def cause_string(cause)
|
320
544
|
lines = ["#{cause.class}: #{cause.message}"]
|
321
545
|
cause.backtrace.each_with_index.reverse_each do |bt, i|
|
@@ -327,7 +551,7 @@ module Toys
|
|
327
551
|
def context_string(error)
|
328
552
|
lines = [
|
329
553
|
error.banner || "Unexpected error!",
|
330
|
-
" #{error.cause.class}: #{error.cause.message}"
|
554
|
+
" #{error.cause.class}: #{error.cause.message}",
|
331
555
|
]
|
332
556
|
if error.config_path
|
333
557
|
lines << " in config file: #{error.config_path}:#{error.config_line}"
|
@@ -342,29 +566,44 @@ module Toys
|
|
342
566
|
end
|
343
567
|
end
|
344
568
|
|
569
|
+
##
|
570
|
+
# A Completion that implements the default algorithm for a CLI. This
|
571
|
+
# algorithm simply determines the tool and uses its completion.
|
572
|
+
#
|
573
|
+
class DefaultCompletion < Completion::Base
|
574
|
+
##
|
575
|
+
# Returns candidates for the current completion.
|
576
|
+
#
|
577
|
+
# @param context [Toys::Completion::Context] the current completion
|
578
|
+
# context including the string fragment.
|
579
|
+
# @return [Array<Toys::Completion::Candidate>] an array of candidates
|
580
|
+
#
|
581
|
+
def call(context)
|
582
|
+
context.tool.completion.call(context)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
345
586
|
class << self
|
346
587
|
##
|
347
588
|
# Returns a default set of middleware that may be used as a starting
|
348
589
|
# point for a typical CLI. This set includes the following in order:
|
349
590
|
#
|
350
591
|
# * {Toys::StandardMiddleware::SetDefaultDescriptions} providing
|
351
|
-
# defaults for description fields
|
352
|
-
# * {Toys::StandardMiddleware::ShowHelp} adding the `--help` flag
|
592
|
+
# defaults for description fields.
|
593
|
+
# * {Toys::StandardMiddleware::ShowHelp} adding the `--help` flag and
|
594
|
+
# providing default behavior for namespaces.
|
353
595
|
# * {Toys::StandardMiddleware::HandleUsageErrors}
|
354
|
-
# * {Toys::StandardMiddleware::ShowHelp} providing default behavior for
|
355
|
-
# namespaces
|
356
596
|
# * {Toys::StandardMiddleware::AddVerbosityFlags} adding the `--verbose`
|
357
|
-
# and `--quiet` flags for managing the logger level
|
597
|
+
# and `--quiet` flags for managing the logger level.
|
358
598
|
#
|
359
|
-
# @return [Array]
|
599
|
+
# @return [Array<Toys::Middleware>]
|
360
600
|
#
|
361
601
|
def default_middleware_stack
|
362
602
|
[
|
363
603
|
[:set_default_descriptions],
|
364
|
-
[:show_help, help_flags: true],
|
604
|
+
[:show_help, help_flags: true, fallback_execution: true],
|
365
605
|
[:handle_usage_errors],
|
366
|
-
[:
|
367
|
-
[:add_verbosity_flags]
|
606
|
+
[:add_verbosity_flags],
|
368
607
|
]
|
369
608
|
end
|
370
609
|
|
@@ -372,40 +611,42 @@ module Toys
|
|
372
611
|
# Returns a default ModuleLookup for mixins that points at the
|
373
612
|
# StandardMixins module.
|
374
613
|
#
|
375
|
-
# @return [Toys::
|
614
|
+
# @return [Toys::ModuleLookup]
|
376
615
|
#
|
377
616
|
def default_mixin_lookup
|
378
|
-
|
617
|
+
ModuleLookup.new.add_path("toys/standard_mixins")
|
379
618
|
end
|
380
619
|
|
381
620
|
##
|
382
621
|
# Returns a default ModuleLookup for middleware that points at the
|
383
622
|
# StandardMiddleware module.
|
384
623
|
#
|
385
|
-
# @return [Toys::
|
624
|
+
# @return [Toys::ModuleLookup]
|
386
625
|
#
|
387
626
|
def default_middleware_lookup
|
388
|
-
|
627
|
+
ModuleLookup.new.add_path("toys/standard_middleware")
|
389
628
|
end
|
390
629
|
|
391
630
|
##
|
392
631
|
# Returns a default empty ModuleLookup for templates.
|
393
632
|
#
|
394
|
-
# @return [Toys::
|
633
|
+
# @return [Toys::ModuleLookup]
|
395
634
|
#
|
396
635
|
def default_template_lookup
|
397
|
-
|
636
|
+
ModuleLookup.new
|
398
637
|
end
|
399
638
|
|
400
639
|
##
|
401
|
-
# Returns a default logger that logs to
|
640
|
+
# Returns a default logger that writes formatted logs to a given stream.
|
402
641
|
#
|
403
|
-
# @param [IO] stream
|
642
|
+
# @param output [IO] The stream to output to (defaults to `$stderr`)
|
404
643
|
# @return [Logger]
|
405
644
|
#
|
406
|
-
def default_logger(
|
407
|
-
|
408
|
-
|
645
|
+
def default_logger(output: nil)
|
646
|
+
require "toys/utils/terminal"
|
647
|
+
output ||= $stderr
|
648
|
+
logger = ::Logger.new(output)
|
649
|
+
terminal = Utils::Terminal.new(output: output)
|
409
650
|
logger.formatter = proc do |severity, time, _progname, msg|
|
410
651
|
msg_str =
|
411
652
|
case msg
|
@@ -426,7 +667,7 @@ module Toys
|
|
426
667
|
|
427
668
|
def format_log(terminal, time, severity, msg)
|
428
669
|
timestr = time.strftime("%Y-%m-%d %H:%M:%S")
|
429
|
-
header = format("[
|
670
|
+
header = format("[%<time>s %<sev>5s]", time: timestr, sev: severity)
|
430
671
|
styled_header =
|
431
672
|
case severity
|
432
673
|
when "FATAL"
|