toys-core 0.10.1 → 0.11.0

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: b63889a0ce44a42d805bba363f2051c1b1f82c6c50e70138b35cab077441dcdd
4
- data.tar.gz: 0403e5af7269879d468baae34bbae1664c1f83dfef4ec91c1545079502db5c86
3
+ metadata.gz: 1cf20d64fb78cfa559000a8007d3f0567db38d1c66ea220daaf8a33f1275fcad
4
+ data.tar.gz: da6b5f0f2e7d7bef30ded0a45b17014e0adc623583b7221f67b6b44fc9ca3805
5
5
  SHA512:
6
- metadata.gz: 401d695bc0375db501df082733813925f229def58bea2b65f947c453b5b0f2989712b27c0a7d8f4d0f19c7e5f90293210a8c8755985a1584efae54bc35e90d2b
7
- data.tar.gz: 258f2466dab53c45d1541f4608af756d9cddf3a7af555d1c11e329ad7de3cb9ce5e52831347cb417e97bb4628a6563220d2fc486669597c38df0f0184bd6aa89
6
+ metadata.gz: e429973defa957eb444c1395c9d6c235052f7f365978d216e7fc4633ebbb7361242ce649845304cd96c47d5227e563d79021c41bd0d2529e9d9b4695edb09ae6
7
+ data.tar.gz: 4cc2e28b0309130bd8c524a6ab3c17018e9160db4251225ad88aa082cf1ab1226eabda6d0e1ba8691d7ee2c107e8c6b4ea661265422d045a3a27692ce4b5745a
@@ -1,5 +1,36 @@
1
1
  # Release History
2
2
 
3
+ ### 0.11.0 / 2020-08-21
4
+
5
+ * ADDED: The load path can be truncated using the `truncate_load_path!` directive.
6
+ * IMPROVED: Generated help for delegates now includes the information for the target tool, plus subtools of the delegate.
7
+ * IMPROVED: The `:bundler` mixin searches for `gems.rb` and `.gems.rb` in addition to `Gemfile`.
8
+ * IMPROVED: The `:budnler` mixin can load a specific Gemfile path.
9
+ * FIXED: The loader can now find data and lib directories at the root level of a Toys directory.
10
+ * FIXED: Exec::Result correctly reports processes that terminated due to signals.
11
+ * FIXED: Fixed a rare Exec capture failure that resulted from a race condition when closing streams.
12
+
13
+ ### 0.10.5 / 2020-07-18
14
+
15
+ * IMPROVED: The bundler mixin silences bundler output during bundle setup.
16
+ * IMPROVED: The bundler mixin allows toys and toys-core to be in the Gemfile. It checks their version requirements against the running Toys version, and either adds the corret version to the bundle or raises IncompatibleToysError.
17
+ * IMPROVED: The bundler mixin automatically updates the bundle if install fails (typically because a transitive dependency has been explicitly updated.)
18
+ * FIXED: Some cases of transitive dependency handling by the bundler mixin.
19
+ * FIXED: Fixed a crash when computing suggestions, when running with a bundle on Ruby 2.6 or earlier.
20
+
21
+ ### 0.10.4 / 2020-07-11
22
+
23
+ * IMPROVED: Bundler integration can now handle Toys itself being in the bundle, as long as the version requirements cover the running Toys version.
24
+ * IMPROVED: Passing `static: true` to the `:bundler` mixin installs the bundle at definition rather than execution time.
25
+
26
+ ### 0.10.3 / 2020-07-04
27
+
28
+ * FIXED: The `exec_separate_tool` method in the `:exec` mixin no longer throws ENOEXEC on Windows.
29
+
30
+ ### 0.10.2 / 2020-07-03
31
+
32
+ * FIXED: The load path no longer loses the toys and toys-core directories after a bundle install.
33
+
3
34
  ### 0.10.1 / 2020-03-07
4
35
 
5
36
  * FIXED: Setting `:exit_on_nonzero_status` explicitly to false now works as expected.
