toys-core 0.7.0 → 0.8.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 +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"
|