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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ebb136d89b19617cfdaf8de9c870cce070857748abd208ab8faa4896b7e7b36
4
- data.tar.gz: eb079b488d1da544e5f31ab0fe0f9730d77d6878549c673bc6ba4711a5d6c295
3
+ metadata.gz: 26fce1d32fe0161a806a4b6d6d831b546be343290a0d0103c5e6a52e14f2f106
4
+ data.tar.gz: b57713c9fd6436f5f90007769663340a4df5599bf9c72f0c9eb160ebcdc7ed8a
5
5
  SHA512:
6
- metadata.gz: 864c7bc16399227e29e013372611ed70df77339185593d3830a31a5092e7dea6540d62ed83704e23ce5d78410ce2b4e34ae5ce9f709d67d8bb82e5075ed09cc3
7
- data.tar.gz: 5eb3e60cefb50d7a289ef2283a91574ede820bf1d2bb5769d8038d60d760e999f36e49882899c06f2de6dd37a23d828ee25bd1e2a0764e11cc0e31553764368c
6
+ metadata.gz: f2427161dc5f1635473fa09f5e67c6f3ad58f861669de6c909c03fe55a2b44778e2b905f44b0bbeb250019e583d721296eb1a3adccd62c2f76e4abed99bdd860
7
+ data.tar.gz: c9ab4052480bcbd91e3452eba2d2e425013eba043f4d825b4f5a88f92a248ca780368206a5c06b0bf2ea9a67dea93fda8163babef2c64e4147cb38c650486f6a
data/CHANGELOG.md CHANGED
@@ -1,9 +1,31 @@
1
1
  # Release History
2
2
 
3
+ ### 0.3.4 / 2018-05-14
4
+
5
+ * CHANGED: Renamed switch to flag
6
+ * CHANGED: Renamed Utils::Usage to Utils::HelpText
7
+ * CHANGED: Renamed show_usage middleware to show_help and default everything false.
8
+ * CHANGED: Renamed docs: parameter again, to desc: and long_desc: to match tool desc.
9
+ * CHANGED: Middleware config method takes a loader as the second arg
10
+ * CHANGED: desc is now a single string rather than an array.
11
+ * CHANGED: Removed Loader#execute, and returned remaining args from Loader#lookup.
12
+ * CHANGED: Wrapped most errors with Toys::ContextualError
13
+ * CHANGED: accept: parameter now controls whether a switch takes a value by default
14
+ * CHANGED: Explicit and implicit show-help now handled by separate middleware instances
15
+ * CHANGED: All description strings are now wrappable
16
+ * IMPROVED: gem_build template can suppress interactive confirmation.
17
+ * IMPROVED: Logger colors the header when possible.
18
+ * IMPROVED: HelpText class can now generate nicer help pages
19
+ * IMPROVED: Style support for spinner helper
20
+ * IMPROVED: Set default descriptions for flags and args
21
+ * ADDED: CLI now takes an error handler to report most errors.
22
+ * ADDED: Alias DSL methods `required`, `optional`, and `remaining`.
23
+ * FIXED: Finish definitions for subtools since the desc may depend on it
24
+
3
25
  ### 0.3.3 / 2018-05-09
4
26
 
5
27
  * CHANGED: Renamed file_utils helper to fileutils.
6
- * CHANGED: Renamed doc: parameter to docs:
28
+ * CHANGED: Renamed `doc:` parameter to `docs:`.
7
29
  * CHANGED: SwitchDefinition has separate fields for acceptor and docs.
8
30
  * CHANGED: Description and long description are now arrays of strings.
9
31
  * FIXED: Documentation strings that begin with "--" no longer cause problems.
data/lib/toys-core.rb CHANGED
@@ -27,6 +27,8 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
+ gem "highline", "~> 1.7"
31
+
30
32
  ##
31
33
  # Toys is a Ruby library and command line tool that lets you build your own
32
34
  # command line suite of tools (with commands and subcommands) using a Ruby DSL.
data/lib/toys/cli.rb CHANGED
@@ -28,6 +28,9 @@
28
28
  ;
29
29
 
30
30
  require "logger"
