toys-core 0.11.5 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +5 -2
  5. data/docs/guide.md +1 -1
  6. data/lib/toys/acceptor.rb +13 -4
  7. data/lib/toys/arg_parser.rb +7 -7
  8. data/lib/toys/cli.rb +170 -120
  9. data/lib/toys/compat.rb +71 -23
  10. data/lib/toys/completion.rb +18 -6
  11. data/lib/toys/context.rb +24 -15
  12. data/lib/toys/core.rb +6 -2
  13. data/lib/toys/dsl/base.rb +87 -0
  14. data/lib/toys/dsl/flag.rb +26 -20
  15. data/lib/toys/dsl/flag_group.rb +18 -14
  16. data/lib/toys/dsl/internal.rb +206 -0
  17. data/lib/toys/dsl/positional_arg.rb +26 -16
  18. data/lib/toys/dsl/tool.rb +180 -218
  19. data/lib/toys/errors.rb +64 -8
  20. data/lib/toys/flag.rb +662 -656
  21. data/lib/toys/flag_group.rb +24 -10
  22. data/lib/toys/input_file.rb +13 -7
  23. data/lib/toys/loader.rb +293 -140
  24. data/lib/toys/middleware.rb +46 -22
  25. data/lib/toys/mixin.rb +10 -8
  26. data/lib/toys/positional_arg.rb +21 -20
  27. data/lib/toys/settings.rb +914 -0
  28. data/lib/toys/source_info.rb +147 -35
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +2 -0
  30. data/lib/toys/standard_middleware/apply_config.rb +6 -4
  31. data/lib/toys/standard_middleware/handle_usage_errors.rb +1 -0
  32. data/lib/toys/standard_middleware/set_default_descriptions.rb +19 -18
  33. data/lib/toys/standard_middleware/show_help.rb +19 -5
  34. data/lib/toys/standard_middleware/show_root_version.rb +2 -0
  35. data/lib/toys/standard_mixins/bundler.rb +24 -15
  36. data/lib/toys/standard_mixins/exec.rb +43 -34
  37. data/lib/toys/standard_mixins/fileutils.rb +3 -1
  38. data/lib/toys/standard_mixins/gems.rb +21 -17
  39. data/lib/toys/standard_mixins/git_cache.rb +46 -0
  40. data/lib/toys/standard_mixins/highline.rb +8 -8
  41. data/lib/toys/standard_mixins/terminal.rb +5 -5
  42. data/lib/toys/standard_mixins/xdg.rb +56 -0
  43. data/lib/toys/template.rb +11 -9
  44. data/lib/toys/{tool.rb → tool_definition.rb} +292 -226
  45. data/lib/toys/utils/completion_engine.rb +7 -2
  46. data/lib/toys/utils/exec.rb +162 -132
  47. data/lib/toys/utils/gems.rb +85 -60
  48. data/lib/toys/utils/git_cache.rb +813 -0
  49. data/lib/toys/utils/help_text.rb +117 -37
  50. data/lib/toys/utils/terminal.rb +11 -3
  51. data/lib/toys/utils/xdg.rb +293 -0
  52. data/lib/toys/wrappable_string.rb +9 -2
  53. data/lib/toys-core.rb +18 -6
  54. metadata +14 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae396604e3a0005b460ad94f95187b603f36e8d10d1147da2a78b069232f7963
4
- data.tar.gz: 267209d6e7ab4575053219eee5fb1c6be47b9bd6ac6468b6da5c9abb9ed227c2
3
+ metadata.gz: 68a552244c25e03216c7bf958a7ccd2a35bb7fb3795a02e77c4a839b7720e258
4
+ data.tar.gz: 58513ce7d5d60646a1b9e51235d04df470ee5d9317a9a72048a8e95872227da0
5
5
  SHA512:
