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