31
+ require "highline"
32
+
33
+ require "toys/utils/line_output"
31
34
 
32
35
  module Toys
33
36
  ##
@@ -69,6 +72,10 @@ module Toys
69
72
  # @param [Integer,nil] base_level The logger level that should correspond
70
73
  # to zero verbosity. If not provided, will default to the current level
71
74
  # of the logger.
75
+ # @param [Proc,nil] error_handler A proc that is called when an error is
76
+ # caught. The proc should take a {Toys::ContextualError} argument and
77
+ # report the error. It should return an exit code (normally nonzero).
78
+ # Default is a {Toys::CLI::DefaultErrorHandler} writing to the logger.
72
79
  #
73
80
  def initialize(
74
81
  binary_name: nil,
@@ -78,7 +85,8 @@ module Toys
78
85
  preload_file_name: nil,
79
86
  middleware_stack: nil,
80
87
  logger: nil,
81
- base_level: nil
88
+ base_level: nil,
89
+ error_handler: nil
82
90
  )
83
91
  @logger = logger || self.class.default_logger
84
92
  @base_level = base_level || @logger.level
@@ -93,6 +101,7 @@ module Toys
93
101
  preload_file_name: preload_file_name,
94
102
  middleware_stack: middleware_stack
95
103
  )
104
+ @error_handler = error_handler || DefaultErrorHandler.new
96
105
  end
97
106
 
98
107
  ##
@@ -200,7 +209,12 @@ module Toys
200
209
  # @return [Integer] The resulting status code
201
210
  #
202
211
  def run(*args, verbosity: 0)
203
- @loader.execute(self, args.flatten, verbosity: verbosity)
212
+ tool, remaining = ContextualError.capture("Error finding tool definition") do
213
+ @loader.lookup(args.flatten)
214
+ end
215
+ tool.execute(self, remaining, verbosity: verbosity)
216
+ rescue ContextualError => e
217
+ @error_handler.call(e)
204
218
  end
205
219
 
206
220
  ##
@@ -216,37 +230,92 @@ module Toys
216
230
  preload_file_name: @preload_file_name,
217
231
  middleware_stack: @middleware_stack,
218
232
  logger: @logger,
219
- base_level: @base_level)
233
+ base_level: @base_level,
234
+ error_handler: @error_handler)
235
+ end
236
+
237
+ ##
238
+ # A basic error handler that prints out captured errors to a stream or
239
+ # a logger.
240
+ #
241
+ class DefaultErrorHandler
242
+ ##
243
+ # Create an error handler.
244
+ #
245
+ # @param [IO,Logger,nil] sink Where to write errors. Default is `$stderr`.
246
+ #
247
+ def initialize(sink = $stderr)
248
+ @line_output = Utils::LineOutput.new(sink, log_level: ::Logger::FATAL)
249
+ end
250
+
251
+ ## @private
252
+ def call(error)
253
+ @line_output.puts(cause_string(error.cause))
254
+ @line_output.puts(context_string(error), :bold)
255
+ -1
256
+ end
257
+
258
+ private
259
+
260
+ def cause_string(cause)
261
+ lines = ["#{cause.class}: #{cause.message}"]
262
+ cause.backtrace.each_with_index.reverse_each do |bt, i|
263
+ lines << " #{(i + 1).to_s.rjust(3)}: #{bt}"
264
+ end
265
+ lines.join("\n")
266
+ end
267
+
268
+ def context_string(error)
269
+ lines = [
270
+ error.banner || "Unexpected error!",
271
+ " #{error.cause.class}: #{error.cause.message}"
272
+ ]
273
+ if error.config_path
274
+ lines << " in config file: #{error.config_path}:#{error.config_line}"
275
+ end
276
+ if error.tool_name
277
+ lines << " while executing tool: #{error.tool_name.join(' ').inspect}"
278
+ if error.tool_args
279
+ lines << " with arguments: #{error.tool_args.inspect}"
280
+ end
281
+ end
282
+ lines.join("\n")
283
+ end
220
284
  end
221
285
 
222
286
  class << self
223
287
  ##
224
288
  # Returns a default set of middleware that may be used as a starting