6
- metadata.gz: 527f51e0e679f93786ecd671ff14386615e2014e0dbd4995fd4bb6e58040cb00b27d83d3721c412eb8cc83fdc82fe7dff63c97e0e3d8bca8f8b8d7b04a5d9f41
7
- data.tar.gz: 07c617895710b6f5dd69c6fdb16613c2ed7474361a1cbd8617ae1d271eca647ffa457e77bec7e1b30915b1e01ad25173c74251f1728f261a5d5565ca29b80d68
6
+ metadata.gz: 299c193de07d0802046029c7b305a1e4cecac715eee32d259a917b6ecc2505975ba10a2aca90b510036076d9f32b58849a02381ae61837336eee5361cd2126ea
7
+ data.tar.gz: b42a4d81db4a71a136deafd4a62f0e7087325097a1c3e7473a5b7edda699b08ee1c8d81377765a70d8a18fd0a2693874781418f9953ca52f92cced670bec1421
data/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # Release History
2
2
 
3
+ ### v0.13.0 / 2022-02-08
4
+
5
+ Toys-Core 0.13.0 is a major release with significant improvements to the git cache, along with compatibility improvements and bug fixes.
6
+
7
+ New functionality:
8
+
9
+ * The `load_git` directive and the underlying `Toys::Utils::GitCache` class now support updating from git based on cache age.
10
+ * The `Toys::Utils::GitCache` class supports copying git content into a provided directory, querying repo information, and deleting cache data.
11
+ * The `Toys::Utils::GitCache` class makes files read-only, to help prevent clients from interfering with one another.
12
+ * The `:terminal` mixin and the underlying `Toys::Utils::Terminal` class now honor the `NO_COLOR` environment variable.
13
+ * Added `Toys::CLI#load_tool`, which is useful for testing tools.
14
+
15
+ Fixes and compatibility updates:
16
+
17
+ * Bundler install/updates are now spawned in subprocesses for compatibility with bundler 2.3. The bundler integration also now requires bundler 2.2 or later.
18
+ * The `exec_tool` and `exec_proc` methods in the `:exec` mixin now log their execution in the same way as other exec functions.
19
+ * Minor compatibility fixes to provide partial support for TruffleRuby.
20
+
21
+ Other notes:
22
+
23
+ * The internal GitCache representation has changed significantly to support additional features and improve robustness and performance. This will force existing caches to update, but should not break existing usage.
24
+
25
+ ### v0.12.2 / 2021-08-30
26
+
27
+ * FIXED: Tool context inspect string is no longer overwhelmingly long
28
+ * FIXED: Fixed an exception in GitCache when updating a changed ref
29
+
30
+ ### v0.12.1 / 2021-08-17
31
+
32
+ * FIXED: Fixed a regression in 0.12.0 where bundler could use the wrong Gemfile if you set a custom context directory
33
+
34
+ ### v0.12.0 / 2021-08-05
35
+
36
+ Toys-Core 0.12.0 is a major release with significant new features and bug fixes, and a few breaking interface changes. Additionally, this release now requires Ruby 2.4 or later.
37
+
38
+ Breaking interface changes:
39
+
40
+ * The Toys::Tool class has been renamed Toys::ToolDefinition so that the old name can be used for class-based tool definition.
41
+ * Tool definition now raises ToolDefinitionError if whitespace, control characters, or certain punctuation are used in a tool name.
42
+ * Toys::Loader#add_path no longer supports multiple paths. Use add_path_set instead.
43
+ * The "name" argument was renamed to "source_name" in Toys::Loader#add_block and Toys::CLI#add_config_block
44
+
45
+ New functionality:
46
+
47
+ * The DSL now supports a class-based tool definition syntax (in addition to the existing block-based syntax). Some users may prefer this new class-based style as more Ruby-like.
48
+ * You can now load tools from a remote git repository using the load_git directive.
49
+ * Whitespace is now automatically considered a name delimiter when defining tools.
50
+ * There is now an extensible settings mechanism to activate less-common tool behavior. Currently there is one setting, which causes subtools to inherit their parent's methods by default.
51
+ * The load directive can load into a new tool.
52
+ * Added a new utility class and mixin that provides XDG Base Directory information.
53
+ * Added a new utility class and mixin that provides cached access to remote git repos.
54
+ * The help text generator now supports splitting the subtool list by source.
55
+ * Loader and CLI methods that add tool configs now uniformly provide optional source_name and context_directory arguments.
56
+ * Toys::SourceInfo now supports getting the root ancestor and priority of a source.
57
+ * Toys::ToolDefinition now has a direct accessor for the source root. This is always set for a tool, even if it isn't marked as finished.
58
+
59
+ Fixes:
60
+
61
+ * Fixed some bundler integration issues that occurred when the bundle is being installed in a separate path such as a vendor directory.
62
+ * Toys::ContextualError now includes the full backtrace of the cause.
63
+ * Cleaned up some unused memory objects during tool loading and lookup.
64
+
3
65
  ### v0.11.5 / 2021-03-28