data/README.md CHANGED
@@ -258,7 +258,7 @@ itself. However, the `toys-core` gem is a dependency, and your users will need
258
258
  to have it installed. You could alleviate this by wrapping your executable in a
259
259
  gem that can declare `toys-core` as a dependency explicitly.
260
260
 
261
- The [examples directory](https://github.com/dazuma/toys/tree/master/toys-core/examples)
261
+ The [examples directory](https://github.com/dazuma/toys/tree/main/toys-core/examples)
262
262
  includes a few simple examples that you can use as a starting point.
263
263
 
264
264
  To experiment with the examples, clone the Toys repo from GitHub:
@@ -272,7 +272,7 @@ Navigate to the simple-gem example:
272
272
 
273
273
  This example wraps the simple "greet" executable that we
274
274
  [covered earlier](#Add_some_functionality) in a gem. You can see the
275
- [executable file](https://github.com/dazuma/toys/tree/master/toys-core/examples/simple-gem/bin/toys-core-simple-example)
275
+ [executable file](https://github.com/dazuma/toys/tree/main/toys-core/examples/simple-gem/bin/toys-core-simple-example)
276
276
  in the bin directory.
277
277
 
278
278
  Try it out by building and installing the gem. From the `examples/simple-gem`
@@ -297,16 +297,16 @@ break it up into multiple files. The multi-file gem example demonstrates this.
297
297
  $ cd ../multi-file-gem
298
298
 
299
299
  This executable's implementation resides in its
300
- [lib directory](https://github.com/dazuma/toys/tree/master/toys-core/examples/multi-file-gem/lib),
300
+ [lib directory](https://github.com/dazuma/toys/tree/main/toys-core/examples/multi-file-gem/lib),
301
301
  a technique that may be familiar to writers of command line executables. More
302
302
  interestingly, the tools themselves are no longer defined in a block passed to
303
303
  the CLI object, but have been moved into a separate
304
- ["tools" directory](https://github.com/dazuma/toys/tree/master/toys-core/examples/multi-file-gem/tools).
304
+ ["tools" directory](https://github.com/dazuma/toys/tree/main/toys-core/examples/multi-file-gem/tools).
305
305
  This directory has the same structure and supports the same features that are
306
306
  available when writing complex sets of tools in a `.toys` directory. You then
307
307
  configure the CLI object to look in this directory for its tools definitions,
308
308
  as you can see in
309
- [the code](https://github.com/dazuma/toys/tree/master/toys-core/examples/multi-file-gem/lib/toys-core-multi-gem-example.rb).
309
+ [the code](https://github.com/dazuma/toys/tree/main/toys-core/examples/multi-file-gem/lib/toys-core-multi-gem-example.rb).
310
310
 
311
311
  Try it out now. From the `examples/multi-file-gem` directory, run:
312
312
 
@@ -1,4 +1,6 @@
1
+ <!--
1
2
  # @title Toys-Core User Guide
3
+ -->
2
4
 
3
5
  # Toys-Core User Guide
4
6
 
@@ -58,6 +58,9 @@ module Toys
58
58
  #
59
59
  attr_accessor :executable_path
60
60
  end
61
+
62
+ # @private
63
+ CORE_LIB_PATH = __dir__
61
64
  end
62
65
 
63
66
  require "toys/acceptor"
@@ -16,21 +16,30 @@ module Toys
16
16
  ::RUBY_PLATFORM == "java"
17
17
  end
18
18
 
19
+ # @private
20
+ def self.windows?
21
+ ::RbConfig::CONFIG["host_os"] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
22
+ end
23
+
19
24
  # @private
20
25
  def self.allow_fork?
21
- !jruby? && ::RbConfig::CONFIG["host_os"] !~ /mswin/
26
+ !jruby? && !windows?
22
27
  end
23
28
 
24
29
  # @private
25
30
  def self.supports_suggestions?
26
31
  unless defined?(@supports_suggestions)
27
- require "rubygems"
28
32
  begin
29
33
  require "did_you_mean"
30
- @supports_suggestions = defined?(::DidYouMean::SpellChecker)
31
34
  rescue ::LoadError
32
- @supports_suggestions = false
35
+ require "rubygems"
36
+ begin
37
+ require "did_you_mean"
38
+ rescue ::LoadError
39
+ # Oh well, it's not available
40
+ end
33
41
  end
42
+ @supports_suggestions = defined?(::DidYouMean::SpellChecker)
34
43
  end
35
44
  @supports_suggestions
36
45
  end
@@ -9,7 +9,7 @@ module Toys
9
9
  # Current version of Toys core.
10
10
  # @return [String]
11
11
  #
12
- VERSION = "0.10.1"
12
+ VERSION = "0.11.0"
13
13
  end
14
14
 
15
15
  ## @private deprecated
@@ -1619,6 +1619,23 @@ module Toys
1619
1619
  self
1620
1620
  end
1621
1621
 
1622
+ ##
1623
+ # Remove lower-priority sources from the load path. This prevents lower-
1624
+ # priority sources (such as Toys files from parent or global directories)
1625
+ # from executing or defining tools.
1626
+ #
1627
+ # This works only if no such sources have already loaded yet.
1628
+ #
1629
+ # @raise [Toys::ToolDefinitionError] if any lower-priority tools have
1630
+ # already been loaded.
1631
+ #
1632
+ def truncate_load_path!
1633
+ unless @__loader.stop_loading_at_priority(@__priority)
1634
+ raise ToolDefinitionError,
1635
+ "Cannot truncate load path because tools have already been loaded"
1636
+ end
1637
+ end
1638
+
1622
1639
  ##
1623
1640
  # Determines whether the current Toys version satisfies the given
1624
1641
  # requirements.
@@ -8,6 +8,9 @@ module Toys
8
8
  # appropriate tool given a set of command line arguments.
9
9
  #
10
10
  class Loader
11
+ # @private
12
+ BASE_PRIORITY = -999_999
13
+
11
14
  ##
12
15
  # Create a Loader
13
16
  #
@@ -69,9 +72,11 @@ module Toys
69
72
  @worklist = []
70
73
  @tool_data = {}
71
74
  @max_priority = @min_priority = 0
75
+ @stop_priority = BASE_PRIORITY
76
+ @min_loaded_priority = 999_999
72
77
  @middleware_stack = Middleware.stack(middleware_stack)
73
78
  @delimiter_handler = DelimiterHandler.new(extra_delimiters)
74
- get_tool([], -999_999)
79
+ get_tool([], BASE_PRIORITY)
75
80
  end
76
81
 
77
82
  ##
@@ -89,7 +94,7 @@ module Toys
89
94
  raise "Cannot add a path after tool loading has started" if @loading_started
90
95
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
91
96
  paths.each do |path|
92
- source = SourceInfo.create_path_root(path)
97
+ source = SourceInfo.create_path_root(path, @data_dir_name, @lib_dir_name)
93
98
  @worklist << [source, [], priority]
94
99
  end
95
100
  end
@@ -114,7 +119,7 @@ module Toys
114
119
  @mutex.synchronize do
115
120
  raise "Cannot add a block after tool loading has started" if @loading_started
116
121
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
117
- source = SourceInfo.create_proc_root(block, name)
122
+ source = SourceInfo.create_proc_root(block, name, @data_dir_name, @lib_dir_name)
118
123
  @worklist << [source, [], priority]
119
124
  end
120
125
  self
@@ -178,15 +183,15 @@ module Toys
178
183
  load_for_prefix(words)
179
184
  found_tools = []
180
185
  len = words.length
181
- tool_data_snapshot.each do |n, td|
182
- next if n.empty?
186
+ all_cur_definitions.each do |tool|
187
+ name = tool.full_name
188
+ next if name.empty?
183
189
  if recursive
184
- next if n.length <= len || n.slice(0, len) != words
190
+ next if name.length <= len || name.slice(0, len) != words
185
191
  else
186
- next unless n.slice(0..-2) == words
192
+ next unless name.slice(0..-2) == words
187
193
  end
188
- tool = td.cur_definition
189
- found_tools << tool unless tool.nil?
194
+ found_tools << tool
190
195
  end
191
196
  sort_tools_by_name(found_tools)
192
197
  include_hidden ? found_tools : filter_hidden_subtools(found_tools)
@@ -202,8 +207,9 @@ module Toys
202
207
  def has_subtools?(words) # rubocop:disable Naming/PredicateName
203
208
  load_for_prefix(words)
204
209
  len = words.length
205
- tool_data_snapshot.each do |n, td|
206
- if !n.empty? && n.length > len && n.slice(0, len) == words && !td.empty?
210
+ all_cur_definitions.each do |tool|
211
+ name = tool.full_name
212
+ if !name.empty? && name.length > len && name.slice(0, len) == words
207
213
  return true
208
214
  end
209
215
  end
@@ -267,6 +273,20 @@ module Toys
267
273
  Tool.new(self, parent, words, priority, middleware_stack, @middleware_lookup)
268
274
  end
269
275
 
276
+ ##
277
+ # Stop search at the given priority. Returns true if successful.
278
+ # Called only from the DSL.
279
+ #
280
+ # @private
281
+ #
282
+ def stop_loading_at_priority(priority)
283
+ @mutex.synchronize do
284
+ return false if priority > @min_loaded_priority || priority < @stop_priority
285
+ @stop_priority = priority
286
+ true
287
+ end
288
+ end
289
+
270
290
  ##
271
291
  # Loads the subtree under the given prefix.
272
292
  #
@@ -278,6 +298,7 @@ module Toys
278
298
  cur_worklist = @worklist
279
299
  @worklist = []
280
300
  cur_worklist.each do |source, words, priority|
301
+ next if priority < @stop_priority
281
302
  remaining_words = calc_remaining_words(prefix, words)
282
303
  if source.source_proc
283
304
  load_proc(source, words, remaining_words, priority)
@@ -349,8 +370,15 @@ module Toys
349
370
 
350
371
  private
351
372
 
352
- def tool_data_snapshot
353
- @mutex.synchronize { @tool_data.dup }
373
+ def all_cur_definitions
374
+ result = []
375
+ @mutex.synchronize do
376
+ @tool_data.map do |_name, td|
377
+ tool = td.cur_definition
378
+ result << tool unless tool.nil?
379
+ end
380
+ end
381
+ result
354
382
  end
355
383
 
356
384
  def get_tool_data(words)
@@ -364,14 +392,16 @@ module Toys
364
392
  def finish_definitions_in_tree(words)
365
393
  load_for_prefix(words)
366
394
  len = words.length
367
- tool_data_snapshot.each do |n, td|
368
- next if n.length < len || n.slice(0, len) != words
369
- td.cur_definition&.finish_definition(self)
395
+ all_cur_definitions.each do |tool|
396
+ name = tool.full_name
397
+ next if name.length < len || name.slice(0, len) != words
398
+ tool.finish_definition(self)
370
399
  end
371
400
  end
372
401
 
373
402
  def load_proc(source, words, remaining_words, priority)
374
403
  if remaining_words
404
+ update_min_loaded_priority(priority)
375
405
  tool_class = get_tool(words, priority).tool_class
376
406
  DSL::Tool.prepare(tool_class, remaining_words, source) do
377
407
  ContextualError.capture("Error while loading Toys config!") do
@@ -393,6 +423,7 @@ module Toys
393
423
 
394
424
  def load_relevant_path(source, words, remaining_words, priority)
395
425
  if source.source_type == :file
426
+ update_min_loaded_priority(priority)
396
427
  tool_class = get_tool(words, priority).tool_class
397
428
  InputFile.evaluate(tool_class, remaining_words, source)
398
429
  else
@@ -406,7 +437,7 @@ module Toys
406
437
 
407
438
  def load_index_in(source, words, remaining_words, priority)
408
439
  return unless @index_file_name
409
- index_source = source.relative_child(@index_file_name, @data_dir_name, @lib_dir_name)
440
+ index_source = source.relative_child(@index_file_name)
410
441
  load_relevant_path(index_source, words, remaining_words, priority) if index_source
411
442
  end
412
443
 
@@ -414,7 +445,7 @@ module Toys
414
445
  return if child.start_with?(".") || child == @index_file_name ||
415
446
  child == @preload_file_name || child == @preload_dir_name ||
416
447
  child == @data_dir_name || child == @lib_dir_name
417
- child_source = source.relative_child(child, @data_dir_name, @lib_dir_name)
448
+ child_source = source.relative_child(child)
418
449
  return unless child_source
419
450
  child_word = ::File.basename(child, ".rb")
420
451
  next_words = words + [child_word]
@@ -422,6 +453,10 @@ module Toys
422
453
  load_validated_path(child_source, next_words, next_remaining, priority)
423
454
  end
424
455
 
456
+ def update_min_loaded_priority(priority)
457
+ @min_loaded_priority = priority if @min_loaded_priority > priority
458
+ end
459
+
425
460
  def do_preload(path)
426
461
  if @preload_file_name
427
462
  preload_file = ::File.join(path, @preload_file_name)
@@ -483,37 +518,42 @@ module Toys
483
518
  # @private
484
519
  #
485
520
  class ToolData
486
- ## @private
521
+ # @private
487
522
  def initialize(words)
488
523
  @words = words
489
524
  @definitions = {}
490
525
  @top_priority = @active_priority = nil
526
+ @mutex = ::Monitor.new
491
527
  end
492
528
 
493
- ## @private
529
+ # @private
494
530
  def cur_definition
495
- active_definition || top_definition
531
+ @mutex.synchronize { active_definition || top_definition }
496
532
  end
497
533
 
498
- ## @private
534
+ # @private
499
535
  def empty?
500
536
  @definitions.empty?
501
537
  end
502
538
 
503
- ## @private
539
+ # @private
504
540
  def get_tool(priority, loader)
505
- if @top_priority.nil? || @top_priority < priority
506
- @top_priority = priority
541
+ @mutex.synchronize do
542
+ if @top_priority.nil? || @top_priority < priority
543
+ @top_priority = priority
544
+ end
545
+ @definitions[priority] ||= loader.build_tool(@words, priority)
507
546
  end
508
- @definitions[priority] ||= loader.build_tool(@words, priority)
509
547
  end
510
548
 
511
- ## @private
549
+ # @private
512
550
  def activate_tool(priority, loader)
513
- return active_definition if @active_priority == priority
514
- return nil if @active_priority && @active_priority > priority
515
- @active_priority = priority
516
- get_tool(priority, loader)
551
+ @mutex.synchronize do
552
+ return active_definition if @active_priority == priority
553
+ return nil if @active_priority && @active_priority > priority
554
+ @active_priority = priority
555
+ get_tool(priority, loader)
556
+ end
517
557
  end
518
558
 
519
559
  private
@@ -10,7 +10,7 @@ module Toys
10
10
  # @private
11
11
  #
12
12
  def initialize(parent, context_directory, source_type, source_path, source_proc,
13
- source_name, data_dir, lib_dir)
13
+ source_name, data_dir_name, lib_dir_name)
14
14
  @parent = parent
15
15
  @context_directory = context_directory
16
16
  @source_type = source_type
@@ -18,8 +18,10 @@ module Toys
18
18
  @source_path = source_path
19
19
  @source_proc = source_proc
20
20
  @source_name = source_name
21
- @data_dir = data_dir
22
- @lib_dir = lib_dir
21
+ @data_dir_name = data_dir_name
22
+ @lib_dir_name = lib_dir_name
23
+ @data_dir = find_special_dir(data_dir_name)
24
+ @lib_dir = find_special_dir(lib_dir_name)
23
25
  end
24
26
 
25
27
  ##
@@ -118,15 +120,13 @@ module Toys
118
120
  # Create a child SourceInfo relative to the parent path.
119
121
  # @private
120
122
  #
121
- def relative_child(filename, data_dir_name, lib_dir_name)
122
- raise "no parent path for relative_child" unless source_path
123
+ def relative_child(filename)
124
+ raise "relative_child is valid only on a directory source" unless source_type == :directory
123
125
  child_path = ::File.join(source_path, filename)
124
126
  child_path, type = SourceInfo.check_path(child_path, true)
125
127
  return nil unless child_path
126
- data_dir = SourceInfo.find_special_dir(type, child_path, data_dir_name)
127
- lib_dir = SourceInfo.find_special_dir(type, child_path, lib_dir_name)
128
- SourceInfo.new(self, context_directory, type, child_path, source_proc, child_path,
129
- data_dir, lib_dir)
128
+ SourceInfo.new(self, context_directory, type, child_path, nil, child_path,
129
+ @data_dir_name, @lib_dir_name)
130
130
  end
131
131
 
132
132
  ##
@@ -135,7 +135,8 @@ module Toys
135
135
  #
136
136
  def absolute_child(child_path)
137
137
  child_path, type = SourceInfo.check_path(child_path, false)
138
- SourceInfo.new(self, context_directory, type, child_path, source_proc, child_path, nil, nil)
138
+ SourceInfo.new(self, context_directory, type, child_path, nil, child_path,
139
+ @data_dir_name, @lib_dir_name)
139
140
  end
140
141
 
141
142
  ##
@@ -144,25 +145,26 @@ module Toys
144
145
  #
145
146
  def proc_child(child_proc, source_name = nil)
146
147
  source_name ||= self.source_name
147
- SourceInfo.new(self, context_directory, :proc, source_path, child_proc, source_name, nil, nil)
148
+ SourceInfo.new(self, context_directory, :proc, source_path, child_proc, source_name,
149
+ @data_dir_name, @lib_dir_name)
148
150
  end
149
151
 
150
152
  ##
151
153
  # Create a root source info for a file path.
152
154
  # @private
153
155
  #
154
- def self.create_path_root(source_path)
156
+ def self.create_path_root(source_path, data_dir_name, lib_dir_name)
155
157
  source_path, type = check_path(source_path, false)
156
158
  context_directory = ::File.dirname(source_path)
157
- new(nil, context_directory, type, source_path, nil, source_path, nil, nil)
159
+ new(nil, context_directory, type, source_path, nil, source_path, data_dir_name, lib_dir_name)
158
160
  end
159
161
 
160
162
  ##
161
163
  # Create a root source info for a proc.
162
164
  # @private
163
165
  #
164
- def self.create_proc_root(source_proc, source_name)
165
- new(nil, nil, :proc, nil, source_proc, source_name, nil, nil)
166
+ def self.create_proc_root(source_proc, source_name, data_dir_name, lib_dir_name)
167
+ new(nil, nil, :proc, nil, source_proc, source_name, data_dir_name, lib_dir_name)
166
168
  end
167
169
 
168
170
  ##
@@ -189,14 +191,11 @@ module Toys
189
191
  end
190
192
  end
191
193
 
192
- ##
193
- # Determine the data directory path, if any.
194
- # @private
195
- #
196
- def self.find_special_dir(type, source_path, dir_name)
197
- return nil if source_path.nil? || dir_name.nil?
198
- source_path = ::File.dirname(source_path) if type == :file
199
- dir = ::File.join(source_path, dir_name)
194
+ private
195
+
196
+ def find_special_dir(dir_name)
197
+ return nil if @source_type != :directory || dir_name.nil?
198
+ dir = ::File.join(@source_path, dir_name)
200
199
  dir if ::File.directory?(dir) && ::File.readable?(dir)
201
200
  end
202
201
  end