225
- # point for a typical CLI. This set includes:
289
+ # point for a typical CLI. This set includes the following in order:
226
290
  #
291
+ # * {Toys::Middleware::SetDefaultDescriptions} providing defaults for
292
+ # description fields
293
+ # * {Toys::Middleware::ShowHelp} adding the `--help` flag
227
294
  # * {Toys::Middleware::HandleUsageErrors}
228
- # * {Toys::Middleware::ShowUsage} adding the `--help` switch and
229
- # providing default behavior for groups
230
- # * {Toys::Middleware::AddVerbositySwitches} adding the `--verbose` and
231
- # `--quiet` switches for managing the logger level
295
+ # * {Toys::Middleware::ShowHelp} providing default behavior for groups
296
+ # * {Toys::Middleware::AddVerbosityFlags} adding the `--verbose` and
297
+ # `--quiet` flags for managing the logger level
232
298
  #
233
299
  # @return [Array]
234
300
  #
235
301
  def default_middleware_stack
236
302
  [
237
- :handle_usage_errors,
238
- :show_usage,
239
- :add_verbosity_switches
303
+ [:set_default_descriptions],
304
+ [:show_help, help_flags: true],
305
+ [:handle_usage_errors],
306
+ [:show_help, fallback_execution: true],
307
+ [:add_verbosity_flags]
240
308
  ]
241
309
  end
242
310
 
243
311
  ##
244
312
  # Returns a default logger that logs to `STDERR`.
245
313
  #
314
+ # @param [IO] stream Stream to write to. Defaults to `$stderr`.
246
315
  # @return [Logger]
247
316
  #
248
- def default_logger
249
- logger = ::Logger.new(::STDERR)
317
+ def default_logger(stream = $stderr)
318
+ logger = ::Logger.new(stream)
250
319
  logger.formatter = proc do |severity, time, _progname, msg|
251
320
  msg_str =
252
321
  case msg
@@ -257,12 +326,36 @@ module Toys
257
326
  else
258
327
  msg.inspect
259
328
  end
260
- timestr = time.strftime("%Y-%m-%d %H:%M:%S")
261
- format("[%s %5s] %s\n", timestr, severity, msg_str)
329
+ format_log(stream.tty?, time, severity, msg_str)
262
330
  end
263
331
  logger.level = ::Logger::WARN
264
332
  logger
265
333
  end
334
+
335
+ private
336
+
337
+ def format_log(is_tty, time, severity, msg)
338
+ timestr = time.strftime("%Y-%m-%d %H:%M:%S")
339
+ header = format("[%s %5s]", timestr, severity)
340
+ if is_tty
341
+ header =
342
+ case severity
343
+ when "FATAL"
344
+ ::HighLine.color(header, :bright_magenta, :bold, :underline)
345
+ when "ERROR"
346
+ ::HighLine.color(header, :bright_red, :bold)
347
+ when "WARN"
348
+ ::HighLine.color(header, :bright_yellow)
349
+ when "INFO"
350
+ ::HighLine.color(header, :bright_cyan)
351
+ when "DEBUG"
352
+ ::HighLine.color(header, :white)
353
+ else
354
+ header
355
+ end
356
+ end
357
+ "#{header} #{msg}\n"
358
+ end
266
359
  end
267
360
  end
268
361
  end
@@ -32,7 +32,7 @@ module Toys
32
32
  # This class defines the DSL for a toys configuration file.
33
33
  #
34
34
  # A toys configuration defines one or more named tools. It provides syntax
35
- # for setting the description, defining switches and arguments, specifying
35
+ # for setting the description, defining flags and arguments, specifying
36
36
  # how to execute the tool, and requesting helper modules and other services.
37
37
  # It also lets you define subtools, nested arbitrarily deep, using blocks.
38
38
  #
@@ -161,52 +161,63 @@ module Toys
161
161
  end
162
162
 
163
163
  ##
164
- # Set the long description for the current tool. The long description is
165
- # displayed in the usage documentation for the tool itself.
164
+ # Set the short description for the current tool. The short description is
165
+ # displayed with the tool in a subtool list. You may also use the
166
+ # equivalent method `short_desc`.
166
167
  #