4
66
 
5
67
  * BREAKING CHANGE: The exit_on_nonzero_status option to exec now exits on signals and failures to spawn, in addition to error codes.
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # License
2
2
 
3
- Copyright 2019-2020 Daniel Azuma and the Toys contributors
3
+ Copyright 2019-2022 Daniel Azuma and the Toys contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -34,11 +34,14 @@ Install the **toys-core** gem using:
34
34
  You may also install the **toys** gem, which brings in **toys-core** as a
35
35
  dependency.
36
36
 
37
- Toys-Core requires Ruby 2.3 or later.
37
+ Toys-Core requires Ruby 2.4 or later.
38
38
 
39
39
  Most parts of Toys-Core work on JRuby. However, JRuby is not recommended
40
40
  because of JVM boot latency, lack of support for Kernel#fork, and other issues.
41
41
 
42
+ Most parts of Toys-Core work on TruffleRuby. However, TruffleRuby is not
43
+ recommended because it has a few known bugs that affect Toys.
44
+
42
45
  ### Create a new executable
43
46
 
44
47
  We'll start by creating an executable Ruby script. Using your favorite text
@@ -338,7 +341,7 @@ templates, and middleware, in the
338
341
 
339
342
  ## License
340
343
 
341
- Copyright 2019-2020 Daniel Azuma and the Toys contributors
344
+ Copyright 2019-2022 Daniel Azuma and the Toys contributors
342
345
 
343
346
  Permission is hereby granted, free of charge, to any person obtaining a copy
344
347
  of this software and associated documentation files (the "Software"), to deal
data/docs/guide.md CHANGED
@@ -31,7 +31,7 @@ sophisticated command line tools.
31
31
 
32
32
  Toys-Core is a command line *framework* in the traditional sense. It is
33
33
  intended to be used to write custom command line executables in Ruby. The
34
- framework provides common facilities such as argumentparsing and online help,
34
+ framework provides common facilities such as argument parsing and online help,
35
35
  while your executable chooses and configures those facilities, and implements
36
36
  the actual behavior.
37
37
 
data/lib/toys/acceptor.rb CHANGED
@@ -178,6 +178,7 @@ module Toys
178
178
 
179
179
  ##
180
180
  # Overrides {Toys::Acceptor::Base#match} to use the given function.
181
+ #
181
182
  # @private
182
183
  #
183
184
  def match(str)
@@ -188,6 +189,7 @@ module Toys
188
189
  ##
189
190
  # Overrides {Toys::Acceptor::Base#convert} to use the given function's
190
191
  # result.
192
+ #
191
193
  # @private
192
194
  #
193
195
  def convert(_str, result)
@@ -234,6 +236,7 @@ module Toys
234
236
 
235
237
  ##
236
238
  # Overrides {Toys::Acceptor::Base#match} to use the given regex.
239
+ #
237
240
  # @private
238
241
  #
239
242
  def match(str)
@@ -242,6 +245,7 @@ module Toys
242
245
 
243
246
  ##
244
247
  # Overrides {Toys::Acceptor::Base#convert} to use the given converter.
248
+ #
245
249
  # @private
246
250
  #
