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.
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 switches and arguments in your tool. Context values may also be set
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 Command line arguments defining another tool
240
- # to run, along with parameters and switches.
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
@@ -32,5 +32,5 @@ module Toys
32
32
  # Current version of Toys core
33
33
  # @return [String]
34
34
  #
35
- CORE_VERSION = "0.3.3".freeze
35
+ CORE_VERSION = "0.3.4".freeze
36
36
  end
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
@@ -27,8 +27,6 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- gem "highline", "~> 1.7"
31
-
32
30
  require "highline"
33
31
 
34
32
  module Toys
@@ -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,Array<String>>] frames An array of frames. Each
60
- # frame should be either a string, or a two-element array of string
61
- # and integer, where the integer is the visible length of the frame
62
- # on screen. The latter form should be used if the frame string
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 { |f| f.is_a?(::Array) ? f : [f, f.size] }
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 LoaderError, "Illegal index file name #{index_file_name.inspect}"
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 LoaderError, "Illegal preload file name #{preload_file_name.inspect}"
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 returns the nearest group that
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
- return tool if tool
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
- # Execute the tool given by the given arguments, in the given context.
145
+ # Returns true if the given path has at least one subtool. Loads from the
146
+ # configuration if necessary.
137
147
  #
138
- # @param [Toys::Context::Base] context_base The context in which to
139
- # execute.
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
- # @private
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 execute(context_base, args, verbosity: 0)
147
- tool = lookup(args)
148
- tool.execute(context_base, args.slice(tool.full_name.length..-1), verbosity: verbosity)
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
@@ -40,11 +40,11 @@ module Toys
40
40
  #
41
41
  # Currently recognized middleware names are:
42
42
  #
43
- # * `:add_verbosity_switches` : Adds switches for affecting verbosity.
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
- # * `:show_usage` : Teaches tools to print their usage documentation.
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 switches specification. Used often in middleware.
93
+ # Resolves a typical flags specification. Used often in middleware.
94
94
  #
95
- # You may provide any of the following for the `switches` parameter:
96
- # * A string, which becomes the single switch
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 switches
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 switch strings, even if empty.
102
+ # Always returns an array of flag strings, even if empty.
103
103
  #
104
- # @param [Boolean,String,Array<String>,Proc] switches Switch spec
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 switches
107
+ # @return [Array<String>] An array of flags
108
108
  #
109
- def resolve_switches_spec(switches, tool, defaults)
110
- switches = switches.call(tool) if switches.respond_to?(:call)
111
- case switches
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
- [switches]
115
+ [flags]
116
116
  when ::Array
117
- switches
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