167
- # @param [String,Toys::Utils::WrappableString...] strs The long description
168
+ # The description may be provided as a {Toys::Utils::WrappableString}, a
169
+ # single string (which will be wrapped), or an array of strings, which will
170
+ # be interpreted as string fragments that will be concatenated and wrapped.
168
171
  #
169
- def long_desc(*strs)
172
+ # @param [Toys::Utils::WrappableString,String,Array<String>] str
173
+ #
174
+ def desc(str)
170
175
  return self if _cur_tool.nil?
171
- _cur_tool.definition_path = @path
172
- _cur_tool.long_desc = strs
176
+ _cur_tool.lock_definition_path(@path)
177
+ _cur_tool.desc = str
173
178
  self
174
179
  end
180
+ alias short_desc desc
175
181
 
176
182
  ##
177
- # Set the short description for the current tool. The short description is
178
- # displayed with the tool in a command list. You may also use the
179
- # equivalent method `short_desc`.
183
+ # Set the long description for the current tool. The long description is
184
+ # displayed in the usage documentation for the tool itself.
185
+ #
186
+ # Each string may be provided as a {Toys::Utils::WrappableString}, a single
187
+ # string (which will be wrapped), or an array of strings, which will be
188
+ # interpreted as string fragments that will be concatenated and wrapped.
180
189
  #
181
- # @param [String,Toys::Utils::WrappableString...] strs The short description
190
+ # @param [Toys::Utils::WrappableString,String,Array<String>...] strs
182
191
  #
183
- def desc(*strs)
192
+ def long_desc(*strs)
184
193
  return self if _cur_tool.nil?
185
- _cur_tool.definition_path = @path
186
- _cur_tool.desc = strs
194
+ _cur_tool.lock_definition_path(@path)
195
+ _cur_tool.long_desc = strs
187
196
  self
188
197
  end
189
- alias short_desc desc
190
198
 
191
199
  ##
192
- # Add a switch to the current tool. Each switch must specify a key which
193
- # the executor may use to obtain the switch value from the context.
194
- # You may then provide the switches themselves in `OptionParser` form.
200
+ # Add a flag to the current tool. Each flag must specify a key which
201
+ # the executor may use to obtain the flag value from the context.
202
+ # You may then provide the flags themselves in `OptionParser` form.
195
203
  #
196
204
  # @param [Symbol] key The key to use to retrieve the value from the
197
205
  # execution context.
198
- # @param [String...] switches The switches in OptionParser format.
206
+ # @param [String...] flags The flags in OptionParser format.
199
207
  # @param [Object,nil] accept An OptionParser acceptor. Optional.
200
208
  # @param [Object] default The default value. This is the value that will
201
- # be set in the context if this switch is not provided on the command
209
+ # be set in the context if this flag is not provided on the command
202
210
  # line. Defaults to `nil`.
203
211
  # @param [String,Toys::Utils::WrappableString,
204
- # Array<String,Toys::Utils::WrappableString>] docs Documentation for
205
- # the switch. Defaults to empty array.
206
- # @param [Boolean] only_unique If true, any switches that are already
207
- # defined in this tool are removed from this switch. For example, if
208
- # an earlier switch uses `-a`, and this switch wants to use both
209
- # `-a` and `-b`, then only `-b` will be assigned to this switch.
212
+ # Array<String,Toys::Utils::WrappableString>] desc Short description
213
+ # for the flag. Defaults to empty array.
214
+ # @param [String,Toys::Utils::WrappableString,
215
+ # Array<String,Toys::Utils::WrappableString>] long_desc Long
216
+ # description for the flag. Defaults to empty array.
217
+ # @param [Boolean] only_unique If true, any flags that are already
218
+ # defined in this tool are removed from this flag. For example, if
219
+ # an earlier flag uses `-a`, and this flag wants to use both
220
+ # `-a` and `-b`, then only `-b` will be assigned to this flag.
210
221
  # Defaults to false.
211
222
  # @param [Proc,nil] handler An optional handler for setting/updating the
