toys-core 0.3.3 → 0.3.4
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 +23 -1
- data/lib/toys-core.rb +2 -0
- data/lib/toys/cli.rb +108 -15
- data/lib/toys/config_dsl.rb +101 -55
- data/lib/toys/context.rb +3 -3
- data/lib/toys/core_version.rb +1 -1
- data/lib/toys/errors.rb +76 -0
- data/lib/toys/helpers/highline.rb +0 -2
- data/lib/toys/helpers/spinner.rb +17 -11
- data/lib/toys/loader.rb +43 -18
- data/lib/toys/middleware.rb +14 -14
- data/lib/toys/middleware/add_verbosity_flags.rb +113 -0
- data/lib/toys/middleware/base.rb +1 -1
- data/lib/toys/middleware/handle_usage_errors.rb +15 -9
- data/lib/toys/middleware/set_default_descriptions.rb +114 -37
- data/lib/toys/middleware/show_help.rb +245 -0
- data/lib/toys/middleware/show_version.rb +20 -16
- data/lib/toys/templates/clean.rb +4 -1
- data/lib/toys/templates/gem_build.rb +3 -1
- data/lib/toys/templates/minitest.rb +5 -6
- data/lib/toys/tool.rb +418 -213
- data/lib/toys/utils/help_text.rb +487 -0
- data/lib/toys/utils/line_output.rb +105 -0
- data/lib/toys/utils/wrappable_string.rb +67 -19
- metadata +6 -5
- data/lib/toys/middleware/add_verbosity_switches.rb +0 -99
- data/lib/toys/middleware/show_usage.rb +0 -174
- data/lib/toys/utils/usage.rb +0 -250
data/lib/toys/context.rb
CHANGED
@@ -37,7 +37,7 @@ module Toys
|
|
37
37
|
# Keys that begin with two underscores are reserved common elements of the
|
38
38
|
# context such as the tool being executed, or the verbosity level.
|
39
39
|
# Other keys are available for use by your tool. Generally, they are set
|
40
|
-
# by
|
40
|
+
# by flags and arguments in your tool. Context values may also be set
|
41
41
|
# by middleware. By convention, middleware-set keys begin with a single
|
42
42
|
# underscore.
|
43
43
|
#
|
@@ -236,8 +236,8 @@ module Toys
|
|
236
236
|
##
|
237
237
|
# Execute another tool, given by the provided arguments.
|
238
238
|
#
|
239
|
-
# @param [String...] args
|
240
|
-
#
|
239
|
+
# @param [String...] args The name of the tool to run along with its
|
240
|
+
# command line arguments and flags.
|
241
241
|
# @param [Toys::CLI,nil] cli The CLI to use to execute the tool. If `nil`
|
242
242
|
# (the default), uses the current CLI.
|
243
243
|
# @param [Boolean] exit_on_nonzero_status If true, exit immediately if the
|
data/lib/toys/core_version.rb
CHANGED
data/lib/toys/errors.rb
CHANGED
@@ -39,4 +39,80 @@ module Toys
|
|
39
39
|
#
|
40
40
|
class LoaderError < ::StandardError
|
41
41
|
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# A wrapper exception used to provide user-oriented context for an exception
|
45
|
+
#
|
46
|
+
class ContextualError < ::StandardError
|
47
|
+
## @private
|
48
|
+
def initialize(cause, banner,
|
49
|
+
config_path: nil, config_line: nil,
|
50
|
+
tool_name: nil, tool_args: nil)
|
51
|
+
super("#{banner} : #{cause.message} (#{cause.class})")
|
52
|
+
@cause = cause
|
53
|
+
@banner = banner
|
54
|
+
@config_path = config_path
|
55
|
+
@config_line = config_line
|
56
|
+
@tool_name = tool_name
|
57
|
+
@tool_args = tool_args
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_reader :cause
|
61
|
+
attr_reader :banner
|
62
|
+
|
63
|
+
attr_accessor :config_path
|
64
|
+
attr_accessor :config_line
|
65
|
+
attr_accessor :tool_name
|
66
|
+
attr_accessor :tool_args
|
67
|
+
|
68
|
+
class << self
|
69
|
+
## @private
|
70
|
+
def capture_path(banner, path, opts = {})
|
71
|
+
yield
|
72
|
+
rescue ContextualError => e
|
73
|
+
add_fields_if_missing(e, opts)
|
74
|
+
add_config_path_if_missing(e, path)
|
75
|
+
raise e
|
76
|
+
rescue ::SyntaxError => e
|
77
|
+
if e.message =~ /#{::Regexp.escape(path)}:(\d+)/
|
78
|
+
opts = opts.merge(config_path: path, config_line: $1.to_i)
|
79
|
+
e = ContextualError.new(e, banner, opts)
|
80
|
+
end
|
81
|
+
raise e
|
82
|
+
rescue ::StandardError => e
|
83
|
+
e = ContextualError.new(e, banner)
|
84
|
+
add_fields_if_missing(e, opts)
|
85
|
+
add_config_path_if_missing(e, path)
|
86
|
+
raise e
|
87
|
+
end
|
88
|
+
|
89
|
+
## @private
|
90
|
+
def capture(banner, opts = {})
|
91
|
+
yield
|
92
|
+
rescue ContextualError => e
|
93
|
+
add_fields_if_missing(e, opts)
|
94
|
+
raise e
|
95
|
+
rescue ::StandardError => e
|
96
|
+
raise ContextualError.new(e, banner, opts)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def add_fields_if_missing(error, opts)
|
102
|
+
opts.each do |k, v|
|
103
|
+
error.send(:"#{k}=", v) if error.send(k).nil?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_config_path_if_missing(error, path)
|
108
|
+
if error.config_path.nil? && error.config_line.nil?
|
109
|
+
l = error.cause.backtrace_locations.find { |b| b.absolute_path == path }
|
110
|
+
if l
|
111
|
+
error.config_path = path
|
112
|
+
error.config_line = l.lineno
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
42
118
|
end
|
data/lib/toys/helpers/spinner.rb
CHANGED
@@ -28,6 +28,7 @@
|
|
28
28
|
;
|
29
29
|
|
30
30
|
require "monitor"
|
31
|
+
require "highline"
|
31
32
|
|
32
33
|
module Toys
|
33
34
|
module Helpers
|
@@ -53,23 +54,23 @@ module Toys
|
|
53
54
|
# spinner will be displayed.
|
54
55
|
#
|
55
56
|
# @param [String] leading_text Optional leading string to display to the
|
56
|
-
# left of the spinner.
|
57
|
+
# left of the spinner. Default is the empty string.
|
57
58
|
# @param [Float] frame_length Length of a single frame, in seconds.
|
58
59
|
# Defaults to {DEFAULT_FRAME_LENGTH}.
|
59
|
-
# @param [Array<String
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
# contains non-printing characters such as ANSI escape codes.
|
64
|
-
# Defaults to {DEFAULT_FRAMES}.
|
60
|
+
# @param [Array<String>] frames An array of frames. Defaults to
|
61
|
+
# {DEFAULT_FRAMES}.
|
62
|
+
# @param [Symbol,Array<Symbol>] style A HighLine style or array of styles
|
63
|
+
# to apply to all frames in the spinner. Defaults to empty,
|
65
64
|
# @param [IO] stream Stream to output the spinner to. Defaults to STDOUT.
|
66
65
|
# Note the spinner will be disabled if this stream is not a tty.
|
67
66
|
# @param [String] final_text Optional final string to display when the
|
68
|
-
# spinner is complete.
|
67
|
+
# spinner is complete. Default is the empty string. A common practice
|
68
|
+
# is to set this to newline.
|
69
69
|
#
|
70
70
|
def spinner(leading_text: "",
|
71
71
|
frame_length: DEFAULT_FRAME_LENGTH,
|
72
72
|
frames: DEFAULT_FRAMES,
|
73
|
+
style: nil,
|
73
74
|
stream: $stdout,
|
74
75
|
final_text: "")
|
75
76
|
return nil unless block_given?
|
@@ -77,7 +78,7 @@ module Toys
|
|
77
78
|
stream.write(leading_text)
|
78
79
|
stream.flush
|
79
80
|
end
|
80
|
-
spin = SpinDriver.new(stream, frames, frame_length)
|
81
|
+
spin = SpinDriver.new(stream, frames, Array(style), frame_length)
|
81
82
|
begin
|
82
83
|
yield
|
83
84
|
ensure
|
@@ -93,9 +94,14 @@ module Toys
|
|
93
94
|
class SpinDriver
|
94
95
|
include ::MonitorMixin
|
95
96
|
|
96
|
-
def initialize(stream, frames, frame_length)
|
97
|
+
def initialize(stream, frames, style, frame_length)
|
97
98
|
@stream = stream
|
98
|
-
@frames = frames.map
|
99
|
+
@frames = frames.map do |f|
|
100
|
+
[
|
101
|
+
style.empty? ? f : ::HighLine.color(f, *style),
|
102
|
+
::HighLine.uncolor(f).size
|
103
|
+
]
|
104
|
+
end
|
99
105
|
@frame_length = frame_length
|
100
106
|
@cur_frame = 0
|
101
107
|
@stopping = false
|
data/lib/toys/loader.rb
CHANGED
@@ -51,10 +51,10 @@ module Toys
|
|
51
51
|
#
|
52
52
|
def initialize(index_file_name: nil, preload_file_name: nil, middleware_stack: [])
|
53
53
|
if index_file_name && ::File.extname(index_file_name) != ".rb"
|
54
|
-
raise
|
54
|
+
raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
|
55
55
|
end
|
56
56
|
if preload_file_name && ::File.extname(preload_file_name) != ".rb"
|
57
|
-
raise
|
57
|
+
raise ::ArgumentError, "Illegal preload file name #{preload_file_name.inspect}"
|
58
58
|
end
|
59
59
|
@index_file_name = index_file_name
|
60
60
|
@preload_file_name = preload_file_name
|
@@ -83,13 +83,17 @@ module Toys
|
|
83
83
|
|
84
84
|
##
|
85
85
|
# Given a list of command line arguments, find the appropriate tool to
|
86
|
-
# handle the command, loading it from the configuration if necessary
|
86
|
+
# handle the command, loading it from the configuration if necessary, and
|
87
|
+
# following aliases.
|
87
88
|
# This always returns a tool. If the specific tool path is not defined and
|
88
|
-
# cannot be found in any configuration, it
|
89
|
+
# cannot be found in any configuration, it finds the nearest group that
|
89
90
|
# _would_ contain that tool, up to the root tool.
|
90
91
|
#
|
92
|
+
# Returns a tuple of the found tool, and the array of remaining arguments
|
93
|
+
# that are not part of the tool name and should be passed as tool args.
|
94
|
+
#
|
91
95
|
# @param [String] args Command line arguments
|
92
|
-
# @return [Toys::Tool]
|
96
|
+
# @return [Array(Toys::Tool,Array<String>)]
|
93
97
|
#
|
94
98
|
def lookup(args)
|
95
99
|
orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
|
@@ -99,12 +103,17 @@ module Toys
|
|
99
103
|
p = orig_prefix.dup
|
100
104
|
while p.length >= cur_prefix.length
|
101
105
|
tool = get_tool(p, [])
|
102
|
-
|
106
|
+
if tool
|
107
|
+
finish_definitions_in_tree(tool.full_name)
|
108
|
+
return [tool, args.slice(p.length..-1)]
|
109
|
+
end
|
103
110
|
p.pop
|
104
111
|
end
|
105
112
|
break unless cur_prefix.pop
|
106
113
|
end
|
107
|
-
get_or_create_tool([])
|
114
|
+
tool = get_or_create_tool([])
|
115
|
+
finish_definitions_in_tree([])
|
116
|
+
[tool, args]
|
108
117
|
end
|
109
118
|
|
110
119
|
##
|
@@ -133,19 +142,35 @@ module Toys
|
|
133
142
|
end
|
134
143
|
|
135
144
|
##
|
136
|
-
#
|
145
|
+
# Returns true if the given path has at least one subtool. Loads from the
|
146
|
+
# configuration if necessary.
|
137
147
|
#
|
138
|
-
# @param [
|
139
|
-
#
|
140
|
-
# @param [String] args Command line arguments
|
141
|
-
# @param [Integer] verbosity Starting verbosity. Defaults to 0.
|
142
|
-
# @return [Integer] The exit code
|
148
|
+
# @param [Array<String>] words The name of the parent tool
|
149
|
+
# @return [Boolean]
|
143
150
|
#
|
144
|
-
|
151
|
+
def has_subtools?(words)
|
152
|
+
load_for_prefix(words)
|
153
|
+
len = words.length
|
154
|
+
@tools.each do |n, _tp|
|
155
|
+
return true if !n.empty? && n.length > len && n.slice(0, len) == words
|
156
|
+
end
|
157
|
+
false
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Finishes all tool definitions under the given path. This generally means
|
162
|
+
# installing middleware.
|
163
|
+
#
|
164
|
+
# @param [Array<String>] words The path to the tool under which all
|
165
|
+
# definitions should be finished.
|
145
166
|
#
|
146
|
-
def
|
147
|
-
|
148
|
-
|
167
|
+
def finish_definitions_in_tree(words)
|
168
|
+
load_for_prefix(words)
|
169
|
+
len = words.length
|
170
|
+
@tools.each do |n, tp|
|
171
|
+
next if n.length < len || n.slice(0, len) != words
|
172
|
+
tp.first.finish_definition(self) unless tp.first.is_a?(Alias)
|
173
|
+
end
|
149
174
|
end
|
150
175
|
|
151
176
|
##
|
@@ -351,7 +376,7 @@ module Toys
|
|
351
376
|
raise LoaderError, "Cannot read directory #{path}"
|
352
377
|
end
|
353
378
|
else
|
354
|
-
raise ArgumentError, "Illegal type #{type}"
|
379
|
+
raise ::ArgumentError, "Illegal type #{type}"
|
355
380
|
end
|
356
381
|
path
|
357
382
|
end
|
data/lib/toys/middleware.rb
CHANGED
@@ -40,11 +40,11 @@ module Toys
|
|
40
40
|
#
|
41
41
|
# Currently recognized middleware names are:
|
42
42
|
#
|
43
|
-
# * `:
|
43
|
+
# * `:add_verbosity_flags` : Adds flags for affecting verbosity.
|
44
44
|
# * `:handle_usage_errors` : Displays the usage error if one occurs.
|
45
45
|
# * `:set_default_descriptions` : Sets default descriptions for tools
|
46
46
|
# that do not have them set explicitly.
|
47
|
-
# * `:
|
47
|
+
# * `:show_help` : Teaches tools to print their usage documentation.
|
48
48
|
# * `:show_version` : Teaches tools to print their version.
|
49
49
|
#
|
50
50
|
# @param [String,Symbol] name Name of the middleware class to return
|
@@ -90,31 +90,31 @@ module Toys
|
|
90
90
|
end
|
91
91
|
|
92
92
|
##
|
93
|
-
# Resolves a typical
|
93
|
+
# Resolves a typical flags specification. Used often in middleware.
|
94
94
|
#
|
95
|
-
# You may provide any of the following for the `
|
96
|
-
# * A string, which becomes the single
|
95
|
+
# You may provide any of the following for the `flags` parameter:
|
96
|
+
# * A string, which becomes the single flag
|
97
97
|
# * An array of strings
|
98
|
-
# * The value `false` or `nil` which resolves to no
|
98
|
+
# * The value `false` or `nil` which resolves to no flags
|
99
99
|
# * The value `true` or `:default` which resolves to the given defaults
|
100
100
|
# * A proc that takes a tool as argument and returns any of the above.
|
101
101
|
#
|
102
|
-
# Always returns an array of
|
102
|
+
# Always returns an array of flag strings, even if empty.
|
103
103
|
#
|
104
|
-
# @param [Boolean,String,Array<String>,Proc]
|
104
|
+
# @param [Boolean,String,Array<String>,Proc] flags Flag spec
|
105
105
|
# @param [Toys::Tool] tool The tool
|
106
106
|
# @param [Array<String>] defaults The defaults to use for `true`.
|
107
|
-
# @return [Array<String>] An array of
|
107
|
+
# @return [Array<String>] An array of flags
|
108
108
|
#
|
109
|
-
def
|
110
|
-
|
111
|
-
case
|
109
|
+
def resolve_flags_spec(flags, tool, defaults)
|
110
|
+
flags = flags.call(tool) if flags.respond_to?(:call)
|
111
|
+
case flags
|
112
112
|
when true, :default
|
113
113
|
Array(defaults)
|
114
114
|
when ::String
|
115
|
-
[
|
115
|
+
[flags]
|
116
116
|
when ::Array
|
117
|
-
|
117
|
+
flags
|
118
118
|
else
|
119
119
|
[]
|
120
120
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# Copyright 2018 Daniel Azuma
|
2
|
+
#
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of the copyright holder, nor the names of any other
|
14
|
+
# contributors to this software, may be used to endorse or promote products
|
15
|
+
# derived from this software without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
;
|
29
|
+
|
30
|
+
require "toys/middleware/base"
|
31
|
+
|
32
|
+
module Toys
|
33
|
+
module Middleware
|
34
|
+
##
|
35
|
+
# A middleware that provides flags for editing the verbosity.
|
36
|
+
#
|
37
|
+
# This middleware adds `-v`, `--verbose`, `-q`, and `--quiet` flags, if
|
38
|
+
# not already defined by the tool. These flags affect the setting of
|
39
|
+
# {Toys::Context::VERBOSITY}, and, thus, the logger level.
|
40
|
+
#
|
41
|
+
class AddVerbosityFlags < Base
|
42
|
+
##
|
43
|
+
# Default verbose flags
|
44
|
+
# @return [Array<String>]
|
45
|
+
#
|
46
|
+
DEFAULT_VERBOSE_FLAGS = ["-v", "--verbose"].freeze
|
47
|
+
|
48
|
+
##
|
49
|
+
# Default quiet flags
|
50
|
+
# @return [Array<String>]
|
51
|
+
#
|
52
|
+
DEFAULT_QUIET_FLAGS = ["-q", "--quiet"].freeze
|
53
|
+
|
54
|
+
##
|
55
|
+
# Create a AddVerbosityFlags middleware.
|
56
|
+
#
|
57
|
+
# @param [Boolean,Array<String>,Proc] verbose_flags Specify flags
|
58
|
+
# to increase verbosity. The value may be any of the following:
|
59
|
+
# * An array of flags that increase verbosity.
|
60
|
+
# * The `true` value to use {DEFAULT_VERBOSE_FLAGS}. (Default)
|
61
|
+
# * The `false` value to disable verbose flags.
|
62
|
+
# * A proc that takes a tool and returns any of the above.
|
63
|
+
# @param [Boolean,Array<String>,Proc] quiet_flags Specify flags
|
64
|
+
# to decrease verbosity. The value may be any of the following:
|
65
|
+
# * An array of flags that decrease verbosity.
|
66
|
+
# * The `true` value to use {DEFAULT_QUIET_FLAGS}. (Default)
|
67
|
+
# * The `false` value to disable quiet flags.
|
68
|
+
# * A proc that takes a tool and returns any of the above.
|
69
|
+
#
|
70
|
+
def initialize(verbose_flags: true, quiet_flags: true)
|
71
|
+
@verbose_flags = verbose_flags
|
72
|
+
@quiet_flags = quiet_flags
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Configure the tool flags.
|
77
|
+
#
|
78
|
+
def config(tool, _loader)
|
79
|
+
add_verbose_flags(tool)
|
80
|
+
add_quiet_flags(tool)
|
81
|
+
yield
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def add_verbose_flags(tool)
|
87
|
+
verbose_flags = Middleware.resolve_flags_spec(@verbose_flags, tool,
|
88
|
+
DEFAULT_VERBOSE_FLAGS)
|
89
|
+
unless verbose_flags.empty?
|
90
|
+
long_desc = "Increase verbosity, causing additional logging levels to display."
|
91
|
+
tool.add_flag(Context::VERBOSITY, *verbose_flags,
|
92
|
+
desc: "Increase verbosity",
|
93
|
+
long_desc: long_desc,
|
94
|
+
handler: ->(_val, cur) { cur + 1 },
|
95
|
+
only_unique: true)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_quiet_flags(tool)
|
100
|
+
quiet_flags = Middleware.resolve_flags_spec(@quiet_flags, tool,
|
101
|
+
DEFAULT_QUIET_FLAGS)
|
102
|
+
unless quiet_flags.empty?
|
103
|
+
long_desc = "Decrease verbosity, causing fewer logging levels to display."
|
104
|
+
tool.add_flag(Context::VERBOSITY, *quiet_flags,
|
105
|
+
desc: "Decrease verbosity",
|
106
|
+
long_desc: long_desc,
|
107
|
+
handler: ->(_val, cur) { cur - 1 },
|
108
|
+
only_unique: true)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|