toys-core 0.11.5 → 0.13.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 (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
data/lib/toys/compat.rb CHANGED
@@ -5,28 +5,44 @@ require "rbconfig"
5
5
  module Toys
6
6
  ##
7
7
  # Compatibility wrappers for older Ruby versions.
8
+ #
8
9
  # @private
9
10
  #
10
11
  module Compat
11
12
  parts = ::RUBY_VERSION.split(".")
12
13
  ruby_version = parts[0].to_i * 10000 + parts[1].to_i * 100 + parts[2].to_i
13
14
 
15
+ ##
14
16
  # @private
17
+ #
15
18
  def self.jruby?
16
- ::RUBY_PLATFORM == "java"
19
+ ::RUBY_ENGINE == "jruby"
20
+ end
21
+
22
+ ##
23
+ # @private
24
+ #
25
+ def self.truffleruby?
26
+ ::RUBY_ENGINE == "truffleruby"
17
27
  end
18
28
 
29
+ ##
19
30
  # @private
31
+ #
20
32
  def self.windows?
21
33
  ::RbConfig::CONFIG["host_os"] =~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
22
34
  end
23
35
 
36
+ ##
24
37
  # @private
38
+ #
25
39
  def self.allow_fork?
26
- !jruby? && !windows?
40
+ !jruby? && !truffleruby? && !windows?
27
41
  end
28
42
 
43
+ ##
29
44
  # @private
45
+ #
30
46
  def self.supports_suggestions?
31
47
  unless defined?(@supports_suggestions)
32
48
  begin
@@ -44,7 +60,9 @@ module Toys
44
60
  @supports_suggestions
45
61
  end
46
62
 
63
+ ##
47
64
  # @private
65
+ #
48
66
  def self.suggestions(word, list)
49
67
  if supports_suggestions?
50
68
  ::DidYouMean::SpellChecker.new(dictionary: list).correct(word)
@@ -53,50 +71,55 @@ module Toys
53
71
  end
54
72
  end
55
73
 
56
- # In Ruby < 2.4, some objects such as nil cannot be cloned.
57
- if ruby_version >= 20400
74
+ # The :base argument to Dir.glob requires Ruby 2.5 or later.
75
+ if ruby_version >= 20500
76
+ ##
58
77
  # @private
59
- def self.merge_clones(hash, orig)
60
- orig.each { |k, v| hash[k] = v.clone }
61
- hash
78
+ #
79
+ def self.glob_in_dir(glob, dir)
80
+ ::Dir.glob(glob, base: dir)
62
81
  end
63
82
  else
83
+ ##
64
84
  # @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
85
+ #
86
+ def self.glob_in_dir(glob, dir)
87
+ ::Dir.chdir(dir) { ::Dir.glob(glob) }
75
88
  end
76
89
  end
77
90
 
78
- # The :base argument to Dir.glob requires Ruby 2.5 or later.
91
+ # Dir.children requires Ruby 2.5 or later.
79
92
  if ruby_version >= 20500
93
+ ##
80
94
  # @private
81
- def self.glob_in_dir(glob, dir)
82
- ::Dir.glob(glob, base: dir)
95
+ #
96
+ def self.dir_children(dir)
97
+ ::Dir.children(dir)
83
98
  end
84
99
  else
100
+ ##
85
101
  # @private
86
- def self.glob_in_dir(glob, dir)
87
- ::Dir.chdir(dir) { ::Dir.glob(glob) }
102
+ #
103
+ def self.dir_children(dir)
104
+ ::Dir.entries(dir) - [".", ".."]
88
105
  end
89
106
  end
90
107
 
91
108
  # Due to a bug in Ruby < 2.7, passing an empty **kwargs splat to
92
109
  # initialize will fail if there are no formal keyword args.
93
- if ruby_version >= 20700
110
+ # This also hits TruffleRuby
111
+ # (see https://github.com/oracle/truffleruby/issues/2567)
112
+ if ruby_version >= 20700 && !truffleruby?
113
+ ##
94
114
  # @private
115
+ #
95
116
  def self.instantiate(klass, args, kwargs, block)
96
117
  klass.new(*args, **kwargs, &block)
97
118
  end
98
119
  else
120
+ ##
99
121
  # @private
122
+ #
100
123
  def self.instantiate(klass, args, kwargs, block)
101
124
  formals = klass.instance_method(:initialize).parameters
102
125
  if kwargs.empty? && formals.all? { |arg| arg.first != :key && arg.first != :keyrest }
@@ -106,5 +129,30 @@ module Toys
106
129
  end
107
130
  end
108
131
  end
132
+
133
+ # File.absolute_path? requires Ruby 2.7 or later. For earlier Rubies, use
134
+ # an ad-hoc mechanism.
135
+ if ruby_version >= 20700
136
+ ##
137
+ # @private
138
+ #
139
+ def self.absolute_path?(path)
140
+ ::File.absolute_path?(path)
141
+ end
142
+ elsif ::Dir.getwd =~ /^[a-zA-Z]:/
143
+ ##
144
+ # @private
145
+ #
146
+ def self.absolute_path?(path)
147
+ /^[a-zA-Z]:/.match?(path)
148
+ end
149
+ else
150
+ ##
151
+ # @private
152
+ #
153
+ def self.absolute_path?(path)
154
+ path.start_with?("/")
155
+ end
156
+ end
109
157
  end
110
158
  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
@@ -116,7 +116,9 @@ module Toys
116
116
  @arg_parser ||= ArgParser.new(@cli, @tool).parse(@args)
117
117
  end
118
118
 
119
- ## @private
119
+ ##
120
+ # @private
121
+ #
120
122
  def inspect
121
123
  "<Toys::Completion::Context previous=#{previous_words.inspect}" \
122
124
  " prefix=#{fragment_prefix.inspect} fragment=#{fragment.inspect}>"
@@ -175,17 +177,23 @@ module Toys
175
177
  !@partial
176
178
  end
177
179
 
178
- ## @private
180
+ ##
181
+ # @private
182
+ #
179
183
  def eql?(other)
180
184
  other.is_a?(Candidate) && other.string.eql?(string) && other.partial? == @partial
181
185
  end
182
186
 
183
- ## @private
187
+ ##
188
+ # @private
189
+ #
184
190
  def <=>(other)
185
191
  string <=> other.string
186
192
  end
187
193
 
188
- ## @private
194
+ ##
195
+ # @private
196
+ #
189
197
  def hash
190
198
  string.hash ^ (partial? ? 1 : 0)
191
199
  end
@@ -236,6 +244,7 @@ module Toys
236
244
  # prefix. Defaults to requiring the prefix be empty.
237
245
  #
238
246
  def initialize(cwd: nil, omit_files: false, omit_directories: false, prefix_constraint: "")
247
+ super()
239
248
  @cwd = cwd || ::Dir.pwd
240
249
  @include_files = !omit_files
241
250
  @include_directories = !omit_directories
@@ -328,6 +337,7 @@ module Toys
328
337
  # prefix. Defaults to requiring the prefix be empty.
329
338
  #
330
339
  def initialize(values, prefix_constraint: "")
340
+ super()
331
341
  @values = values.flatten.map { |v| Candidate.new(v) }.sort
332
342
  @prefix_constraint = prefix_constraint
333
343
  end
@@ -424,7 +434,9 @@ module Toys
424
434
  end
425
435
  end
426
436
 
427
- ## @private
437
+ ##
438
+ # @private
439
+ #
428
440
  def self.scalarize_spec(spec, options, block)
429
441
  spec ||= block
430
442
  if options.empty?
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
@@ -133,19 +133,6 @@ module Toys
133
133
  VERBOSITY = ::Object.new.freeze
134
134
  end
135
135
 
136
- ##
137
- # Create a Context object. Applications generally will not need to create
138
- # these objects directly; they are created by the tool when it is preparing
139
- # for execution.
140
- #
141
- # @private
142
- #
143
- # @param data [Hash]
144
- #
145
- def initialize(data)
146
- @__data = data
147
- end
148
-
149
136
  ##
150
137
  # The raw arguments passed to the tool, as an array of strings.
151
138
  # This does not include the tool name itself.
@@ -335,5 +322,27 @@ module Toys
335
322
  def self.exit(code = 0)
336
323
  throw :result, code
337
324
  end
325
+
326
+ ##
327
+ # Create a Context object. Applications generally will not need to create
328
+ # these objects directly; they are created by the tool when it is preparing
329
+ # for execution.
330
+ #
331
+ # @param data [Hash]
332
+ #
333
+ # @private
334
+ #
335
+ def initialize(data)
336
+ @__data = data
337
+ end
338
+
339
+ ##
340
+ # @private
341
+ #
342
+ def inspect
343
+ name = Array(@__data[Key::TOOL_NAME]).join(" ")
344
+ id = object_id.to_s(16)
345
+ "#<Toys::Context id=0x#{id} #{name}>"
346
+ end
338
347
  end
339
348
  end
data/lib/toys/core.rb CHANGED
@@ -9,9 +9,13 @@ module Toys
9
9
  # Current version of Toys core.
10
10
  # @return [String]
11
11
  #
12
- VERSION = "0.11.5"
12
+ VERSION = "0.13.0"
13
13
  end
14
14
 
15
- ## @private deprecated
15
+ ##
16
+ # Deprecated
17
+ #
18
+ # @private
19
+ #
16
20
  CORE_VERSION = Core::VERSION
17
21
  end
@@ -0,0 +1,87 @@
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
+ ##
79
+ # @private
80
+ #
81
+ def self.inherited(tool_class)
82
+ DSL::Internal.configure_class(tool_class)
83
+ super
84
+ DSL::Internal.setup_class_dsl(tool_class)
85
+ end
86
+ end
87
+ end
data/lib/toys/dsl/flag.rb CHANGED
@@ -9,7 +9,7 @@ module Toys
9
9
  # These directives are available inside a block passed to
10
10
  # {Toys::DSL::Tool#flag}.
11
11
  #
12
- # ## Example
12
+ # ### Example
13
13
  #
14
14
  # tool "mytool" do
15
15
  # flag :value do
@@ -22,22 +22,6 @@ module Toys
22
22
  # end
23
23
  #
24
24
  class Flag
25
- ## @private
26
- def initialize(flags, acceptor, default, handler, flag_completion, value_completion,
27
- report_collisions, group, desc, long_desc, display_name)
28
- @flags = flags
29
- @default = default
30
- @handler = handler
31
- @report_collisions = report_collisions
32
- @group = group
33
- @desc = desc
34
- @long_desc = long_desc || []
35
- @display_name = display_name
36
- accept(acceptor)
37
- complete_flags(flag_completion, **{})
38
- complete_values(value_completion, **{})
39
- end
40
-
41
25
  ##
42
26
  # Add flags in OptionParser format. This may be called multiple times,
43
27
  # and the results are cumulative.
@@ -212,7 +196,7 @@ module Toys
212
196
  # across the strings in the array. In this case, whitespace is not
213
197
  # compacted.
214
198
  #
215
- # ## Examples
199
+ # ### Examples
216
200
  #
217
201
  # If you pass in a sentence as a simple string, it may be word wrapped
218
202
  # when displayed:
@@ -243,7 +227,7 @@ module Toys
243
227
  # word-wrapped when displayed. To insert a blank line, include an empty
244
228
  # string as one of the descriptions.
245
229
  #
246
- # ## Example
230
+ # ### Example
247
231
  #
248
232
  # long_desc "This initial paragraph might get word wrapped.",
249
233
  # "This next paragraph is followed by a blank line.",
@@ -284,7 +268,29 @@ module Toys
284
268
  self
285
269
  end
286
270
 
287
- ## @private
271
+ ##
272
+ # Called only from DSL::Tool
273
+ #
274
+ # @private
275
+ #
276
+ def initialize(flags, acceptor, default, handler, flag_completion, value_completion,
277
+ report_collisions, group, desc, long_desc, display_name)
278
+ @flags = flags
279
+ @default = default
280
+ @handler = handler
281
+ @report_collisions = report_collisions
282
+ @group = group
283
+ @desc = desc
284
+ @long_desc = long_desc || []
285
+ @display_name = display_name
286
+ accept(acceptor)
287
+ complete_flags(flag_completion, **{})
288
+ complete_values(value_completion, **{})
289
+ end
290
+
291
+ ##
292
+ # @private
293
+ #
288
294
  def _add_to(tool, key)
289
295
  tool.add_flag(key, @flags,
290
296
  accept: @acceptor, default: @default, handler: @handler,
@@ -10,7 +10,7 @@ module Toys
10
10
  # {Toys::DSL::Tool#at_most_one}, {Toys::DSL::Tool#at_least_one}, or
11
11
  # {Toys::DSL::Tool#exactly_one}.
12
12
  #
13
- # ## Example
13
+ # ### Example
14
14
  #
15
15
  # tool "login" do
16
16
  # all_required do
@@ -22,13 +22,6 @@ module Toys
22
22
  # end
23
23
  #
24
24
  class FlagGroup
25
- ## @private
26
- def initialize(tool_dsl, tool, flag_group)
27
- @tool_dsl = tool_dsl
28
- @tool = tool
29
- @flag_group = flag_group
30
- end
31
-
32
25
  ##
33
26
  # Add a flag to the current group. Each flag must specify a key which
34
27
  # the script may use to obtain the flag value from the context.
@@ -43,7 +36,7 @@ module Toys
43
36
  # set in a block passed to this method. If you provide a block, you can
44
37
  # use directives in {Toys::DSL::Flag} within the block.
45
38
  #
46
- # ## Flag syntax
39
+ # ### Flag syntax
47
40
  #
48
41
  # The flags themselves should be provided in OptionParser form. Following
49
42
  # are examples of valid syntax.
@@ -98,7 +91,7 @@ module Toys
98
91
  # or off. This effectively creates two flags, `--abc` which sets the
99
92
  # value to `true`, and `--no-abc` which sets the falue to `false`.
100
93
  #
101
- # ## Default flag syntax
94
+ # ### Default flag syntax
102
95
  #
103
96
  # If no flag syntax strings are provided, a default syntax will be
104
97
  # inferred based on the key and other options.
@@ -123,7 +116,7 @@ module Toys
123
116
  # flag :number, accept: Integer
124
117
  # flag :number, "--number=VAL", accept: Integer
125
118
  #
126
- # ## More examples
119
+ # ### More examples
127
120
  #
128
121
  # A flag that sets its value to the number of times it appears on the
129
122
  # command line:
@@ -200,7 +193,7 @@ module Toys
200
193
  report_collisions, @flag_group, desc, long_desc, display_name)
201
194
  flag_dsl.instance_exec(flag_dsl, &block) if block
202
195
  flag_dsl._add_to(@tool, key)
203
- DSL::Tool.maybe_add_getter(@tool_dsl, key)
196
+ DSL::Internal.maybe_add_getter(@tool_dsl, key)
204
197
  self
205
198
  end
206
199
 
@@ -221,7 +214,7 @@ module Toys
221
214
  # across the strings in the array. In this case, whitespace is not
222
215
  # compacted.
223
216
  #
224
- # ## Examples
217
+ # ### Examples
225
218
  #
226
219
  # If you pass in a sentence as a simple string, it may be word wrapped
227
220
  # when displayed:
@@ -252,7 +245,7 @@ module Toys
252
245
  # word-wrapped when displayed. To insert a blank line, include an empty
253
246
  # string as one of the descriptions.
254
247
  #
255
- # ## Example
248
+ # ### Example
256
249
  #
257
250
  # long_desc "This initial paragraph might get word wrapped.",
258
251
  # "This next paragraph is followed by a blank line.",
@@ -268,6 +261,17 @@ module Toys
268
261
  @flag_group.append_long_desc(long_desc)
269
262
  self
270
263
  end
264
+
265
+ ##
266
+ # Called only from DSL::Tool.
267
+ #
268
+ # @private
269
+ #
270
+ def initialize(tool_dsl, tool, flag_group)
271
+ @tool_dsl = tool_dsl
272
+ @tool = tool
273
+ @flag_group = flag_group
274
+ end
271
275
  end
272
276
  end
273
277
  end