toys-core 0.11.2 → 0.12.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/README.md +1 -1
  4. data/lib/toys-core.rb +4 -1
  5. data/lib/toys/acceptor.rb +3 -3
  6. data/lib/toys/arg_parser.rb +6 -7
  7. data/lib/toys/cli.rb +44 -14
  8. data/lib/toys/compat.rb +19 -22
  9. data/lib/toys/completion.rb +3 -1
  10. data/lib/toys/context.rb +2 -2
  11. data/lib/toys/core.rb +1 -1
  12. data/lib/toys/dsl/base.rb +85 -0
  13. data/lib/toys/dsl/flag.rb +3 -3
  14. data/lib/toys/dsl/flag_group.rb +7 -7
  15. data/lib/toys/dsl/internal.rb +206 -0
  16. data/lib/toys/dsl/positional_arg.rb +3 -3
  17. data/lib/toys/dsl/tool.rb +174 -216
  18. data/lib/toys/errors.rb +1 -0
  19. data/lib/toys/flag.rb +15 -18
  20. data/lib/toys/flag_group.rb +5 -4
  21. data/lib/toys/input_file.rb +4 -4
  22. data/lib/toys/loader.rb +189 -50
  23. data/lib/toys/middleware.rb +1 -1
  24. data/lib/toys/mixin.rb +2 -2
  25. data/lib/toys/positional_arg.rb +3 -3
  26. data/lib/toys/settings.rb +900 -0
  27. data/lib/toys/source_info.rb +121 -18
  28. data/lib/toys/standard_middleware/apply_config.rb +5 -4
  29. data/lib/toys/standard_middleware/set_default_descriptions.rb +18 -18
  30. data/lib/toys/standard_middleware/show_help.rb +17 -5
  31. data/lib/toys/standard_mixins/bundler.rb +5 -1
  32. data/lib/toys/standard_mixins/exec.rb +22 -15
  33. data/lib/toys/standard_mixins/git_cache.rb +48 -0
  34. data/lib/toys/standard_mixins/xdg.rb +56 -0
  35. data/lib/toys/template.rb +2 -2
  36. data/lib/toys/{tool.rb → tool_definition.rb} +100 -41
  37. data/lib/toys/utils/exec.rb +37 -16
  38. data/lib/toys/utils/gems.rb +48 -14
  39. data/lib/toys/utils/git_cache.rb +184 -0
  40. data/lib/toys/utils/help_text.rb +90 -34
  41. data/lib/toys/utils/terminal.rb +1 -1
  42. data/lib/toys/utils/xdg.rb +293 -0
  43. metadata +15 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37117af06be544465a07f95706310e3b8de1ad4dbc58f53e113b2595eed70e47
4
- data.tar.gz: 6e27a716c74435ff2060838dbde56af35b4df93dcdc671051397755f478cc233
3
+ metadata.gz: 2e6f3ce99b43220466c4b43842c1fa78d3cfca815ecc559be749518d26156be9
4
+ data.tar.gz: 8a3dbb068001b3a52d5b070ae0f8652aeea5f1763ae19a938aa84da693023c2c
5
5
  SHA512:
6
- metadata.gz: a9dc2dc2e8287fbdb1c7695ed9111360d7bfcfc17167c6b790b170843988acb198daa8d99050d910ee5ea41876b2bb6c2e22cbab9629e322c9e7687f90a34c99
7
- data.tar.gz: '03283300b1140b477ae56276a066953eb4fc6c71aa2a14b2e0fbe66cc9934c3afbd30862349ee922d98c4e0b9e80e74e50ba180bddc939dec99ead630eb7ddf2'
6
+ metadata.gz: de8dc024af7e706749b65a8e2b4fe9430e380b58add4bc9b643bc357dce7d5a5109057a2aaccdbfb657410def1372494b020cab59b1ebd03de83d2359a0746ab
7
+ data.tar.gz: 96b6ca463c27eff9fe403d365afe15a6d4c75bed057b8dc162b40c696b0f31606738197c5bdb68ed759d9c0a0f29af0e4ddd4aed929e41649cb6a46b67909ea8
data/CHANGELOG.md CHANGED
@@ -1,8 +1,53 @@
1
1
  # Release History
2
2
 
