toys-core 0.7.0 → 0.8.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.md +16 -24
  4. data/README.md +307 -59
  5. data/docs/guide.md +44 -4
  6. data/lib/toys-core.rb +58 -49
  7. data/lib/toys/acceptor.rb +672 -0
  8. data/lib/toys/alias.rb +106 -0
  9. data/lib/toys/arg_parser.rb +624 -0
  10. data/lib/toys/cli.rb +422 -181
  11. data/lib/toys/compat.rb +83 -0
  12. data/lib/toys/completion.rb +442 -0
  13. data/lib/toys/context.rb +354 -0
  14. data/lib/toys/core_version.rb +18 -26
  15. data/lib/toys/dsl/flag.rb +213 -56
  16. data/lib/toys/dsl/flag_group.rb +237 -51
  17. data/lib/toys/dsl/positional_arg.rb +210 -0
  18. data/lib/toys/dsl/tool.rb +968 -317
  19. data/lib/toys/errors.rb +46 -28
  20. data/lib/toys/flag.rb +821 -0
  21. data/lib/toys/flag_group.rb +282 -0
  22. data/lib/toys/input_file.rb +18 -26
  23. data/lib/toys/loader.rb +110 -100
  24. data/lib/toys/middleware.rb +24 -31
  25. data/lib/toys/mixin.rb +90 -59
  26. data/lib/toys/module_lookup.rb +125 -0
  27. data/lib/toys/positional_arg.rb +184 -0
  28. data/lib/toys/source_info.rb +192 -0
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
  32. data/lib/toys/standard_middleware/show_help.rb +130 -113
  33. data/lib/toys/standard_middleware/show_root_version.rb +29 -35
  34. data/lib/toys/standard_mixins/exec.rb +116 -78
  35. data/lib/toys/standard_mixins/fileutils.rb +16 -24
  36. data/lib/toys/standard_mixins/gems.rb +29 -30
  37. data/lib/toys/standard_mixins/highline.rb +34 -41
  38. data/lib/toys/standard_mixins/terminal.rb +72 -26
  39. data/lib/toys/template.rb +51 -35
  40. data/lib/toys/tool.rb +1161 -206
  41. data/lib/toys/utils/completion_engine.rb +171 -0
  42. data/lib/toys/utils/exec.rb +279 -182
  43. data/lib/toys/utils/gems.rb +58 -49
  44. data/lib/toys/utils/help_text.rb +117 -111
  45. data/lib/toys/utils/terminal.rb +69 -62
  46. data/lib/toys/wrappable_string.rb +162 -0
  47. metadata +24 -22
  48. data/lib/toys/definition/acceptor.rb +0 -191
  49. data/lib/toys/definition/alias.rb +0 -112
  50. data/lib/toys/definition/arg.rb +0 -140
  51. data/lib/toys/definition/flag.rb +0 -370
  52. data/lib/toys/definition/flag_group.rb +0 -205
  53. data/lib/toys/definition/source_info.rb +0 -190
  54. data/lib/toys/definition/tool.rb +0 -842
  55. data/lib/toys/dsl/arg.rb +0 -132
  56. data/lib/toys/runner.rb +0 -188
  57. data/lib/toys/standard_middleware.rb +0 -47
  58. data/lib/toys/utils/module_lookup.rb +0 -135
  59. data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Daniel Azuma
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
22
+ ;
23
+
24
+ module Toys
25
+ ##
26
+ # Compatibility wrappers for older Ruby versions.
27
+ # @private
28
+ #
29
+ module Compat
30
+ ## @private
31
+ CURRENT_VERSION = ::Gem::Version.new(::RUBY_VERSION)
32
+
33
+ ## @private
34
+ def self.check_minimum_version(version)
35
+ CURRENT_VERSION >= ::Gem::Version.new(version)
36
+ end
37
+
38
+ if check_minimum_version("2.4.0")
39
+ ## @private
40
+ def self.suggestions(word, list)
41
+ ::DidYouMean::SpellChecker.new(dictionary: list).correct(word)
42
+ end
43
+ else
44
+ ## @private
45
+ def self.suggestions(_word, _list)
46
+ []
47
+ end
48
+ end
49
+
50
+ if check_minimum_version("2.4.0")
51
+ ## @private
52
+ def self.merge_clones(hash, orig)
53
+ orig.each { |k, v| hash[k] = v.clone }
54
+ hash
55
+ end
56
+ else
57
+ ## @private
58
+ def self.merge_clones(hash, orig)
59
+ orig.each do |k, v|
60
+ hash[k] =
61
+ begin
62
+ v.clone
63
+ rescue ::TypeError
64
+ v
65
+ end
66
+ end
67
+ hash
68
+ end
69
+ end
70
+
71
+ if check_minimum_version("2.5.0")
72
+ ## @private
73
+ def self.glob_in_dir(glob, dir)
74
+ ::Dir.glob(glob, base: dir)
75
+ end
76
+ else
77
+ ## @private
78
+ def self.glob_in_dir(glob, dir)
79
+ ::Dir.chdir(dir) { ::Dir.glob(glob) }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,442 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Daniel Azuma
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
22
+ ;
23
+
24
+ module Toys
25
+ ##
26
+ # A Completion is a callable Proc that determines candidates for shell tab
27
+ # completion. You pass a {Toys::Completion::Context} object (which includes
28
+ # the current string fragment and other information) and it returns an array
29
+ # of candidates, represented by {Toys::Completion::Candidate} objects, for
30
+ # completing the fragment.
31
+ #
32
+ # A useful method here is the class method {Toys::Completion.create} which
33
+ # takes a variety of inputs and returns a suitable completion Proc.
34
+ #
35
+ module Completion
36
+ ##
37
+ # The context in which to determine completion candidates.
38
+ #
39
+ class Context
40
+ ##
41
+ # Create a completion context
42
+ #
43
+ # @param cli [Toys::CLI] The CLI being run. Required.
44
+ # @param previous_words [Array<String>] Array of complete strings that
45
+ # appeared prior to the fragment to complete.
46
+ # @param fragment_prefix [String] A prefix in the fragment that does not
47
+ # participate in completion. (e.g. "key=")
48
+ # @param fragment [String] The string fragment to complete.
49
+ # @param params [Hash] Miscellaneous context data
50
+ #
51
+ def initialize(cli:, previous_words: [], fragment_prefix: "", fragment: "", **params)
52
+ @cli = cli
53
+ @previous_words = previous_words
54
+ @fragment_prefix = fragment_prefix
55
+ @fragment = fragment
56
+ extra_params = {
57
+ cli: cli, previous_words: previous_words, fragment_prefix: fragment_prefix,
58
+ fragment: fragment
59
+ }
60
+ @params = params.merge(extra_params)
61
+ @tool = nil
62
+ @args = nil
63
+ @arg_parser = nil
64
+ end
65
+
66
+ ##
67
+ # Create a new completion context with the given modifications.
68
+ #
69
+ # @param delta_params [Hash] Replace context data.
70
+ # @return [Toys::Completion::Context]
71
+ #
72
+ def with(**delta_params)
73
+ Context.new(@params.merge(delta_params))
74
+ end
75
+
76
+ ##
77
+ # The CLI being run.
78
+ # @return [Toys::CLI]
79
+ #
80
+ attr_reader :cli
81
+
82
+ ##
83
+ # All previous words.
84
+ # @return [Array<String>]
85
+ #
86
+ attr_reader :previous_words
87
+
88
+ ##
89
+ # A non-completed prefix for the current fragment.
90
+ # @return [String]
91
+ #
92
+ attr_reader :fragment_prefix
93
+
94
+ ##
95
+ # The current string fragment to complete
96
+ # @return [String]
97
+ #
98
+ attr_reader :fragment
99
+
100
+ ##
101
+ # Get data for arbitrary key.
102
+ # @param [Symbol] key
103
+ # @return [Object]
104
+ #
105
+ def [](key)
106
+ @params[key]
107
+ end
108
+ alias get []
109
+
110
+ ##
111
+ # The tool being invoked, which should control the completion.
112
+ # @return [Toys::Tool]
113
+ #
114
+ def tool
115
+ lookup_tool
116
+ @tool
117
+ end
118
+
119
+ ##
120
+ # An array of complete arguments passed to the tool, prior to the
121
+ # fragment to complete.
122
+ # @return [Array<String>]
123
+ #
124
+ def args
125
+ lookup_tool
126
+ @args
127
+ end
128
+
129
+ ##
130
+ # Current ArgParser indicating the status of argument parsing up to
131
+ # this point.
132
+ #
133
+ # @return [Toys::ArgParser]
134
+ #
135
+ def arg_parser
136
+ lookup_tool
137
+ @arg_parser ||= ArgParser.new(@cli, @tool).parse(@args)
138
+ end
139
+
140
+ ## @private
141
+ def inspect
142
+ "<Toys::Completion::Context previous=#{previous_words.inspect}" \
143
+ " prefix=#{fragment_prefix.inspect} fragment=#{fragment.inspect}>"
144
+ end
145
+
146
+ private
147
+
148
+ def lookup_tool
149
+ @tool, @args = @cli.loader.lookup(@previous_words) unless @tool
150
+ end
151
+ end
152
+
153
+ ##
154
+ # A candidate for completing a string fragment.
155
+ #
156
+ # A candidate includes a string representing the potential completed
157
+ # word, as well as a flag indicating whether it is a *partial* completion
158
+ # (i.e. a prefix that could still be added to) versus a *final* word.
159
+ # Generally, tab completion systems should add a trailing space after a
160
+ # final completion but not after a partial completion.
161
+ #
162
+ class Candidate
163
+ include ::Comparable
164
+
165
+ ##
166
+ # Create a new candidate
167
+ # @param string [String] The candidate string
168
+ # @param partial [Boolean] Whether the candidate is partial. Defaults
169
+ # to `false`.
170
+ #
171
+ def initialize(string, partial: false)
172
+ @string = string.to_s
173
+ @partial = partial ? true : false
174
+ end
175
+
176
+ ##
177
+ # Get the candidate string.
178
+ # @return [String]
179
+ #
180
+ attr_reader :string
181
+ alias to_s string
182
+
183
+ ##
184
+ # Determine whether the candidate is partial completion.
185
+ # @return [Boolean]
186
+ #
187
+ def partial?
188
+ @partial
189
+ end
190
+
191
+ ##
192
+ # Determine whether the candidate is a final completion.
193
+ # @return [Boolean]
194
+ #
195
+ def final?
196
+ !@partial
197
+ end
198
+
199
+ ## @private
200
+ def eql?(other)
201
+ other.is_a?(Candidate) && other.string.eql?(string) && other.partial? == @partial
202
+ end
203
+
204
+ ## @private
205
+ def <=>(other)
206
+ string <=> other.string
207
+ end
208
+
209
+ ## @private
210
+ def hash
211
+ string.hash
212
+ end
213
+
214
+ ##
215
+ # Create an array of candidates given an array of strings.
216
+ #
217
+ # @param array [Array<String>]
218
+ # @return [Array<Toys::Completion::Candidate]
219
+ #
220
+ def self.new_multi(array, partial: false)
221
+ array.map { |s| new(s, partial: partial) }
222
+ end
223
+ end
224
+
225
+ ##
226
+ # A base class that returns no completions.
227
+ #
228
+ # Completions *may* but do not need to subclass this base class. They
229
+ # merely need to duck-type `Proc` by implementing the `call` method.
230
+ #
231
+ class Base
232
+ ##
233
+ # Returns candidates for the current completion.
234
+ # This default implementation returns an empty list.
235
+ #
236
+ # @param context [Toys::Completion::Context] The current completion
237
+ # context including the string fragment.
238
+ # @return [Array<Toys::Completion::Candidate>] An array of candidates
239
+ #
240
+ def call(context) # rubocop:disable Lint/UnusedMethodArgument
241
+ []
242
+ end
243
+ end
244
+
245
+ ##
246
+ # A Completion that returns candidates from the local file system.
247
+ #
248
+ class FileSystem < Base
249
+ ##
250
+ # Create a completion that gets candidates from names in the local file
251
+ # system.
252
+ #
253
+ # @param cwd [String] Working directory (defaults to the current dir).
254
+ # @param omit_files [Boolean] Omit files from candidates
255
+ # @param omit_directories [Boolean] Omit directories from candidates
256
+ # @param prefix_constraint [String,Regexp] Constraint on the fragment
257
+ # prefix. Defaults to requiring the prefix be empty.
258
+ #
259
+ def initialize(cwd: nil, omit_files: false, omit_directories: false, prefix_constraint: "")
260
+ @cwd = cwd || ::Dir.pwd
261
+ @include_files = !omit_files
262
+ @include_directories = !omit_directories
263
+ @prefix_constraint = prefix_constraint
264
+ end
265
+
266
+ ##
267
+ # Whether files are included in the completion candidates.
268
+ # @return [Boolean]
269
+ #
270
+ attr_reader :include_files
271
+
272
+ ##
273
+ # Whether directories are included in the completion candidates.
274
+ # @return [Boolean]
275
+ #
276
+ attr_reader :include_directories
277
+
278
+ ##
279
+ # Constraint on the fragment prefix.
280
+ # @return [String,Regexp]
281
+ #
282
+ attr_reader :prefix_constraint
283
+
284
+ ##
285
+ # Path to the starting directory.
286
+ # @return [String]
287
+ #
288
+ attr_reader :cwd
289
+
290
+ ##
291
+ # Returns candidates for the current completion.
292
+ #
293
+ # @param context [Toys::Completion::Context] the current completion
294
+ # context including the string fragment.
295
+ # @return [Array<Toys::Completion::Candidate>] an array of candidates
296
+ #
297
+ def call(context)
298
+ return [] unless @prefix_constraint === context.fragment_prefix
299
+ substring = context.fragment
300
+ prefix, name =
301
+ if substring.empty? || substring.end_with?("/")
302
+ [substring, ""]
303
+ else
304
+ ::File.split(substring)
305
+ end
306
+ dir = ::File.expand_path(prefix, @cwd)
307
+ return [] unless ::File.directory?(dir)
308
+ prefix = nil if [".", ""].include?(prefix)
309
+ omits = [".", ".."]
310
+ children = Compat.glob_in_dir(name, dir).find_all do |child|
311
+ !omits.include?(child)
312
+ end
313
+ children += ::Dir.entries(dir).find_all do |child|
314
+ child.start_with?(name) && !omits.include?(child)
315
+ end
316
+ generate_candidates(children.uniq.sort, prefix, dir)
317
+ end
318
+
319
+ private
320
+
321
+ def generate_candidates(children, prefix, dir)
322
+ children.flat_map do |child|
323
+ path = ::File.join(dir, child)
324
+ str = prefix ? ::File.join(prefix, child) : child
325
+ if ::File.file?(path)
326
+ @include_files ? [Candidate.new(str)] : []
327
+ elsif ::File.directory?(path)
328
+ if @include_directories
329
+ [Candidate.new("#{str}/", partial: true)]
330
+ else
331
+ []
332
+ end
333
+ else
334
+ []
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ ##
341
+ # A Completion whose candidates come from a static list of strings.
342
+ #
343
+ class Enum < Base
344
+ ##
345
+ # Create a completion from a list of values.
346
+ #
347
+ # @param values [Array<String>]
348
+ # @param prefix_constraint [String,Regexp] Constraint on the fragment
349
+ # prefix. Defaults to requiring the prefix be empty.
350
+ #
351
+ def initialize(values, prefix_constraint: "")
352
+ @values = values.flatten.map { |v| Candidate.new(v) }.sort
353
+ @prefix_constraint = prefix_constraint
354
+ end
355
+
356
+ ##
357
+ # The array of completion candidates.
358
+ # @return [Array<String>]
359
+ #
360
+ attr_reader :values
361
+
362
+ ##
363
+ # Constraint on the fragment prefix.
364
+ # @return [String,Regexp]
365
+ #
366
+ attr_reader :prefix_constraint
367
+
368
+ ##
369
+ # Returns candidates for the current completion.
370
+ #
371
+ # @param context [Toys::Completion::Context] the current completion
372
+ # context including the string fragment.
373
+ # @return [Array<Toys::Completion::Candidate>] an array of candidates
374
+ #
375
+ def call(context)
376
+ return [] unless @prefix_constraint === context.fragment_prefix
377
+ fragment = context.fragment
378
+ @values.find_all { |val| val.string.start_with?(fragment) }
379
+ end
380
+ end
381
+
382
+ ##
383
+ # An instance of the empty completion that returns no candidates.
384
+ # @return [Toys:::Completion::Base]
385
+ #
386
+ EMPTY = Base.new
387
+
388
+ ##
389
+ # Create a completion Proc from a variety of specification formats. The
390
+ # completion is constructed from the given specification object and/or the
391
+ # given block. Additionally, some completions can take a hash of options.
392
+ #
393
+ # Recognized specs include:
394
+ #
395
+ # * `:empty`: Returns the empty completion. Any block or options are
396
+ # ignored.
397
+ #
398
+ # * `:file_system`: Returns a completion that searches the current
399
+ # directory for file and directory names. You may also pass any of the
400
+ # options recognized by {Toys::Completion::FileSystem#initialize}. The
401
+ # block is ignored.
402
+ #
403
+ # * An **Array** of strings. Returns a completion that uses those values
404
+ # as candidates. You may also pass any of the options recognized by
405
+ # {Toys::Completion::Enum#initialize}. The block is ignored.
406
+ #
407
+ # * A **function**, either passed as a Proc (where the block is ignored)
408
+ # or as a block (if the spec is nil). The function must behave as a
409
+ # completion object, taking {Toys::Completion::Context} as the sole
410
+ # argument, and returning an array of {Toys::Completion::Candidate}.
411
+ #
412
+ # * `:default` and `nil` indicate the **default completion**. For this
413
+ # method, the default is the empty completion (i.e. these are synonyms
414
+ # for `:empty`). However, other completion resolution methods might
415
+ # have a different default.
416
+ #
417
+ # @param spec [Object] See the description for recognized values.
418
+ # @param options [Hash] Additional options to pass to the completion.
419
+ # @param block [Proc] See the description for recognized forms.
420
+ # @return [Toys::Completion::Base,Proc]
421
+ #
422
+ def self.create(spec = nil, **options, &block)
423
+ spec ||= block
424
+ case spec
425
+ when nil, :empty, :default
426
+ EMPTY
427
+ when ::Proc, Base
428
+ spec
429
+ when ::Array
430
+ Enum.new(spec, options)
431
+ when :file_system
432
+ FileSystem.new(options)
433
+ else
434
+ if spec.respond_to?(:call)
435
+ spec
436
+ else
437
+ raise ToolDefinitionError, "Illegal completion spec: #{spec.inspect}"
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end