toys-core 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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