3
+ ### v0.12.0 / 2021-08-05
4
+
5
+ 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.
6
+
7
+ Breaking interface changes:
8
+
9
+ * The Toys::Tool class has been renamed Toys::ToolDefinition so that the old name can be used for class-based tool definition.
10
+ * Tool definition now raises ToolDefinitionError if whitespace, control characters, or certain punctuation are used in a tool name.
11
+ * Toys::Loader#add_path no longer supports multiple paths. Use add_path_set instead.
12
+ * The "name" argument was renamed to "source_name" in Toys::Loader#add_block and Toys::CLI#add_config_block
13
+
14
+ New functionality:
15
+
16
+ * 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.
17
+ * You can now load tools from a remote git repository using the load_git directive.
18
+ * Whitespace is now automatically considered a name delimiter when defining tools.
19
+ * 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.
20
+ * The load directive can load into a new tool.
21
+ * Added a new utility class and mixin that provides XDG Base Directory information.
22
+ * Added a new utility class and mixin that provides cached access to remote git repos.
23
+ * The help text generator now supports splitting the subtool list by source.
24
+ * Loader and CLI methods that add tool configs now uniformly provide optional source_name and context_directory arguments.
25
+ * Toys::SourceInfo now supports getting the root ancestor and priority of a source.
26
+ * 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.
27
+
28
+ Fixes:
29
+
30
+ * Fixed some bundler integration issues that occurred when the bundle is being installed in a separate path such as a vendor directory.
31
+ * Toys::ContextualError now includes the full backtrace of the cause.
32
+ * Cleaned up some unused memory objects during tool loading and lookup.
33
+
34
+ ### v0.11.5 / 2021-03-28
35
+
36
+ * BREAKING CHANGE: The exit_on_nonzero_status option to exec now exits on signals and failures to spawn, in addition to error codes.
37
+ * ADDED: Support retries in the bundler integration.
38
+ * FIXED: Fix a bundler 2.2 integration issue that fails install in certain cases when an update is needed.
39
+
40
+ ### v0.11.4 / 2020-10-11
41
+
42
+ * FIXED: Doesn't modify bundler lockfiles when adding Toys to a bundle
43
+
44
+ ### v0.11.3 / 2020-09-13
45
+
46
+ * FIXED: The Exec library recognizes the argv0 option, and logs it appropriately
47
+
3
48
  ### v0.11.2 / 2020-09-06
4
49
 
5
- * FIXED: Fix a JRuby-specific race condition when capturing exec streams
50
+ * FIXED: Fix a JRuby-specific race condition when capturing exec streams
6
51
 
7
52
  ### v0.11.1 / 2020-08-24
8
53
 
data/README.md CHANGED
@@ -34,7 +34,7 @@ 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.
data/lib/toys-core.rb CHANGED
@@ -70,8 +70,10 @@ require "toys/compat"
70
70
  require "toys/completion"
71
71
  require "toys/context"
72
72
  require "toys/core"
73
+ require "toys/dsl/base"
73
74
  require "toys/dsl/flag"
74
75
  require "toys/dsl/flag_group"
76
+ require "toys/dsl/internal"
75
77
  require "toys/dsl/positional_arg"
76
78
  require "toys/dsl/tool"
77
79
  require "toys/errors"
@@ -83,7 +85,8 @@ require "toys/middleware"
83
85
  require "toys/mixin"
84
86
  require "toys/module_lookup"
85
87
  require "toys/positional_arg"
88
+ require "toys/settings"
86
89
  require "toys/source_info"
87
90
  require "toys/template"
88
- require "toys/tool"
91
+ require "toys/tool_definition"
89
92
  require "toys/wrappable_string"
data/lib/toys/acceptor.rb CHANGED
@@ -602,11 +602,11 @@ module Toys
602
602
  Simple.new(type_desc: "boolean", well_known_spec: spec) do |s|
603
603
  if s.nil?
604
604
  default
605
+ elsif s.empty?
606
+ REJECT
605
607
  else
606
608
  s = s.downcase
607
- if s.empty?
608
- REJECT
609
- elsif TRUE_STRINGS.any? { |t| t.start_with?(s) }
609
+ if TRUE_STRINGS.any? { |t| t.start_with?(s) }
610
610
  true
611
611
  elsif FALSE_STRINGS.any? { |f| f.start_with?(s) }
612
612
  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
 