247
251
  def convert(str, *extra)
@@ -285,6 +289,7 @@ module Toys
285
289
 
286
290
  ##
287
291
  # Overrides {Toys::Acceptor::Base#match} to find the value.
292
+ #
288
293
  # @private
289
294
  #
290
295
  def match(str)
@@ -294,6 +299,7 @@ module Toys
294
299
  ##
295
300
  # Overrides {Toys::Acceptor::Base#convert} to return the actual enum
296
301
  # element.
302
+ #
297
303
  # @private
298
304
  #
299
305
  def convert(_str, elem)
@@ -303,6 +309,7 @@ module Toys
303
309
  ##
304
310
  # Overrides {Toys::Acceptor::Base#suggestions} to return close matches
305
311
  # from the enum.
312
+ #
306
313
  # @private
307
314
  #
308
315
  def suggestions(str)
@@ -513,7 +520,9 @@ module Toys
513
520
  internal_create(spec, options, block)
514
521
  end
515
522
 
516
- ## @private
523
+ ##
524
+ # @private
525
+ #
517
526
  def scalarize_spec(spec, options, block)
518
527
  spec ||= block
519
528
  if options.empty?
@@ -602,11 +611,11 @@ module Toys
602
611
  Simple.new(type_desc: "boolean", well_known_spec: spec) do |s|
603
612
  if s.nil?
604
613
  default
614
+ elsif s.empty?
615
+ REJECT
605
616
  else
606
617
  s = s.downcase
607
- if s.empty?
608
- REJECT
609
- elsif TRUE_STRINGS.any? { |t| t.start_with?(s) }
618
+ if TRUE_STRINGS.any? { |t| t.start_with?(s) }
610
619
  true
611
620
  elsif FALSE_STRINGS.any? { |f| f.start_with?(s) }
612
621
  false
@@ -261,7 +261,7 @@ module Toys
261
261
  # @param message [String] The message. Required.
262
262
  #
263
263
  def initialize(message)
264
- super(message)
264
+ super(message, name: nil)
265
265
  end
266
266
  end
267
267
 
@@ -269,7 +269,7 @@ module Toys
269
269
  # Create an argument parser for a particular tool.
270
270
  #
271
271
  # @param cli [Toys::CLI] The CLI in effect.
272
- # @param tool [Toys::Tool] The tool defining the argument format.
272
+ # @param tool [Toys::ToolDefinition] The tool defining the argument format.
273
273
  # @param default_data [Hash] Additional initial data (such as verbosity).
274
274
  # @param require_exact_flag_match [Boolean] Whether to require flag matches
275
275
  # be exact (not partial). Default is false.
@@ -295,7 +295,7 @@ module Toys
295
295
 
296
296
  ##
297
297
  # The tool definition governing this parser.
298
- # @return [Toys::Tool]
298
+ # @return [Toys::ToolDefinition]
299
299
  #
300
300
  attr_reader :tool
301
301
 
@@ -417,6 +417,7 @@ module Toys
417
417
 
418
418
  REMAINING_HANDLER = ->(val, prev) { prev.is_a?(::Array) ? prev << val : [val] }
419
419
  ARG_HANDLER = ->(val, _prev) { val }
420
+ private_constant :REMAINING_HANDLER, :ARG_HANDLER
420
421
 
421
422
  def initial_data(cli, tool, default_data)
422
423
  data = {
@@ -429,7 +430,7 @@ module Toys
429
430
  Context::Key::TOOL_NAME => tool.full_name,
430
431
  Context::Key::USAGE_ERRORS => [],
431
432
  }
432
- Compat.merge_clones(data, tool.default_data)
433
+ tool.default_data.each { |k, v| data[k] = v.clone }
433
434
  default_data.each { |k, v| data[k] ||= v }
434
435
  data
435
436
  end
@@ -450,7 +451,7 @@ module Toys
450
451
  case arg
451
452
  when "--"
452
453
  @flags_allowed = false
