toys-core 0.3.3 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|