@@ -429,7 +429,7 @@ module Toys
429
429
  Context::Key::TOOL_NAME => tool.full_name,
430
430
  Context::Key::USAGE_ERRORS => [],
431
431
  }
432
- Compat.merge_clones(data, tool.default_data)
432
+ tool.default_data.each { |k, v| data[k] = v.clone }
433
433
  default_data.each { |k, v| data[k] ||= v }
434
434
  data
435
435
  end
@@ -450,7 +450,7 @@ module Toys
450
450
  case arg
451
451
  when "--"
452
452
  @flags_allowed = false
453
- when /\A(--\w[\?\w-]*)=(.*)\z/
453
+ when /\A(--\w[?\w-]*)=(.*)\z/
454
454
  handle_valued_flag(::Regexp.last_match(1), ::Regexp.last_match(2))
455
455
  when /\A--.+\z/
456
456
  handle_plain_flag(arg)
@@ -474,8 +474,7 @@ module Toys
474
474
  return "" unless flag_def
475
475
  @seen_flag_keys << flag_def.key
476
476
  if flag_def.flag_type == :boolean
477
- add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?,
478
- :flag, name)
477
+ add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?, :flag, name)
479
478
  elsif following.empty?
480
479
  if flag_def.value_type == :required || flag_result.unique_flag_syntax.value_delim == " "
481
480
  @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}.
@@ -321,10 +321,22 @@ module Toys
321
321
  # a Toys directory.
322
322
  # @param high_priority [Boolean] Add the config at the head of the priority
323
323
  # list rather than the tail.
324
+ # @param source_name [String] A custom name for the root source. Optional.
325
+ # @param context_directory [String,nil,:path,:parent] The context directory
326
+ # for tools loaded from this path. You can pass a directory path as a
327
+ # string, `:path` to denote the given path, `:parent` to denote the
328
+ # given path's parent directory, or `nil` to denote no context.
329
+ # Defaults to `:parent`.
324
330
  # @return [self]
325
331
  #
326
- def add_config_path(path, high_priority: false)
327
- @loader.add_path(path, high_priority: high_priority)
332
+ def add_config_path(path,
333
+ high_priority: false,
334
+ source_name: nil,
335
+ context_directory: :parent)
336
+ @loader.add_path(path,
337
+ high_priority: high_priority,
338
+ source_name: source_name,
339
+ context_directory: context_directory)
328
340
  self
329
341
  end
330
342
 
@@ -336,15 +348,24 @@ module Toys
336
348
  #
337
349
  # @param high_priority [Boolean] Add the config at the head of the priority
338
350
  # 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.
351
+ # @param source_name [String] The source name that will be shown in
352
+ # documentation for tools defined in this block. If omitted, a default
353
+ # unique string will be generated.
342
354
  # @param block [Proc] The block of configuration, executed in the context
343
355
  # of the tool DSL {Toys::DSL::Tool}.
356
+ # @param context_directory [String,nil] The context directory for tools
357
+ # loaded from this block. You can pass a directory path as a string, or
358
+ # `nil` to denote no context. Defaults to `nil`.
344
359
  # @return [self]
345
360
  #
346
- def add_config_block(high_priority: false, name: nil, &block)
347
- @loader.add_block(high_priority: high_priority, name: name, &block)
361
+ def add_config_block(high_priority: false,
362
+ source_name: nil,
363
+ context_directory: nil,
364
+ &block)
365
+ @loader.add_block(high_priority: high_priority,
366
+ source_name: source_name,
367
+ context_directory: context_directory,
368
+ &block)
348
369
  self
349
370
  end
350
371
 
@@ -358,19 +379,28 @@ module Toys
358
379
  # @param search_path [String] A path to search for configs.
359
380
  # @param high_priority [Boolean] Add the configs at the head of the
360
381
  # priority list rather than the tail.
382
+ # @param context_directory [String,nil,:path,:parent] The context directory
383
+ # for tools loaded from this path. You can pass a directory path as a
384
+ # string, `:path` to denote the given path, `:parent` to denote the
385
+ # given path's parent directory, or `nil` to denote no context.
386
+ # Defaults to `:path`.
361
387
  # @return [self]
362
388
  #
363
- def add_search_path(search_path, high_priority: false)
389
+ def add_search_path(search_path,
390
+ high_priority: false,
391
+ context_directory: :path)
364
392
  paths = []