453
- when /\A(--\w[\?\w-]*)=(.*)\z/
454
+ when /\A(--\w[?\w-]*)=(.*)\z/
454
455
  handle_valued_flag(::Regexp.last_match(1), ::Regexp.last_match(2))
455
456
  when /\A--.+\z/
456
457
  handle_plain_flag(arg)
@@ -474,8 +475,7 @@ module Toys
474
475
  return "" unless flag_def
475
476
  @seen_flag_keys << flag_def.key
476
477
  if flag_def.flag_type == :boolean
477
- add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?,
478
- :flag, name)
478
+ add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?, :flag, name)
479
479
  elsif following.empty?
480
480
  if flag_def.value_type == :required || flag_result.unique_flag_syntax.value_delim == " "
481
481
  @active_flag_def = flag_def
data/lib/toys/cli.rb CHANGED
@@ -78,8 +78,8 @@ module Toys
78
78
  # with different verbosity settings (since the logger cannot have
79
79
  # multiple level settings simultaneously). In that case, do not set a
80
80
  # global logger, but use the `logger_factory` parameter instead.
81
- # @param logger_factory [Proc] A proc that takes a {Toys::Tool} as an
82
- # argument, and returns a `Logger` to use when running that tool.
81
+ # @param logger_factory [Proc] A proc that takes a {Toys::ToolDefinition}
82
+ # as an argument, and returns a `Logger` to use when running that tool.
83
83
  # Optional. If not provided (and no global logger is set), CLI will use
84
84
  # a default factory that writes generates loggers writing formatted
85
85
  # output to `STDERR`, as defined by {Toys::CLI.default_logger_factory}.
@@ -175,26 +175,24 @@ module Toys
175
175
  # Optional. If not provided, lib directories are disabled.
176
176
  # Note: the standard toys executable sets this to `".lib"`.
177
177
  #
178
- def initialize( # rubocop:disable Metrics/MethodLength
179
- executable_name: nil,
180
- middleware_stack: nil,
181
- extra_delimiters: "",
182
- config_dir_name: nil,
183
- config_file_name: nil,
184
- index_file_name: nil,
185
- preload_file_name: nil,
186
- preload_dir_name: nil,
187
- data_dir_name: nil,
188
- lib_dir_name: nil,
189
- mixin_lookup: nil,
190
- middleware_lookup: nil,
191
- template_lookup: nil,
192
- logger_factory: nil,
193
- logger: nil,
194
- base_level: nil,
195
- error_handler: nil,
196
- completion: nil
197
- )
178
+ def initialize(executable_name: nil, # rubocop:disable Metrics/MethodLength
179
+ middleware_stack: nil,
180
+ extra_delimiters: "",
181
+ config_dir_name: nil,
182
+ config_file_name: nil,
183
+ index_file_name: nil,
184
+ preload_file_name: nil,
185
+ preload_dir_name: nil,
186
+ data_dir_name: nil,
187
+ lib_dir_name: nil,
188
+ mixin_lookup: nil,
189
+ middleware_lookup: nil,
190
+ template_lookup: nil,
191
+ logger_factory: nil,
192
+ logger: nil,
193
+ base_level: nil,
194
+ error_handler: nil,
195
+ completion: nil)
198
196
  @executable_name = executable_name || ::File.basename($PROGRAM_NAME)
199
197
  @middleware_stack = middleware_stack || CLI.default_middleware_stack
200
198
  @mixin_lookup = mixin_lookup || CLI.default_mixin_lookup
@@ -321,10 +319,22 @@ module Toys
321
319
  # a Toys directory.
322
320
  # @param high_priority [Boolean] Add the config at the head of the priority
323
321
  # list rather than the tail.
322
+ # @param source_name [String] A custom name for the root source. Optional.
323
+ # @param context_directory [String,nil,:path,:parent] The context directory
324
+ # for tools loaded from this path. You can pass a directory path as a
325
+ # string, `:path` to denote the given path, `:parent` to denote the
326
+ # given path's parent directory, or `nil` to denote no context.
327
+ # Defaults to `:parent`.
324
328
  # @return [self]