212
223
  # value. If given, it should take two arguments, the new given value
@@ -214,13 +225,17 @@ module Toys
214
225
  # should be set. The default handler simply replaces the previous
215
226
  # value. i.e. the default is effectively `-> (val, _prev) { val }`.
216
227
  #
217
- def switch(key, *switches,
218
- accept: nil, default: nil, docs: nil, only_unique: false, handler: nil)
228
+ def flag(key, *flags,
229
+ accept: nil, default: nil, desc: nil, long_desc: nil,
230
+ only_unique: false, handler: nil,
231
+ &block)
219
232
  return self if _cur_tool.nil?
220
- _cur_tool.definition_path = @path
221
- _cur_tool.add_switch(key, *switches,
222
- accept: accept, default: default, docs: docs,
223
- only_unique: only_unique, handler: handler)
233
+ _cur_tool.lock_definition_path(@path)
234
+ _cur_tool.add_flag(key, *flags,
235
+ accept: accept, default: default,
236
+ desc: desc, long_desc: long_desc,
237
+ only_unique: only_unique, handler: handler,
238
+ &block)
224
239
  self
225
240
  end
226
241
 
@@ -232,16 +247,25 @@ module Toys
232
247
  # @param [Symbol] key The key to use to retrieve the value from the
233
248
  # execution context.
234
249
  # @param [Object,nil] accept An OptionParser acceptor. Optional.
250
+ # @param [String] display_name A name to use for display (in help text and
251
+ # error reports). Defaults to the key in upper case.
252
+ # @param [String,Toys::Utils::WrappableString,
253
+ # Array<String,Toys::Utils::WrappableString>] desc Short description
254
+ # for the arg. Defaults to empty array.
235
255
  # @param [String,Toys::Utils::WrappableString,
236
- # Array<String,Toys::Utils::WrappableString>] docs Documentation for the
237
- # arg. Defaults to empty array.
256
+ # Array<String,Toys::Utils::WrappableString>] long_desc Long
257
+ # description for the arg. Defaults to empty array.
238
258
  #
239
- def required_arg(key, accept: nil, docs: nil)
259
+ def required_arg(key, accept: nil, display_name: nil, desc: nil, long_desc: nil, &block)
240
260
  return self if _cur_tool.nil?
241
- _cur_tool.definition_path = @path
242
- _cur_tool.add_required_arg(key, accept: accept, docs: docs)
261
+ _cur_tool.lock_definition_path(@path)
262
+ _cur_tool.add_required_arg(key,
263
+ accept: accept, display_name: display_name,
264
+ desc: desc, long_desc: long_desc,
265
+ &block)
243
266
  self
244
267
  end
268
+ alias required required_arg
245
269
 
246
270
  ##
247
271
  # Add an optional positional argument to the current tool. You must specify
@@ -251,20 +275,30 @@ module Toys
251
275
  #
252
276
  # @param [Symbol] key The key to use to retrieve the value from the
253
277
  # execution context.
254
- # @param [Object,nil] accept An OptionParser acceptor. Optional.
255
278
  # @param [Object] default The default value. This is the value that will
256
279
  # be set in the context if this argument is not provided on the command
257
280
  # line. Defaults to `nil`.
281
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
282
+ # @param [String] display_name A name to use for display (in help text and
283
+ # error reports). Defaults to the key in upper case.
284
+ # @param [String,Toys::Utils::WrappableString,
285
+ # Array<String,Toys::Utils::WrappableString>] desc Short description
286
+ # for the arg. Defaults to empty array.
258
287
  # @param [String,Toys::Utils::WrappableString,
259
- # Array<String,Toys::Utils::WrappableString>] docs Documentation for the
260
- # arg. Defaults to empty array.
288
+ # Array<String,Toys::Utils::WrappableString>] long_desc Long
289
+ # description for the arg. Defaults to empty array.
261
290
  #
262
- def optional_arg(key, accept: nil, default: nil, docs: nil)
291
+ def optional_arg(key, default: nil, accept: nil, display_name: nil,
292
+ desc: nil, long_desc: nil, &block)
263
293
  return self if _cur_tool.nil?
