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.
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