325
329
  #
326
- def add_config_path(path, high_priority: false)
327
- @loader.add_path(path, high_priority: high_priority)
330
+ def add_config_path(path,
331
+ high_priority: false,
332
+ source_name: nil,
333
+ context_directory: :parent)
334
+ @loader.add_path(path,
335
+ high_priority: high_priority,
336
+ source_name: source_name,
337
+ context_directory: context_directory)
328
338
  self
329
339
  end
330
340
 
@@ -336,15 +346,24 @@ module Toys
336
346
  #
337
347
  # @param high_priority [Boolean] Add the config at the head of the priority
338
348
  # list rather than the tail.
339
- # @param name [String] The source name that will be shown in documentation
340
- # for tools defined in this block. If omitted, a default unique string
341
- # will be generated.
349
+ # @param source_name [String] The source name that will be shown in
350
+ # documentation for tools defined in this block. If omitted, a default
351
+ # unique string will be generated.
342
352
  # @param block [Proc] The block of configuration, executed in the context
343
353
  # of the tool DSL {Toys::DSL::Tool}.
354
+ # @param context_directory [String,nil] The context directory for tools
355
+ # loaded from this block. You can pass a directory path as a string, or
356
+ # `nil` to denote no context. Defaults to `nil`.
344
357
  # @return [self]
345
358
  #
346
- def add_config_block(high_priority: false, name: nil, &block)
347
- @loader.add_block(high_priority: high_priority, name: name, &block)
359
+ def add_config_block(high_priority: false,
360
+ source_name: nil,
361
+ context_directory: nil,
362
+ &block)
363
+ @loader.add_block(high_priority: high_priority,
364
+ source_name: source_name,
365
+ context_directory: context_directory,
366
+ &block)
348
367
  self
349
368
  end
350
369
 
@@ -358,19 +377,28 @@ module Toys
358
377
  # @param search_path [String] A path to search for configs.
359
378
  # @param high_priority [Boolean] Add the configs at the head of the
360
379
  # priority list rather than the tail.
380
+ # @param context_directory [String,nil,:path,:parent] The context directory
381
+ # for tools loaded from this path. You can pass a directory path as a
382
+ # string, `:path` to denote the given path, `:parent` to denote the
383
+ # given path's parent directory, or `nil` to denote no context.
384
+ # Defaults to `:path`.
361
385
  # @return [self]
362
386
  #
363
- def add_search_path(search_path, high_priority: false)
387
+ def add_search_path(search_path,
388
+ high_priority: false,
389
+ context_directory: :path)
364
390
  paths = []
365
391
  if @config_file_name
366
392
  file_path = ::File.join(search_path, @config_file_name)
367
- paths << file_path if !::File.directory?(file_path) && ::File.readable?(file_path)
393
+ paths << @config_file_name if !::File.directory?(file_path) && ::File.readable?(file_path)
368
394
  end
369
395
  if @config_dir_name
370
396
  dir_path = ::File.join(search_path, @config_dir_name)
371
- paths << dir_path if ::File.directory?(dir_path) && ::File.readable?(dir_path)
397
+ paths << @config_dir_name if ::File.directory?(dir_path) && ::File.readable?(dir_path)
372
398
  end
373
- @loader.add_path(paths, high_priority: high_priority)
399
+ @loader.add_path_set(search_path, paths,
400
+ high_priority: high_priority,
401
+ context_directory: context_directory)
374
402
  self
375
403
  end
376
404
 
@@ -416,6 +444,9 @@ module Toys
416
444
  # run and what arguments to pass to it. You may pass either a single
417
445
  # array of strings, or a series of string arguments.
418
446
  # @param verbosity [Integer] Initial verbosity. Default is 0.
447
+ # @param delegated_from [Toys::Context] The context from which this
448
+ # execution is delegated. Optional. Should be set only if this is a
449
+ # delegated execution.
419
450
  #
420
451
  # @return [Integer] The resulting process status code (i.e. 0 for success).