365
393
  if @config_file_name
366
394
  file_path = ::File.join(search_path, @config_file_name)
367
- paths << file_path if !::File.directory?(file_path) && ::File.readable?(file_path)
395
+ paths << @config_file_name if !::File.directory?(file_path) && ::File.readable?(file_path)
368
396
  end
369
397
  if @config_dir_name
370
398
  dir_path = ::File.join(search_path, @config_dir_name)
371
- paths << dir_path if ::File.directory?(dir_path) && ::File.readable?(dir_path)
399
+ paths << @config_dir_name if ::File.directory?(dir_path) && ::File.readable?(dir_path)
372
400
  end
373
- @loader.add_path(paths, high_priority: high_priority)
401
+ @loader.add_path_set(search_path, paths,
402
+ high_priority: high_priority,
403
+ context_directory: context_directory)
374
404
  self
375
405
  end
376
406
 
@@ -443,7 +473,7 @@ module Toys
443
473
  # Run the given tool with the given arguments.
444
474
  # Does not handle exceptions.
445
475
  #
446
- # @param tool [Toys::Tool] The tool to run.
476
+ # @param tool [Toys::ToolDefinition] The tool to run.
447
477
  # @param args [Array<String>] Command line arguments passed to the tool.
448
478
  # @param default_data [Hash] Initial tool context data.
449
479
  # @return [Integer] The resulting status code
data/lib/toys/compat.rb CHANGED
@@ -53,28 +53,6 @@ module Toys
53
53
  end
54
54
  end
55
55
 
56
- # In Ruby < 2.4, some objects such as nil cannot be cloned.
57
- if ruby_version >= 20400
58
- # @private
59
- def self.merge_clones(hash, orig)
60
- orig.each { |k, v| hash[k] = v.clone }
61
- hash
62
- end
63
- else
64
- # @private
65
- def self.merge_clones(hash, orig)
66
- orig.each do |k, v|
67
- hash[k] =
68
- begin
69
- v.clone
70
- rescue ::TypeError
71
- v
72
- end
73
- end
74
- hash
75
- end
76
- end
77
-
78
56
  # The :base argument to Dir.glob requires Ruby 2.5 or later.
79
57
  if ruby_version >= 20500
80
58
  # @private
@@ -106,5 +84,24 @@ module Toys
106
84
  end
107
85
  end
108
86
  end
87
+
88
+ # File.absolute_path? requires Ruby 2.7 or later. For earlier Rubies, use
89
+ # an ad-hoc mechanism.
90
+ if ruby_version >= 20700
91
+ # @private
92
+ def self.absolute_path?(path)
93
+ ::File.absolute_path?(path)
94
+ end
95
+ elsif ::Dir.getwd =~ /^[a-zA-Z]:/
96
+ # @private
97
+ def self.absolute_path?(path)
98
+ /^[a-zA-Z]:/.match?(path)
99
+ end
100
+ else
101
+ # @private
102
+ def self.absolute_path?(path)
103
+ path.start_with?("/")
104
+ end
105
+ end
109
106
  end
110
107
  end
@@ -88,7 +88,7 @@ module Toys
88
88
 
89
89
  ##
90
90
  # The tool being invoked, which should control the completion.
91
- # @return [Toys::Tool]
91
+ # @return [Toys::ToolDefinition]
92
92
  #
93
93
  def tool
94
94
  lookup_tool
@@ -236,6 +236,7 @@ module Toys
236
236
  # prefix. Defaults to requiring the prefix be empty.
237
237
  #
238
238
  def initialize(cwd: nil, omit_files: false, omit_directories: false, prefix_constraint: "")
239
+ super()
239
240
  @cwd = cwd || ::Dir.pwd
240
241
  @include_files = !omit_files
241
242
  @include_directories = !omit_directories
@@ -328,6 +329,7 @@ module Toys
328
329
  # prefix. Defaults to requiring the prefix be empty.
329
330
  #
330
331
  def initialize(values, prefix_constraint: "")
332
+ super()
331
333
  @values = values.flatten.map { |v| Candidate.new(v) }.sort
332
334
  @prefix_constraint = prefix_constraint
333
335
  end
data/lib/toys/context.rb CHANGED
@@ -33,7 +33,7 @@ module Toys
33
33
  # This module is mixed into the runtime context. This means you can
