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
@@ -1,205 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- module Toys
33
- module Definition
34
- ##
35
- # Representation of a group of flags with the same requirement settings.
36
- #
37
- class FlagGroup
38
- ##
39
- # Create a flag group.
40
- # @private
41
- #
42
- def initialize(name, desc, long_desc)
43
- @name = name
44
- @desc = Utils::WrappableString.make(desc || default_desc)
45
- @long_desc = Utils::WrappableString.make_array(long_desc || default_long_desc)
46
- @flag_definitions = []
47
- end
48
-
49
- ##
50
- # Returns the symbolic name for this group
51
- # @return [String,Symbol,nil]
52
- #
53
- attr_reader :name
54
-
55
- ##
56
- # Returns the short description string.
57
- # @return [Toys::Utils::WrappableString]
58
- #
59
- attr_reader :desc
60
-
61
- ##
62
- # Returns the long description strings as an array.
63
- # @return [Array<Toys::Utils::WrappableString>]
64
- #
65
- attr_reader :long_desc
66
-
67
- ##
68
- # Returns an array of flags that are in this group.
69
- # Do not modify the returned array.
70
- # @return [Array<Toys::Definition::Flag>]
71
- #
72
- attr_reader :flag_definitions
73
-
74
- ##
75
- # Returns true if this group is empty
76
- # @return [Boolean]
77
- #
78
- def empty?
79
- flag_definitions.empty?
80
- end
81
-
82
- ## @private
83
- def <<(flag)
84
- flag_definitions << flag
85
- end
86
-
87
- ## @private
88
- def default_desc
89
- "Flags"
90
- end
91
-
92
- ## @private
93
- def default_long_desc
94
- nil
95
- end
96
-
97
- ## @private
98
- def validation_error(_seen)
99
- nil
100
- end
101
-
102
- ##
103
- # A FlagGroup containing all required flags
104
- #
105
- class Required < FlagGroup
106
- ## @private
107
- def validation_error(seen)
108
- flag_definitions.each do |flag|
109
- unless seen.include?(flag.key)
110
- return "Flag \"#{flag.display_name}\" is required"
111
- end
112
- end
113
- nil
114
- end
115
-
116
- ## @private
117
- def default_desc
118
- "Required Flags"
119
- end
120
-
121
- ## @private
122
- def default_long_desc
123
- "These flags are required."
124
- end
125
- end
126
-
127
- ##
128
- # A FlagGroup containing all optional flags
129
- #
130
- class Optional < FlagGroup
131
- end
132
-
133
- ##
134
- # A FlagGroup in which exactly one flag must be set
135
- #
136
- class ExactlyOne < FlagGroup
137
- ## @private
138
- def validation_error(seen)
139
- set_flag = nil
140
- flag_definitions.each do |flag|
141
- if seen.include?(flag.key)
142
- if set_flag
143
- return "Exactly one out of group \"#{desc}\" is required, but both" \
144
- " \"#{set_flag.display_name}\" and \"#{flag.display_name}\" were set"
145
- else
146
- set_flag = flag
147
- end
148
- end
149
- end
150
- return "Exactly one out of group \"#{desc}\" is required" unless set_flag
151
- nil
152
- end
153
-
154
- ## @private
155
- def default_long_desc
156
- "Exactly one of these flags must be set."
157
- end
158
- end
159
-
160
- ##
161
- # A FlagGroup in which at most one flag must be set
162
- #
163
- class AtMostOne < FlagGroup
164
- ## @private
165
- def validation_error(seen)
166
- set_flag = nil
167
- flag_definitions.each do |flag|
168
- if seen.include?(flag.key)
169
- if set_flag
170
- return "At most one out of group \"#{desc}\" is required, but both" \
171
- " \"#{set_flag.display_name}\" and \"#{flag.display_name}\" were set"
172
- else
173
- set_flag = flag
174
- end
175
- end
176
- end
177
- nil
178
- end
179
-
180
- ## @private
181
- def default_long_desc
182
- "At most one of these flags must be set."
183
- end
184
- end
185
-
186
- ##
187
- # A FlagGroup in which at least one flag must be set
188
- #
189
- class AtLeastOne < FlagGroup
190
- ## @private
191
- def validation_error(seen)
192
- flag_definitions.each do |flag|
193
- return nil if seen.include?(flag.key)
194
- end
195
- "At least one out of group \"#{desc}\" is required"
196
- end
197
-
198
- ## @private
199
- def default_long_desc
200
- "At least one of these flags must be set."
201
- end
202
- end
203
- end
204
- end
205
- end
@@ -1,190 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- module Toys
33
- module Definition
34
- ##
35
- # Information about source toys directories and files.
36
- #
37
- class SourceInfo
38
- ##
39
- # Create a SourceInfo.
40
- # @private
41
- #
42
- def initialize(parent, context_directory, source, source_type, source_name, data_dir_name)
43
- @parent = parent
44
- @context_directory = context_directory
45
- @source = source
46
- @source_type = source_type
47
- @source_path = source if source.is_a?(::String)
48
- @source_proc = source if source.is_a?(::Proc)
49
- @source_name = source_name
50
- @data_dir =
51
- if data_dir_name && @source_path
52
- dir = ::File.join(::File.dirname(@source_path), data_dir_name)
53
- dir if ::File.directory?(dir) && ::File.readable?(dir)
54
- end
55
- end
56
-
57
- ##
58
- # Return the parent SourceInfo, or nil if this is the root.
59
- # @return [Toys::Definition::SourceInfo,nil]
60
- #
61
- attr_reader :parent
62
-
63
- ##
64
- # Return the context directory path (normally the directory containing
65
- # the toplevel toys file or directory). May return nil if there is no
66
- # context (e.g. the tool is being defined from a block).
67
- # @return [String,nil]
68
- #
69
- attr_reader :context_directory
70
-
71
- ##
72
- # Return the source, which may be a path or a proc.
73
- # @return [String,Proc]
74
- #
75
- attr_reader :source
76
-
77
- ##
78
- # Return the type of source.
79
- # @return [:file,:directory,:proc]
80
- #
81
- attr_reader :source_type
82
-
83
- ##
84
- # Return the path of the current source file or directory, or nil if this
85
- # source is not a file system path.
86
- # @return [String,nil]
87
- #
88
- attr_reader :source_path
89
-
90
- ##
91
- # Return the source proc, or nil if this source is not a proc.
92
- # @return [Proc,nil]
93
- #
94
- attr_reader :source_proc
95
-
96
- ##
97
- # Return the user-visible name of this source.
98
- # @return [String]
99
- #
100
- attr_reader :source_name
101
- alias to_s source_name
102
-
103
- ##
104
- # Return the absolute path to the given data file or directory.
105
- #
106
- # @param [String] path The relative path to find
107
- # @param [nil,:file,:directory] type Type of file system object to find,
108
- # or nil to return any type.
109
- # @return [String,nil] Absolute path of the result, or nil if not found.
110
- #
111
- def find_data(path, type: nil)
112
- if @data_dir
113
- full_path = ::File.join(@data_dir, path)
114
- case type
115
- when :file
116
- return full_path if ::File.file?(full_path)
117
- when :directory
118
- return full_path if ::File.directory?(full_path)
119
- else
120
- return full_path if ::File.readable?(full_path)
121
- end
122
- end
123
- parent&.find_data(path, type: type)
124
- end
125
-
126
- ##
127
- # Create a child SourceInfo relative to the parent path.
128
- # @private
129
- #
130
- def relative_child(filename, data_dir_name)
131
- raise "Cannot create relative child of a proc" unless source_path
132
- child_path = ::File.join(source_path, filename)
133
- child_path, type = SourceInfo.check_path(child_path, true)
134
- return nil unless child_path
135
- SourceInfo.new(self, context_directory, child_path, type, child_path, data_dir_name)
136
- end
137
-
138
- ##
139
- # Create a child SourceInfo with an absolute path.
140
- # @private
141
- #
142
- def absolute_child(child_path)
143
- child_path, type = SourceInfo.check_path(child_path, false)
144
- SourceInfo.new(self, context_directory, child_path, type, child_path, nil)
145
- end
146
-
147
- ##
148
- # Create a root source info for a file path.
149
- # @private
150
- #
151
- def self.create_path_root(source_path)
152
- source_path, type = check_path(source_path, false)
153
- context_directory = ::File.dirname(source_path)
154
- new(nil, context_directory, source_path, type, source_path, nil)
155
- end
156
-
157
- ##
158
- # Create a root source info for a proc.
159
- # @private
160
- #
161
- def self.create_proc_root(source_proc, source_name)
162
- new(nil, nil, source_proc, :proc, source_name, nil)
163
- end
164
-
165
- ##
166
- # Check a path and determine the canonical path and type.
167
- # @private
168
- #
169
- def self.check_path(path, lenient)
170
- path = ::File.expand_path(path)
171
- unless ::File.readable?(path)
172
- raise LoaderError, "Cannot read: #{path}" unless lenient
173
- return [nil, nil]
174
- end
175
- if ::File.file?(path)
176
- unless ::File.extname(path) == ".rb"
177
- raise LoaderError, "File is not a ruby file: #{path}" unless lenient
178
- return [nil, nil]
179
- end
180
- [path, :file]
181
- elsif ::File.directory?(path)
182
- [path, :directory]
183
- else
184
- raise LoaderError, "Unknown type: #{path}" unless lenient
185
- [nil, nil]
186
- end
187
- end
188
- end
189
- end
190
- end
@@ -1,842 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Daniel Azuma
4
- #
5
- # All rights reserved.
6
- #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
30
- ;
31
-
32
- require "optparse"
33
- require "set"
34
-
35
- module Toys
36
- module Definition
37
- ##
38
- # A Tool is a single command that can be invoked using Toys.
39
- # It has a name, a series of one or more words that you use to identify
40
- # the tool on the command line. It also has a set of formal flags and
41
- # command line arguments supported, and a block that gets run when the
42
- # tool is executed.
43
- #
44
- class Tool
45
- ##
46
- # Built-in acceptors (i.e. those recognized by OptionParser).
47
- # You can reference these acceptors directly. Otherwise, you have to add
48
- # one explicitly to the tool using {Tool#add_acceptor}.
49
- #
50
- OPTPARSER_ACCEPTORS = ::Set.new(
51
- [
52
- ::Object,
53
- ::NilClass,
54
- ::String,
55
- ::Integer,
56
- ::Float,
57
- ::Numeric,
58
- ::TrueClass,
59
- ::FalseClass,
60
- ::Array,
61
- ::Regexp,
62
- ::OptionParser::DecimalInteger,
63
- ::OptionParser::OctalInteger,
64
- ::OptionParser::DecimalNumeric
65
- ]
66
- ).freeze
67
-
68
- ##
69
- # Create a new tool.
70
- # @private
71
- #
72
- def initialize(loader, parent, full_name, priority, middleware_stack)
73
- @parent = parent
74
- @full_name = full_name.dup.freeze
75
- @priority = priority
76
- @middleware_stack = middleware_stack
77
-
78
- @acceptors = {}
79
- @mixins = {}
80
- @templates = {}
81
-
82
- reset_definition(loader)
83
- end
84
-
85
- ##
86
- # Reset the definition of this tool, deleting all definition data but
87
- # leaving named acceptors, mixins, and templates intact.
88
- # Should be called only from the DSL.
89
- # @private
90
- #
91
- def reset_definition(loader)
92
- @tool_class = DSL::Tool.new_class(@full_name, @priority, loader)
93
-
94
- @source_info = nil
95
- @definition_finished = false
96
-
97
- @desc = Utils::WrappableString.new("")
98
- @long_desc = []
99
-
100
- @default_data = {}
101
- @used_flags = []
102
- @initializers = []
103
-
104
- default_flag_group = Definition::FlagGroup.new(nil, nil, nil)
105
- @flag_groups = [default_flag_group]
106
- @flag_group_names = {nil => default_flag_group}
107
-
108
- @flag_definitions = []
109
- @required_arg_definitions = []
110
- @optional_arg_definitions = []
111
- @remaining_args_definition = nil
112
-
113
- @disable_argument_parsing = false
114
- @includes_modules = false
115
- @custom_context_directory = nil
116
- end
117
-
118
- ##
119
- # Return the name of the tool as an array of strings.
120
- # This array may not be modified.
121
- # @return [Array<String>]
122
- #
123
- attr_reader :full_name
124
-
125
- ##
126
- # Return the priority of this tool definition.
127
- # @return [Integer]
128
- #
129
- attr_reader :priority
130
-
131
- ##
132
- # Return the tool class.
133
- # @return [Class]
134
- #
135
- attr_reader :tool_class
136
-
137
- ##
138
- # Returns the short description string.
139
- # @return [Toys::Utils::WrappableString]
140
- #
141
- attr_reader :desc
142
-
143
- ##
144
- # Returns the long description strings as an array.
145
- # @return [Array<Toys::Utils::WrappableString>]
146
- #
147
- attr_reader :long_desc
148
-
149
- ##
150
- # Return a list of all defined flag groups, in order.
151
- # @return [Array<Toys::Definition::FlagGroup>]
152
- #
153
- attr_reader :flag_groups
154
-
155
- ##
156
- # Return a list of all defined flags.
157
- # @return [Array<Toys::Definition::Flag>]
158
- #
159
- attr_reader :flag_definitions
160
-
161
- ##
162
- # Return a list of all defined required positional arguments.
163
- # @return [Array<Toys::Definition::Arg>]
164
- #
165
- attr_reader :required_arg_definitions
166
-
167
- ##
168
- # Return a list of all defined optional positional arguments.
169
- # @return [Array<Toys::Definition::Arg>]
170
- #
171
- attr_reader :optional_arg_definitions
172
-
173
- ##
174
- # Return the remaining arguments specification, or `nil` if remaining
175
- # arguments are currently not supported by this tool.
176
- # @return [Toys::Definition::Arg,nil]
177
- #
178
- attr_reader :remaining_args_definition
179
-
180
- ##
181
- # Return a list of flags that have been used in the flag definitions.
182
- # @return [Array<String>]
183
- #
184
- attr_reader :used_flags
185
-
186
- ##
187
- # Return the default argument data.
188
- # @return [Hash]
189
- #
190
- attr_reader :default_data
191
-
192
- ##
193
- # Returns the middleware stack
194
- # @return [Array<Object>]
195
- #
196
- attr_reader :middleware_stack
197
-
198
- ##
199
- # Returns info on the source of this tool, or nil if the source is not
200
- # defined.
201
- # @return [Toys::Definition::SourceInfo,nil]
202
- #
203
- attr_reader :source_info
204
-
205
- ##
206
- # Returns the custom context directory set for this tool, or nil if none
207
- # is set.
208
- # @return [String,nil]
209
- #
210
- attr_reader :custom_context_directory
211
-
212
- ##
213
- # Returns the local name of this tool.
214
- # @return [String]
215
- #
216
- def simple_name
217
- full_name.last
218
- end
219
-
220
- ##
221
- # Returns a displayable name of this tool, generally the full name
222
- # delimited by spaces.
223
- # @return [String]
224
- #
225
- def display_name
226
- full_name.join(" ")
227
- end
228
-
229
- ##
230
- # Returns true if this tool is a root tool.
231
- # @return [Boolean]
232
- #
233
- def root?
234
- full_name.empty?
235
- end
236
-
237
- ##
238
- # Returns true if this tool is marked as runnable.
239
- # @return [Boolean]
240
- #
241
- def runnable?
242
- tool_class.public_instance_methods(false).include?(:run)
243
- end
244
-
245
- ##
246
- # Returns true if this tool has at least one included module.
247
- # @return [Boolean]
248
- #
249
- def includes_modules?
250
- @includes_modules
251
- end
252
-
253
- ##
254
- # Returns true if there is a specific description set for this tool.
255
- # @return [Boolean]
256
- #
257
- def includes_description?
258
- !long_desc.empty? || !desc.empty?
259
- end
260
-
261
- ##
262
- # Returns true if at least one flag or positional argument is defined
263
- # for this tool.
264
- # @return [Boolean]
265
- #
266
- def includes_arguments?
267
- !default_data.empty? || !flag_definitions.empty? ||
268
- !required_arg_definitions.empty? || !optional_arg_definitions.empty? ||
269
- !remaining_args_definition.nil?
270
- end
271
-
272
- ##
273
- # Returns true if this tool has any definition information.
274
- # @return [Boolean]
275
- #
276
- def includes_definition?
277
- includes_arguments? || runnable? || argument_parsing_disabled? ||
278
- includes_modules? || includes_description?
279
- end
280
-
281
- ##
282
- # Returns true if this tool's definition has been finished and is locked.
283
- # @return [Boolean]
284
- #
285
- def definition_finished?
286
- @definition_finished
287
- end
288
-
289
- ##
290
- # Returns true if this tool has disabled argument parsing.
291
- # @return [Boolean]
292
- #
293
- def argument_parsing_disabled?
294
- @disable_argument_parsing
295
- end
296
-
297
- ##
298
- # Returns all arg definitions in order: required, optional, remaining.
299
- # @return [Array<Toys::Definition::Arg>]
300
- #
301
- def arg_definitions
302
- result = required_arg_definitions + optional_arg_definitions
303
- result << remaining_args_definition if remaining_args_definition
304
- result
305
- end
306
-
307
- ##
308
- # Returns a list of all custom acceptors used by this tool.
309
- # @return [Array<Toys::Definition::Acceptor>]
310
- #
311
- def custom_acceptors
312
- result = []
313
- flag_definitions.each do |f|
314
- result << f.accept if f.accept.is_a?(Acceptor)
315
- end
316
- arg_definitions.each do |a|
317
- result << a.accept if a.accept.is_a?(Acceptor)
318
- end
319
- result.uniq
320
- end
321
-
322
- ##
323
- # Resolve the given acceptor. You may pass in a
324
- # {Toys::Definition::Acceptor}, an acceptor name, a well-known acceptor
325
- # understood by OptionParser, or `nil`.
326
- #
327
- # Returns either `nil` or an acceptor that is usable by OptionParser.
328
- #
329
- # If an acceptor name is given, it may be resolved by this tool or any of
330
- # its ancestors. Raises {Toys::ToolDefinitionError} if the name is not
331
- # recognized.
332
- #
333
- # @param [Object] accept An acceptor input.
334
- # @return [Object] The resolved acceptor.
335
- #
336
- def resolve_acceptor(accept)
337
- return accept if accept.nil? || accept.is_a?(Acceptor)
338
- name = accept
339
- accept = @acceptors.fetch(name) do |k|
340
- if @parent
341
- @parent.resolve_acceptor(k)
342
- elsif OPTPARSER_ACCEPTORS.include?(k)
343
- k
344
- end
345
- end
346
- if accept.nil?
347
- raise ToolDefinitionError, "Unknown acceptor: #{name.inspect}"
348
- end
349
- accept
350
- end
351
-
352
- ##
353
- # Get the named template from this tool or its ancestors.
354
- #
355
- # @param [String] name The template name
356
- # @return [Class,nil] The template class, or `nil` if not found.
357
- #
358
- def resolve_template(name)
359
- @templates.fetch(name.to_s) { |k| @parent ? @parent.resolve_template(k) : nil }
360
- end
361
-
362
- ##
363
- # Get the named mixin from this tool or its ancestors.
364
- #
365
- # @param [String] name The mixin name
366
- # @return [Module,nil] The mixin module, or `nil` if not found.
367
- #
368
- def resolve_mixin(name)
369
- @mixins.fetch(name.to_s) { |k| @parent ? @parent.resolve_mixin(k) : nil }
370
- end
371
-
372
- ##
373
- # Include the given mixin in the tool class.
374
- #
375
- # @param [String,Symbol,Module] name The mixin name or module
376
- #
377
- def include_mixin(name)
378
- tool_class.include(name)
379
- self
380
- end
381
-
382
- ##
383
- # Sets the path to the file that defines this tool.
384
- # A tool may be defined from at most one path. If a different path is
385
- # already set, raises {Toys::ToolDefinitionError}
386
- #
387
- # @param [Toys::Definition::SourceInfo] source Source info
388
- #
389
- def lock_source(source)
390
- if source_info && source_info.source != source.source
391
- raise ToolDefinitionError,
392
- "Cannot redefine tool #{display_name.inspect} in #{source.source_name}" \
393
- " (already defined in #{source_info.source_name})"
394
- end
395
- @source_info = source
396
- end
397
-
398
- ##
399
- # Set the short description string.
400
- #
401
- # The description may be provided as a {Toys::Utils::WrappableString}, a
402
- # single string (which will be wrapped), or an array of strings, which will
403
- # be interpreted as string fragments that will be concatenated and wrapped.
404
- #
405
- # @param [Toys::Utils::WrappableString,String,Array<String>] desc
406
- #
407
- def desc=(desc)
408
- check_definition_state
409
- @desc = Utils::WrappableString.make(desc)
410
- end
411
-
412
- ##
413
- # Set the long description strings.
414
- #
415
- # Each string may be provided as a {Toys::Utils::WrappableString}, a single
416
- # string (which will be wrapped), or an array of strings, which will be
417
- # interpreted as string fragments that will be concatenated and wrapped.
418
- #
419
- # @param [Array<Toys::Utils::WrappableString,String,Array<String>>] long_desc
420
- #
421
- def long_desc=(long_desc)
422
- check_definition_state
423
- @long_desc = Utils::WrappableString.make_array(long_desc)
424
- end
425
-
426
- ##
427
- # Append long description strings.
428
- #
429
- # Each string may be provided as a {Toys::Utils::WrappableString}, a single
430
- # string (which will be wrapped), or an array of strings, which will be
431
- # interpreted as string fragments that will be concatenated and wrapped.
432
- #
433
- # @param [Array<Toys::Utils::WrappableString,String,Array<String>>] long_desc
434
- #
435
- def append_long_desc(long_desc)
436
- check_definition_state
437
- @long_desc += Utils::WrappableString.make_array(long_desc)
438
- end
439
-
440
- ##
441
- # Add an acceptor to the tool. This acceptor may be refereneced by name
442
- # when adding a flag or an arg.
443
- #
444
- # @param [Toys::Definition::Acceptor] acceptor The acceptor to add.
445
- #
446
- def add_acceptor(acceptor)
447
- if @acceptors.key?(acceptor.name)
448
- raise ToolDefinitionError,
449
- "An acceptor named #{acceptor.name.inspect} has already been" \
450
- " defined in tool #{display_name.inspect}."
451
- end
452
- @acceptors[acceptor.name] = acceptor
453
- self
454
- end
455
-
456
- ##
457
- # Add a named mixin module to this tool.
458
- #
459
- # @param [String] name The name of the mixin.
460
- # @param [Module] mixin_module The mixin module.
461
- #
462
- def add_mixin(name, mixin_module)
463
- name = name.to_s
464
- if @mixins.key?(name)
465
- raise ToolDefinitionError,
466
- "A mixin named #{name.inspect} has already been defined in tool" \
467
- " #{display_name.inspect}."
468
- end
469
- @mixins[name] = mixin_module
470
- self
471
- end
472
-
473
- ##
474
- # Add a named template class to this tool.
475
- #
476
- # @param [String] name The name of the template.
477
- # @param [Class] template_class The template class.
478
- #
479
- def add_template(name, template_class)
480
- name = name.to_s
481
- if @templates.key?(name)
482
- raise ToolDefinitionError,
483
- "A template named #{name.inspect} has already been defined in tool" \
484
- " #{display_name.inspect}."
485
- end
486
- @templates[name] = template_class
487
- self
488
- end
489
-
490
- ##
491
- # Disable argument parsing for this tool
492
- #
493
- def disable_argument_parsing
494
- check_definition_state
495
- if includes_arguments?
496
- raise ToolDefinitionError,
497
- "Cannot disable argument parsing for tool #{display_name.inspect}" \
498
- " because arguments have already been defined."
499
- end
500
- @disable_argument_parsing = true
501
- self
502
- end
503
-
504
- ##
505
- # Add a flag group to the group list.
506
- #
507
- # @param [Symbol] type The type of group. Allowed values: `:required`,
508
- # `:optional`, `:exactly_one`, `:at_most_one`, `:at_least_one`.
509
- # Default is `:optional`.
510
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
511
- # description for the group. See {Toys::Definition::Tool#desc=} for a
512
- # description of allowed formats. Defaults to `"Flags"`.
513
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
514
- # Long description for the flag group. See
515
- # {Toys::Definition::Tool#long_desc=} for a description of allowed
516
- # formats. Defaults to the empty array.
517
- # @param [String,Symbol,nil] name The name of the group, or nil for no
518
- # name.
519
- # @param [Boolean] report_collisions If `true`, raise an exception if a
520
- # the given name is already taken. If `false`, ignore. Default is
521
- # `true`.
522
- # @param [Boolean] prepend If `true`, prepend rather than append the
523
- # group to the list. Default is `false`.
524
- #
525
- def add_flag_group(type: :optional, desc: nil, long_desc: nil,
526
- name: nil, report_collisions: true, prepend: false)
527
- if !name.nil? && @flag_group_names.key?(name)
528
- return self unless report_collisions
529
- raise ToolDefinitionError, "Flag group #{name} already exists"
530
- end
531
- unless type.is_a?(::Class)
532
- type = Utils::ModuleLookup.to_module_name(type)
533
- type = Definition::FlagGroup.const_get(type)
534
- end
535
- group = type.new(name, desc, long_desc)
536
- @flag_group_names[name] = group unless name.nil?
537
- if prepend
538
- @flag_groups.unshift(group)
539
- else
540
- @flag_groups.push(group)
541
- end
542
- self
543
- end
544
-
545
- ##
546
- # Add a flag to the current tool. Each flag must specify a key which
547
- # the script may use to obtain the flag value from the context.
548
- # You may then provide the flags themselves in `OptionParser` form.
549
- #
550
- # @param [String,Symbol] key The key to use to retrieve the value from
551
- # the execution context.
552
- # @param [Array<String>] flags The flags in OptionParser format.
553
- # @param [Object] accept An acceptor that validates and/or converts the
554
- # value. You may provide either the name of an acceptor you have
555
- # defined, or one of the default acceptors provided by OptionParser.
556
- # Optional. If not specified, accepts any value as a string.
557
- # @param [Object] default The default value. This is the value that will
558
- # be set in the context if this flag is not provided on the command
559
- # line. Defaults to `nil`.
560
- # @param [Proc,nil] handler An optional handler for setting/updating the
561
- # value. If given, it should take two arguments, the new given value
562
- # and the previous value, and it should return the new value that
563
- # should be set. The default handler simply replaces the previous
564
- # value. i.e. the default is effectively `-> (val, _prev) { val }`.
565
- # @param [Boolean] report_collisions Raise an exception if a flag is
566
- # requested that is already in use or marked as disabled. Default is
567
- # true.
568
- # @param [Toys::Definition::FlagGroup,String,Symbol,nil] group Group for
569
- # this flag. You may provide a group name, a FlagGroup object, or
570
- # `nil` which denotes the default group.
571
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
572
- # description for the flag. See {Toys::Definition::Tool#desc=} for a
573
- # description of allowed formats. Defaults to the empty string.
574
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
575
- # Long description for the flag. See
576
- # {Toys::Definition::Tool#long_desc=} for a description of allowed
577
- # formats. Defaults to the empty array.
578
- # @param [String] display_name A display name for this flag, used in help
579
- # text and error messages.
580
- #
581
- def add_flag(key, flags = [],
582
- accept: nil, default: nil, handler: nil,
583
- report_collisions: true, group: nil,
584
- desc: nil, long_desc: nil, display_name: nil)
585
- unless group.is_a?(Definition::FlagGroup)
586
- group_name = group
587
- group = @flag_group_names[group_name]
588
- raise ToolDefinitionError, "No such flag group: #{group_name.inspect}" if group.nil?
589
- end
590
- check_definition_state(is_arg: true)
591
- accept = resolve_acceptor(accept)
592
- flag_def = Definition::Flag.new(key, flags, @used_flags, report_collisions,
593
- accept, handler, default, display_name, group)
594
- flag_def.desc = desc if desc
595
- flag_def.long_desc = long_desc if long_desc
596
- if flag_def.active?
597
- @flag_definitions << flag_def
598
- group << flag_def
599
- end
600
- @default_data[key] = default
601
- self
602
- end
603
-
604
- ##
605
- # Mark one or more flags as disabled, preventing their use by any
606
- # subsequent flag definition. This may be used to prevent middleware from
607
- # defining a particular flag.
608
- #
609
- # @param [String...] flags The flags to disable
610
- #
611
- def disable_flag(*flags)
612
- check_definition_state(is_arg: true)
613
- flags = flags.uniq
614
- intersection = @used_flags & flags
615
- unless intersection.empty?
616
- raise ToolDefinitionError, "Cannot disable flags already used: #{intersection.inspect}"
617
- end
618
- @used_flags.concat(flags)
619
- self
620
- end
621
-
622
- ##
623
- # Add a required positional argument to the current tool. You must specify
624
- # a key which the script may use to obtain the argument value from the
625
- # context.
626
- #
627
- # @param [String,Symbol] key The key to use to retrieve the value from
628
- # the execution context.
629
- # @param [Object] accept An acceptor that validates and/or converts the
630
- # value. You may provide either the name of an acceptor you have
631
- # defined, or one of the default acceptors provided by OptionParser.
632
- # Optional. If not specified, accepts any value as a string.
633
- # @param [String] display_name A name to use for display (in help text and
634
- # error reports). Defaults to the key in upper case.
635
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
636
- # description for the arg. See {Toys::Definition::Tool#desc=} for a
637
- # description of allowed formats. Defaults to the empty string.
638
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
639
- # Long description for the arg. See
640
- # {Toys::Definition::Tool#long_desc=} for a description of allowed
641
- # formats. Defaults to the empty array.
642
- #
643
- def add_required_arg(key, accept: nil, display_name: nil, desc: nil, long_desc: nil)
644
- check_definition_state(is_arg: true)
645
- accept = resolve_acceptor(accept)
646
- arg_def = Definition::Arg.new(key, :required, accept, nil, desc, long_desc, display_name)
647
- @required_arg_definitions << arg_def
648
- self
649
- end
650
-
651
- ##
652
- # Add an optional positional argument to the current tool. You must specify
653
- # a key which the script may use to obtain the argument value from the
654
- # context. If an optional argument is not given on the command line, the
655
- # value is set to the given default.
656
- #
657
- # @param [String,Symbol] key The key to use to retrieve the value from
658
- # the execution context.
659
- # @param [Object] default The default value. This is the value that will
660
- # be set in the context if this argument is not provided on the command
661
- # line. Defaults to `nil`.
662
- # @param [Object] accept An acceptor that validates and/or converts the
663
- # value. You may provide either the name of an acceptor you have
664
- # defined, or one of the default acceptors provided by OptionParser.
665
- # Optional. If not specified, accepts any value as a string.
666
- # @param [String] display_name A name to use for display (in help text and
667
- # error reports). Defaults to the key in upper case.
668
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
669
- # description for the arg. See {Toys::Definition::Tool#desc=} for a
670
- # description of allowed formats. Defaults to the empty string.
671
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
672
- # Long description for the arg. See
673
- # {Toys::Definition::Tool#long_desc=} for a description of allowed
674
- # formats. Defaults to the empty array.
675
- #
676
- def add_optional_arg(key, default: nil, accept: nil, display_name: nil,
677
- desc: nil, long_desc: nil)
678
- check_definition_state(is_arg: true)
679
- accept = resolve_acceptor(accept)
680
- arg_def = Definition::Arg.new(key, :optional, accept, default,
681
- desc, long_desc, display_name)
682
- @optional_arg_definitions << arg_def
683
- @default_data[key] = default
684
- self
685
- end
686
-
687
- ##
688
- # Specify what should be done with unmatched positional arguments. You must
689
- # specify a key which the script may use to obtain the remaining args
690
- # from the context.
691
- #
692
- # @param [String,Symbol] key The key to use to retrieve the value from
693
- # the execution context.
694
- # @param [Object] default The default value. This is the value that will
695
- # be set in the context if no unmatched arguments are provided on the
696
- # command line. Defaults to the empty array `[]`.
697
- # @param [Object] accept An acceptor that validates and/or converts the
698
- # value. You may provide either the name of an acceptor you have
699
- # defined, or one of the default acceptors provided by OptionParser.
700
- # Optional. If not specified, accepts any value as a string.
701
- # @param [String] display_name A name to use for display (in help text and
702
- # error reports). Defaults to the key in upper case.
703
- # @param [String,Array<String>,Toys::Utils::WrappableString] desc Short
704
- # description for the arg. See {Toys::Definition::Tool#desc=} for a
705
- # description of allowed formats. Defaults to the empty string.
706
- # @param [Array<String,Array<String>,Toys::Utils::WrappableString>] long_desc
707
- # Long description for the arg. See
708
- # {Toys::Definition::Tool#long_desc=} for a description of allowed
709
- # formats. Defaults to the empty array.
710
- #
711
- def set_remaining_args(key, default: [], accept: nil, display_name: nil,
712
- desc: nil, long_desc: nil)
713
- check_definition_state(is_arg: true)
714
- accept = resolve_acceptor(accept)
715
- arg_def = Definition::Arg.new(key, :remaining, accept, default,
716
- desc, long_desc, display_name)
717
- @remaining_args_definition = arg_def
718
- @default_data[key] = default
719
- self
720
- end
721
-
722
- ##
723
- # Set the runnable block
724
- #
725
- # @param [Proc] proc The runnable block
726
- #
727
- def runnable=(proc)
728
- @tool_class.to_run(&proc)
729
- end
730
-
731
- ##
732
- # Add an initializer.
733
- #
734
- # @param [Proc] proc The initializer block
735
- # @param [Object...] args Arguments to pass to the initializer
736
- #
737
- def add_initializer(proc, *args)
738
- check_definition_state
739
- @initializers << [proc, args]
740
- self
741
- end
742
-
743
- ##
744
- # Set the custom context directory.
745
- #
746
- # @param [String] dir
747
- #
748
- def custom_context_directory=(dir)
749
- check_definition_state
750
- @custom_context_directory = dir
751
- end
752
-
753
- ##
754
- # Return the effective context directory.
755
- # If there is a custom context directory, uses that. Otherwise, looks for
756
- # a custom context directory up the tool ancestor chain. If none is
757
- # found, uses the default context directory from the source info. It is
758
- # possible for there to be no context directory at all, in which case,
759
- # returns nil.
760
- #
761
- # @return [String,nil]
762
- #
763
- def context_directory
764
- lookup_custom_context_directory || source_info&.context_directory
765
- end
766
-
767
- ##
768
- # Lookup the custom context directory in this tool and its ancestors.
769
- # @private
770
- #
771
- def lookup_custom_context_directory
772
- custom_context_directory || @parent&.lookup_custom_context_directory
773
- end
774
-
775
- ##
776
- # Mark this tool as having at least one module included
777
- # @private
778
- #
779
- def mark_includes_modules
780
- check_definition_state
781
- @includes_modules = true
782
- self
783
- end
784
-
785
- ##
786
- # Complete definition and run middleware configs. Should be called from
787
- # the Loader only.
788
- # @private
789
- #
790
- def finish_definition(loader)
791
- unless @definition_finished
792
- ContextualError.capture("Error installing tool middleware!", tool_name: full_name) do
793
- config_proc = proc {}
794
- middleware_stack.reverse_each do |middleware|
795
- config_proc = make_config_proc(middleware, loader, config_proc)
796
- end
797
- config_proc.call
798
- end
799
- flag_groups.each do |flag_group|
800
- flag_group.flag_definitions.sort_by!(&:sort_str)
801
- end
802
- @definition_finished = true
803
- end
804
- self
805
- end
806
-
807
- ##
808
- # Run all initializers against a tool. Should be called from the Runner
809
- # only.
810
- # @private
811
- #
812
- def run_initializers(tool)
813
- @initializers.each do |func, args|
814
- tool.instance_exec(*args, &func)
815
- end
816
- end
817
-
818
- ##
819
- # Check that the tool can still be defined. Should be called internally
820
- # or from the DSL only.
821
- # @private
822
- #
823
- def check_definition_state(is_arg: false)
824
- if @definition_finished
825
- raise ToolDefinitionError,
826
- "Defintion of tool #{display_name.inspect} is already finished"
827
- end
828
- if is_arg && argument_parsing_disabled?
829
- raise ToolDefinitionError,
830
- "Tool #{display_name.inspect} has disabled argument parsing"
831
- end
832
- self
833
- end
834
-
835
- private
836
-
837
- def make_config_proc(middleware, loader, next_config)
838
- proc { middleware.config(self, loader, &next_config) }
839
- end
840
- end
841
- end
842
- end