421
452
  #
@@ -427,101 +458,32 @@ module Toys
427
458
  "Error during tool execution!", tool.source_info&.source_path,
428
459
  tool_name: tool.full_name, tool_args: remaining
429
460
  ) do
430
- default_data = {
431
- Context::Key::VERBOSITY => verbosity,
432
- Context::Key::DELEGATED_FROM => delegated_from,
433
- }
434
- run_tool(tool, remaining, default_data)
461
+ context = build_context(tool, remaining,
462
+ verbosity: verbosity,
463
+ delegated_from: delegated_from)
464
+ execute_tool(tool, context, &:run)
435
465
  end
436
466
  rescue ContextualError, ::Interrupt => e
437
467
  @error_handler.call(e).to_i
438
468
  end
439
469
 
440
- private
441
-
442
470
  ##
443
- # Run the given tool with the given arguments.
444
- # Does not handle exceptions.
471
+ # Prepare a tool to be run, but just execute the given block rather than
472
+ # performing a full run of the tool. This is intended for testing tools.
473
+ # Unlike {#run}, this does not catch errors and perform error handling.
445
474
  #
446
- # @param tool [Toys::Tool] The tool to run.
447
- # @param args [Array<String>] Command line arguments passed to the tool.
448
- # @param default_data [Hash] Initial tool context data.
449
- # @return [Integer] The resulting status code
475
+ # @param args [String...] Command line arguments specifying which tool to
476
+ # run and what arguments to pass to it. You may pass either a single
477
+ # array of strings, or a series of string arguments.
478
+ # @yieldparam context [Toys::Context] Yields the tool context.
450
479
  #
451
- def run_tool(tool, args, default_data)
452
- arg_parser = ArgParser.new(self, tool,
453
- default_data: default_data,
454
- require_exact_flag_match: tool.exact_flag_match_required?)
455
- arg_parser.parse(args).finish
456
- context = tool.tool_class.new(arg_parser.data)
457
- tool.source_info&.apply_lib_paths
458
- tool.run_initializers(context)
459
-
460
- cur_logger = context[Context::Key::LOGGER]
461
- if cur_logger
462
- original_level = cur_logger.level
463
- cur_logger.level = (base_level || original_level) - context[Context::Key::VERBOSITY].to_i
464
- end
465
- begin
466
- execute_tool_in_context(context, tool)
467
- ensure
468
- cur_logger.level = original_level if cur_logger
469
- end
470
- end
471
-
472
- def execute_tool_in_context(context, tool)
473
- executor = proc do
474
- begin
475
- if !context[Context::Key::USAGE_ERRORS].empty?
476
- handle_usage_errors(context, tool)
477
- elsif !tool.runnable?
478
- raise NotRunnableError, "No implementation for tool #{tool.display_name.inspect}"
479
- else
480
- context.run
481
- end
482
- rescue ::Interrupt => e
483
- raise e unless tool.handles_interrupts?
484
- handle_interrupt(context, tool.interrupt_handler, e)
485
- end
486
- end
487
- tool.built_middleware.reverse_each do |middleware|
488
- executor = make_executor(middleware, context, executor)
489
- end
490
- catch(:result) do
491
- executor.call
492
- 0
493
- end
494
- end
495
-
496
- def handle_usage_errors(context, tool)
497
- usage_errors = context[Context::Key::USAGE_ERRORS]
498
- handler = tool.usage_error_handler
499
- raise ArgParsingError, usage_errors if handler.nil?
500
- handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
501
- if handler.arity.zero?
502
- context.instance_exec(&handler)
503
- else
504
- context.instance_exec(usage_errors, &handler)
505
- end
506
- end
507
-
508
- def handle_interrupt(context, handler, exception)
509
- handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
510
- if handler.arity.zero?
511
- context.instance_exec(&handler)
512
- else
513
- context.instance_exec(exception, &handler)
514
- end
515
- rescue ::Interrupt => e
516
- raise e if e.equal?(exception)
517
- handle_interrupt(context, handler, e)
518
- end
519
-
520
- def make_executor(middleware, context, next_executor)
521
- if middleware.respond_to?(:run)
522
- proc { middleware.run(context, &next_executor) }
523
- else
524
- next_executor
480
+ # @return [Object] The value returned from the block.
481
+ #
482
+ def load_tool(*args)
483
+ tool, remaining = @loader.lookup(args.flatten)
484
+ context = build_context(tool, remaining)
485
+ execute_tool(tool, context) do |ctx|
486
+ ctx.exit(yield ctx)
525
487
  end
