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