264
- _cur_tool.definition_path = @path
265
- _cur_tool.add_optional_arg(key, accept: accept, default: default, docs: docs)
294
+ _cur_tool.lock_definition_path(@path)
295
+ _cur_tool.add_optional_arg(key,
296
+ accept: accept, default: default, display_name: display_name,
297
+ desc: desc, long_desc: long_desc,
298
+ &block)
266
299
  self
267
300
  end
301
+ alias optional optional_arg
268
302
 
269
303
  ##
270
304
  # Specify what should be done with unmatched positional arguments. You must
@@ -273,20 +307,30 @@ module Toys
273
307
  #
274
308
  # @param [Symbol] key The key to use to retrieve the value from the
275
309
  # execution context.
276
- # @param [Object,nil] accept An OptionParser acceptor. Optional.
277
310
  # @param [Object] default The default value. This is the value that will
278
311
  # be set in the context if no unmatched arguments are provided on the
279
312
  # command line. Defaults to the empty array `[]`.
313
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
314
+ # @param [String] display_name A name to use for display (in help text and
315
+ # error reports). Defaults to the key in upper case.
316
+ # @param [String,Toys::Utils::WrappableString,
317
+ # Array<String,Toys::Utils::WrappableString>] desc Short description
318
+ # for the arg. Defaults to empty array.
280
319
  # @param [String,Toys::Utils::WrappableString,
281
- # Array<String,Toys::Utils::WrappableString>] docs Documentation for the
282
- # args. Defaults to empty array.
320
+ # Array<String,Toys::Utils::WrappableString>] long_desc Long
321
+ # description for the arg. Defaults to empty array.
283
322
  #
284
- def remaining_args(key, accept: nil, default: [], docs: nil)
323
+ def remaining_args(key, default: [], accept: nil, display_name: nil,
324
+ desc: nil, long_desc: nil, &block)
285
325
  return self if _cur_tool.nil?
286
- _cur_tool.definition_path = @path
287
- _cur_tool.set_remaining_args(key, accept: accept, default: default, docs: docs)
326
+ _cur_tool.lock_definition_path(@path)
327
+ _cur_tool.set_remaining_args(key,
328
+ accept: accept, default: default, display_name: display_name,
329
+ desc: desc, long_desc: long_desc,
330
+ &block)
288
331
  self
289
332
  end
333
+ alias remaining remaining_args
290
334
 
291
335
  ##
292
336
  # Specify the executor for this tool. This is a block that will be called,
@@ -294,7 +338,7 @@ module Toys
294
338
  #
295
339
  def execute(&block)
296
340
  return self if _cur_tool.nil?
297
- _cur_tool.definition_path = @path
341
+ _cur_tool.lock_definition_path(@path)
298
342
  _cur_tool.executor = block
299
343
  self
300
344
  end
@@ -309,7 +353,7 @@ module Toys
309
353
  #
310
354
  def helper(name, &block)
311
355
  return self if _cur_tool.nil?
312
- _cur_tool.definition_path = @path
356
+ _cur_tool.lock_definition_path(@path)
313
357
  _cur_tool.add_helper(name, &block)
314
358
  self
315
359
  end
@@ -324,7 +368,7 @@ module Toys
324
368
  #
325
369
  def use(mod)
326
370
  return self if _cur_tool.nil?
327
- _cur_tool.definition_path = @path
371
+ _cur_tool.lock_definition_path(@path)
328
372
  _cur_tool.use_module(mod)
329
373
  self
330
374
  end
@@ -347,9 +391,11 @@ module Toys
347
391
  dsl = new(words, remaining_words, priority, loader, path)
348
392
  case source
349
393
  when String
350
- # rubocop:disable Security/Eval
351
- eval(source, dsl._binding, path, 1)
352
- # rubocop:enable Security/Eval
394
+ ContextualError.capture_path("Error while loading Toys config!", path) do
395
+ # rubocop:disable Security/Eval
396
+ eval(source, dsl._binding, path, 1)
397
+ # rubocop:enable Security/Eval
398
+ end
353
399
  when ::Proc
354
400
  dsl.instance_eval(&source)
355
401
  end