526
488
  end
527
489
 
@@ -727,5 +689,93 @@ module Toys
727
689
  "#{styled_header} #{msg}\n"
728
690
  end
729
691
  end
692
+
693
+ private
694
+
695
+ def build_context(tool, args, verbosity: 0, delegated_from: nil)
696
+ default_data = {
697
+ Context::Key::VERBOSITY => verbosity,
698
+ Context::Key::DELEGATED_FROM => delegated_from,
699
+ }
700
+ arg_parser = ArgParser.new(self, tool,
701
+ default_data: default_data,
702
+ require_exact_flag_match: tool.exact_flag_match_required?)
703
+ arg_parser.parse(args).finish
704
+ tool.tool_class.new(arg_parser.data)
705
+ end
706
+
707
+ def execute_tool(tool, context)
708
+ tool.source_info&.apply_lib_paths
709
+ tool.run_initializers(context)
710
+ cur_logger = context[Context::Key::LOGGER]
711
+ if cur_logger
712
+ original_level = cur_logger.level
713
+ cur_logger.level = (base_level || original_level) - context[Context::Key::VERBOSITY].to_i
714
+ end
715
+ begin
716
+ executor = build_executor(tool, context) do
717
+ yield context
718
+ end
719
+ catch(:result) do
720
+ executor.call
721
+ 0
722
+ end
723
+ ensure
724
+ cur_logger.level = original_level if cur_logger
725
+ end
726
+ end
727
+
728
+ def build_executor(tool, context)
729
+ executor = proc do
730
+ begin
731
+ if !context[Context::Key::USAGE_ERRORS].empty?
732
+ handle_usage_errors(context, tool)
733
+ elsif !tool.runnable?
734
+ raise NotRunnableError, "No implementation for tool #{tool.display_name.inspect}"
735
+ else
736
+ yield
737
+ end
738
+ rescue ::Interrupt => e
739
+ raise e unless tool.handles_interrupts?
740
+ handle_interrupt(context, tool.interrupt_handler, e)
741
+ end
742
+ end
743
+ tool.built_middleware.reverse_each do |middleware|
744
+ executor = make_executor(middleware, context, executor)
745
+ end
746
+ executor
747
+ end
748
+
749
+ def handle_usage_errors(context, tool)
750
+ usage_errors = context[Context::Key::USAGE_ERRORS]
751
+ handler = tool.usage_error_handler
752
+ raise ArgParsingError, usage_errors if handler.nil?
753
+ handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
754
+ if handler.arity.zero?
755
+ context.instance_exec(&handler)
756
+ else
757
+ context.instance_exec(usage_errors, &handler)
758
+ end
759
+ end
760
+
761
+ def handle_interrupt(context, handler, exception)
762
+ handler = context.method(handler).to_proc if handler.is_a?(::Symbol)
763
+ if handler.arity.zero?
764
+ context.instance_exec(&handler)
765
+ else
766
+ context.instance_exec(exception, &handler)
767
+ end
768
+ rescue ::Interrupt => e
769
+ raise e if e.equal?(exception)
770
+ handle_interrupt(context, handler, e)
771
+ end
772
+
773
+ def make_executor(middleware, context, next_executor)
774
+ if middleware.respond_to?(:run)
775
+ proc { middleware.run(context, &next_executor) }
776
+ else
777
+ next_executor
778
+ end
779
+ end
730
780
  end
731
781
  end