34
34
  # reference any of these constants directly from your run method.
35
35
  #
36
- # ## Example
36
+ # ### Example
37
37
  #
38
38
  # tool "my-name" do
39
39
  # def run
@@ -78,7 +78,7 @@ module Toys
78
78
  LOGGER = ::Object.new.freeze
79
79
 
80
80
  ##
81
- # Context key for the {Toys::Tool} object being executed.
81
+ # Context key for the {Toys::ToolDefinition} object being executed.
82
82
  # @return [Object]
83
83
  #
84
84
  TOOL = ::Object.new.freeze
data/lib/toys/core.rb CHANGED
@@ -9,7 +9,7 @@ module Toys
9
9
  # Current version of Toys core.
10
10
  # @return [String]
11
11
  #
12
- VERSION = "0.11.2"
12
+ VERSION = "0.12.0"
13
13
  end
14
14
 
15
15
  ## @private deprecated
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Create a base class for defining a tool with a given name.
5
+ #
6
+ # This method returns a base class for defining a tool with a given name.
7
+ # This is useful if the naming behavior of {Toys::Tool} is not adequate for
8
+ # your tool.
9
+ #
10
+ # ### Example
11
+ #
12
+ # class FooBar < Toys.Tool("Foo_Bar")
13
+ # desc "This is a tool called Foo_Bar"
14
+ #
15
+ # def run
16
+ # puts "Foo_Bar called"
17
+ # end
18
+ # end
19
+ #
20
+ # @param name [String] Name of the tool. Defaults to a name inferred from the
21
+ # class name. (See {Toys::Tool}.)
22
+ # @param base [Class] Use this tool class as the base class, and inherit helper
23
+ # methods from it.
24
+ # @param args [String,Class] Any string-valued positional argument is
25
+ # interpreted as the name. Any class-valued positional argument is
26
+ # interpreted as the base class.
27
+ #
28
+ def Toys.Tool(*args, name: nil, base: nil) # rubocop:disable Naming/MethodName
29
+ args.each do |arg|
30
+ case arg
31
+ when ::Class
32
+ raise ::ArgumentError, "Both base keyword argument and class-valud argument received" if base
33
+ base = arg
34
+ when ::String, ::Symbol
35
+ raise ::ArgumentError, "Both name keyword argument and string-valud argument received" if name
36
+ name = arg
37
+ else
38
+ raise ::ArgumentError, "Unrecognized argument: #{arg}"
39
+ end
40
+ end
41
+ if base && !base.ancestors.include?(::Toys::Context)
42
+ raise ::ArgumentError, "Base class must itself be a tool"
43
+ end
44
+ return base || ::Toys::Tool if name.nil?
45
+ ::Class.new(base || ::Toys::Context) do
46
+ base_class = self
47
+ define_singleton_method(:inherited) do |tool_class|
48
+ ::Toys::DSL::Internal.configure_class(tool_class, base_class == self ? name.to_s : nil)
49
+ super(tool_class)
50
+ ::Toys::DSL::Internal.setup_class_dsl(tool_class)
51
+ end
52
+ end
53
+ end
54
+
55
+ module Toys
56
+ ##
57
+ # Base class for defining tools
58
+ #
59
+ # This base class provides an alternative to the {Toys::DSL::Tool#tool}
60
+ # directive for defining tools in the Toys DSL. Creating a subclass of
61
+ # `Toys::Tool` will create a tool whose name is the "kebab-case" of the class
62
+ # name. Subclasses can be created only in the context of a tool configuration
63
+ # DSL. Furthermore, a class-defined tool can be created only at the top level
64
+ # of a configuration file, or within another class-defined tool. It cannot
65
+ # be a subtool of a tool block.
66
+ #
67
+ # ### Example
68
+ #
69
+ # class FooBar < Toys::Tool
70
+ # desc "This is a tool called foo-bar"
71
+ #
72
+ # def run
73
+ # puts "foo-bar called"
74
+ # end
75
+ # end
76
+ #
77
+ class Tool < Context
78
+ # @private
79
+ def self.inherited(tool_class)
80
+ DSL::Internal.configure_class(tool_class)
81
+ super
82
+ DSL::Internal.setup_class_dsl(tool_class)
83
+ end
84
+ end
85
+ end