toys-core 0.3.2

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.
@@ -0,0 +1,356 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ module Toys
31
+ ##
32
+ # This class defines the DSL for a toys configuration file.
33
+ #
34
+ # A toys configuration defines one or more named tools. It provides syntax
35
+ # for setting the description, defining switches and arguments, specifying
36
+ # how to execute the tool, and requesting helper modules and other services.
37
+ # It also lets you define subtools, nested arbitrarily deep, using blocks.
38
+ #
39
+ # Generally ConfigDSL is invoked from the {Loader}. Applications should not
40
+ # need to create instances of ConfigDSL directly.
41
+ #
42
+ # ## Simple example
43
+ #
44
+ # Create a file called `.toys.rb` in the current directory, with the
45
+ # following contents:
46
+ #
47
+ # tool "greet" do
48
+ # desc "Prints a simple greeting"
49
+ #
50
+ # optional_arg :recipient, default: "world"
51
+ #
52
+ # execute do
53
+ # puts "Hello, #{self[:recipient]}!"
54
+ # end
55
+ # end
56
+ #
57
+ # Now you can execute it using:
58
+ #
59
+ # toys greet
60
+ #
61
+ # or try:
62
+ #
63
+ # toys greet rubyists
64
+ #
65
+ class ConfigDSL
66
+ ##
67
+ # Create an instance of the DSL.
68
+ # @private
69
+ #
70
+ # @param [Array<String>] words Full name of the current tool.
71
+ # @param [Array<String>,nil] remaining_words Arguments remaining in the
72
+ # current lookup.
73
+ # @param [Integer] priority Priority of this configuration
74
+ # @param [Toys::Loader] loader Current active loader
75
+ # @param [String] path The path to the config file being evaluated
76
+ #
77
+ # @return [Toys::ConfigDSL]
78
+ #
79
+ def initialize(words, remaining_words, priority, loader, path)
80
+ @words = words
81
+ @remaining_words = remaining_words
82
+ @priority = priority
83
+ @loader = loader
84
+ @path = path
85
+ end
86
+
87
+ ##
88
+ # Create a subtool. You must provide a block defining the subtool.
89
+ #
90
+ # If the subtool is already defined (either as a tool or a group), the old
91
+ # definition is discarded and replaced with the new definition. If the old
92
+ # tool was a group, all its descendants are also discarded, recursively.
93
+ #
94
+ # @param [String] word The name of the subtool
95
+ #
96
+ def tool(word, &block)
97
+ word = word.to_s
98
+ subtool_words = @words + [word]
99
+ next_remaining = Loader.next_remaining_words(@remaining_words, word)
100
+ ConfigDSL.evaluate(subtool_words, next_remaining, @priority, @loader, @path, block)
101
+ self
102
+ end
103
+ alias name tool
104
+
105
+ ##
106
+ # Create an alias in the current group.
107
+ #
108
+ # @param [String] word The name of the alias
109
+ # @param [String] target The target of the alias
110
+ #
111
+ def alias_tool(word, target)
112
+ @loader.make_alias(@words + [word.to_s], @words + [target.to_s], @priority)
113
+ self
114
+ end
115
+
116
+ ##
117
+ # Create an alias of the current tool.
118
+ #
119
+ # @param [String] word The name of the alias
120
+ #
121
+ def alias_as(word)
122
+ if @words.empty?
123
+ raise ToolDefinitionError, "Cannot make an alias of the root."
124
+ end
125
+ @loader.make_alias(@words[0..-2] + [word.to_s], @words, @priority)
126
+ self
127
+ end
128
+
129
+ ##
130
+ # Include another config file or directory at the current location.
131
+ #
132
+ # @param [String] path The file or directory to include.
133
+ #
134
+ def include(path)
135
+ @loader.include_path(path, @words, @remaining_words, @priority)
136
+ self
137
+ end
138
+
139
+ ##
140
+ # Expand the given template in the current location.
141
+ #
142
+ # The template may be specified as a class or a well-known template name.
143
+ # You may also provide arguments to pass to the template.
144
+ #
145
+ # @param [Class,String,Symbol] template_class The template, either as a
146
+ # class or a well-known name.
147
+ # @param [Object...] args Template arguments
148
+ #
149
+ def expand(template_class, *args)
150
+ unless template_class.is_a?(::Class)
151
+ name = template_class.to_s
152
+ template_class = Templates.lookup(name)
153
+ if template_class.nil?
154
+ raise ToolDefinitionError, "Template not found: #{name.inspect}"
155
+ end
156
+ end
157
+ template = template_class.new(*args)
158
+ yield template if block_given?
159
+ instance_exec(template, &template_class.expander)
160
+ self
161
+ end
162
+
163
+ ##
164
+ # Set the long description for the current tool. The long description is
165
+ # displayed in the usage documentation for the tool itself.
166
+ #
167
+ # @param [String] desc The long description string.
168
+ #
169
+ def long_desc(desc)
170
+ return self if _cur_tool.nil?
171
+ _cur_tool.definition_path = @path
172
+ _cur_tool.long_desc = desc
173
+ self
174
+ end
175
+
176
+ ##
177
+ # Set the short description for the current tool. The short description is
178
+ # displayed with the tool in a command list. You may also use the
179
+ # equivalent method `short_desc`.
180
+ #
181
+ # @param [String] desc The short description string.
182
+ #
183
+ def desc(desc)
184
+ return self if _cur_tool.nil?
185
+ _cur_tool.definition_path = @path
186
+ _cur_tool.desc = desc
187
+ self
188
+ end
189
+ alias short_desc desc
190
+
191
+ ##
192
+ # Add a switch to the current tool. Each switch must specify a key which
193
+ # the executor may use to obtain the switch value from the context.
194
+ # You may then provide the switches themselves in `OptionParser` form.
195
+ #
196
+ # @param [Symbol] key The key to use to retrieve the value from the
197
+ # execution context.
198
+ # @param [String...] switches The switches in OptionParser format.
199
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
200
+ # @param [Object] default The default value. This is the value that will
201
+ # be set in the context if this switch is not provided on the command
202
+ # line. Defaults to `nil`.
203
+ # @param [String,nil] doc The documentation for the switch, which appears
204
+ # in the usage documentation. Defaults to `nil` for no documentation.
205
+ # @param [Boolean] only_unique If true, any switches that are already
206
+ # defined in this tool are removed from this switch. For example, if
207
+ # an earlier switch uses `-a`, and this switch wants to use both
208
+ # `-a` and `-b`, then only `-b` will be assigned to this switch.
209
+ # Defaults to false.
210
+ # @param [Proc,nil] handler An optional handler for setting/updating the
211
+ # value. If given, it should take two arguments, the new given value
212
+ # and the previous value, and it should return the new value that
213
+ # should be set. The default handler simply replaces the previous
214
+ # value. i.e. the default is effectively `-> (val, _prev) { val }`.
215
+ #
216
+ def switch(key, *switches,
217
+ accept: nil, default: nil, doc: nil, only_unique: false, handler: nil)
218
+ return self if _cur_tool.nil?
219
+ _cur_tool.definition_path = @path
220
+ _cur_tool.add_switch(key, *switches,
221
+ accept: accept, default: default, doc: doc,
222
+ only_unique: only_unique, handler: handler)
223
+ self
224
+ end
225
+
226
+ ##
227
+ # Add a required positional argument to the current tool. You must specify
228
+ # a key which the executor may use to obtain the argument value from the
229
+ # context.
230
+ #
231
+ # @param [Symbol] key The key to use to retrieve the value from the
232
+ # execution context.
233
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
234
+ # @param [String,nil] doc The documentation for the switch, which appears
235
+ # in the usage documentation. Defaults to `nil` for no documentation.
236
+ #
237
+ def required_arg(key, accept: nil, doc: nil)
238
+ return self if _cur_tool.nil?
239
+ _cur_tool.definition_path = @path
240
+ _cur_tool.add_required_arg(key, accept: accept, doc: doc)
241
+ self
242
+ end
243
+
244
+ ##
245
+ # Add an optional positional argument to the current tool. You must specify
246
+ # a key which the executor may use to obtain the argument value from the
247
+ # context. If an optional argument is not given on the command line, the
248
+ # value is set to the given default.
249
+ #
250
+ # @param [Symbol] key The key to use to retrieve the value from the
251
+ # execution context.
252
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
253
+ # @param [Object] default The default value. This is the value that will
254
+ # be set in the context if this argument is not provided on the command
255
+ # line. Defaults to `nil`.
256
+ # @param [String,nil] doc The documentation for the argument, which appears
257
+ # in the usage documentation. Defaults to `nil` for no documentation.
258
+ #
259
+ def optional_arg(key, accept: nil, default: nil, doc: nil)
260
+ return self if _cur_tool.nil?
261
+ _cur_tool.definition_path = @path
262
+ _cur_tool.add_optional_arg(key, accept: accept, default: default, doc: doc)
263
+ self
264
+ end
265
+
266
+ ##
267
+ # Specify what should be done with unmatched positional arguments. You must
268
+ # specify a key which the executor may use to obtain the remaining args
269
+ # from the context.
270
+ #
271
+ # @param [Symbol] key The key to use to retrieve the value from the
272
+ # execution context.
273
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
274
+ # @param [Object] default The default value. This is the value that will
275
+ # be set in the context if no unmatched arguments are provided on the
276
+ # command line. Defaults to the empty array `[]`.
277
+ # @param [String,nil] doc The documentation for the remaining arguments,
278
+ # which appears in the usage documentation. Defaults to `nil` for no
279
+ # documentation.
280
+ #
281
+ def remaining_args(key, accept: nil, default: [], doc: nil)
282
+ return self if _cur_tool.nil?
283
+ _cur_tool.definition_path = @path
284
+ _cur_tool.set_remaining_args(key, accept: accept, default: default, doc: doc)
285
+ self
286
+ end
287
+
288
+ ##
289
+ # Specify the executor for this tool. This is a block that will be called,
290
+ # with `self` set to a {Toys::Context}.
291
+ #
292
+ def execute(&block)
293
+ return self if _cur_tool.nil?
294
+ _cur_tool.definition_path = @path
295
+ _cur_tool.executor = block
296
+ self
297
+ end
298
+
299
+ ##
300
+ # Define a helper method that may be called from this tool's executor.
301
+ # You must provide a name for the method, and a block for the method
302
+ # definition.
303
+ #
304
+ # @param [String,Symbol] name Name of the method. May not begin with an
305
+ # underscore.
306
+ #
307
+ def helper(name, &block)
308
+ return self if _cur_tool.nil?
309
+ _cur_tool.definition_path = @path
310
+ _cur_tool.add_helper(name, &block)
311
+ self
312
+ end
313
+
314
+ ##
315
+ # Specify that the given module should be mixed in to this tool's executor.
316
+ # Effectively, the module is added to the {Toys::Context} object.
317
+ # You may either provide a module directly, or specify the name of a
318
+ # well-known module.
319
+ #
320
+ # @param [Module,Symbol] mod Module or name of well-known module.
321
+ #
322
+ def use(mod)
323
+ return self if _cur_tool.nil?
324
+ _cur_tool.definition_path = @path
325
+ _cur_tool.use_module(mod)
326
+ self
327
+ end
328
+
329
+ ## @private
330
+ def _binding
331
+ binding
332
+ end
333
+
334
+ ## @private
335
+ def _cur_tool
336
+ unless defined? @_cur_tool
337
+ @_cur_tool = @loader.get_or_create_tool(@words, priority: @priority)
338
+ end
339
+ @_cur_tool
340
+ end
341
+
342
+ ## @private
343
+ def self.evaluate(words, remaining_words, priority, loader, path, source)
344
+ dsl = new(words, remaining_words, priority, loader, path)
345
+ case source
346
+ when String
347
+ # rubocop:disable Security/Eval
348
+ eval(source, dsl._binding, path, 1)
349
+ # rubocop:enable Security/Eval
350
+ when ::Proc
351
+ dsl.instance_eval(&source)
352
+ end
353
+ nil
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,278 @@
1
+ # Copyright 2018 Daniel Azuma
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder, nor the names of any other
14
+ # contributors to this software, may be used to endorse or promote products
15
+ # derived from this software without specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+ ;
29
+
30
+ require "logger"
31
+
32
+ module Toys
33
+ ##
34
+ # The object context in effect during the execution of a tool.
35
+ #
36
+ # The context is generally a hash of key-value pairs.
37
+ # Keys that begin with two underscores are reserved common elements of the
38
+ # context such as the tool being executed, or the verbosity level.
39
+ # Other keys are available for use by your tool. Generally, they are set
40
+ # by switches and arguments in your tool. Context values may also be set
41
+ # by middleware. By convention, middleware-set keys begin with a single
42
+ # underscore.
43
+ #
44
+ class Context
45
+ ##
46
+ # Context key for the verbosity value. Verbosity is an integer defaulting
47
+ # to 0, with higher values meaning more verbose and lower meaning quieter.
48
+ # @return [Symbol]
49
+ #
50
+ VERBOSITY = :__verbosity
51
+
52
+ ##
53
+ # Context key for the `Toys::Tool` object being executed.
54
+ # @return [Symbol]
55
+ #
56
+ TOOL = :__tool
57
+
58
+ ##
59
+ # Context key for the full name of the tool being executed. Value is an
60
+ # array of strings.
61
+ # @return [Symbol]
62
+ #
63
+ TOOL_NAME = :__tool_name
64
+
65
+ ##
66
+ # Context key for the active `Toys::Loader` object.
67
+ # @return [Symbol]
68
+ #
69
+ LOADER = :__loader
70
+
71
+ ##
72
+ # Context key for the active `Logger` object.
73
+ # @return [Symbol]
74
+ #
75
+ LOGGER = :__logger
76
+
77
+ ##
78
+ # Context key for the name of the toys binary. Value is a string.
79
+ # @return [Symbol]
80
+ #
81
+ BINARY_NAME = :__binary_name
82
+
83
+ ##
84
+ # Context key for the argument list passed to the current tool. Value is
85
+ # an array of strings.
86
+ # @return [Symbol]
87
+ #
88
+ ARGS = :__args
89
+
90
+ ##
91
+ # Context key for the usage error raised. Value is a string if there was
92
+ # an error, or nil if there was no error.
93
+ # @return [Symbol]
94
+ #
95
+ USAGE_ERROR = :__usage_error
96
+
97
+ ##
98
+ # Context key for whether nonzero exit codes from subprocesses should cause
99
+ # an immediate exit. Value is a truthy or falsy value.
100
+ # @return [Symbol]
101
+ #
102
+ EXIT_ON_NONZERO_STATUS = :__exit_on_nonzero_status
103
+
104
+ ##
105
+ # Create a Context object. Applications generally will not need to create
106
+ # these objects directly; they are created by the tool when it is preparing
107
+ # for execution.
108
+ # @private
109
+ #
110
+ # @param [Toys::CLI] cli
111
+ # @param [Hash] data
112
+ #
113
+ def initialize(cli, data)
114
+ @_cli = cli
115
+ @_data = data
116
+ @_data[LOADER] = cli.loader
117
+ @_data[BINARY_NAME] = cli.binary_name
118
+ @_data[LOGGER] = cli.logger
119
+ end
120
+
121
+ ##
122
+ # Return the verbosity as an integer.
123
+ # @return [Integer]
124
+ #
125
+ def verbosity
126
+ @_data[VERBOSITY]
127
+ end
128
+
129
+ ##
130
+ # Return the tool being executed.
131
+ # @return [Toys::Tool]
132
+ #
133
+ def tool
134
+ @_data[TOOL]
135
+ end
136
+
137
+ ##
138
+ # Return the name of the tool being executed, as an array of strings.
139
+ # @return [Array[String]]
140
+ #
141
+ def tool_name
142
+ @_data[TOOL_NAME]
143
+ end
144
+
145
+ ##
146
+ # Return the raw arguments passed to the tool, as an array of strings.
147
+ # This does not include the tool name itself.
148
+ # @return [Array[String]]
149
+ #
150
+ def args
151
+ @_data[ARGS]
152
+ end
153
+
154
+ ##
155
+ # Return any usage error detected during argument parsing, or `nil` if
156
+ # no error was detected.
157
+ # @return [String,nil]
158
+ #
159
+ def usage_error
160
+ @_data[USAGE_ERROR]
161
+ end
162
+
163
+ ##
164
+ # Return the logger for this execution.
165
+ # @return [Logger]
166
+ #
167
+ def logger
168
+ @_data[LOGGER]
169
+ end
170
+
171
+ ##
172
+ # Return the active loader that can be used to get other tools.
173
+ # @return [Toys::Loader]
174
+ #
175
+ def loader
176
+ @_data[LOADER]
177
+ end
178
+
179
+ ##
180
+ # Return the name of the binary that was executed.
181
+ # @return [String]
182
+ #
183
+ def binary_name
184
+ @_data[BINARY_NAME]
185
+ end
186
+
187
+ ##
188
+ # Return an option or other piece of data by key.
189
+ #
190
+ # @param [Symbol] key
191
+ # @return [Object]
192
+ #
193
+ def [](key)
194
+ @_data[key]
195
+ end
196
+ alias get []
197
+
198
+ ##
199
+ # Set an option or other piece of data by key.
200
+ #
201
+ # @param [Symbol] key
202
+ # @param [Object] value
203
+ #
204
+ def []=(key, value)
205
+ @_data[key] = value
206
+ end
207
+
208
+ ##
209
+ # Set an option or other piece of data by key.
210
+ #
211
+ # @param [Symbol] key
212
+ # @param [Object] value
213
+ #
214
+ def set(key, value = nil)
215
+ if key.is_a?(::Hash)
216
+ @_data.merge!(key)
217
+ else
218
+ @_data[key] = value
219
+ end
220
+ self
221
+ end
222
+
223
+ ##
224
+ # Returns the subset of the context that does not include well-known keys
225
+ # such as tool and verbosity. Technically, this includes all keys that do
226
+ # not begin with two underscores.
227
+ #
228
+ # @return [Hash]
229
+ #
230
+ def options
231
+ @_data.select do |k, _v|
232
+ !k.is_a?(::Symbol) || !k.to_s.start_with?("__")
233
+ end
234
+ end
235
+
236
+ ##
237
+ # Execute another tool, given by the provided arguments.
238
+ #
239
+ # @param [String...] args Command line arguments defining another tool
240
+ # to run, along with parameters and switches.
241
+ # @param [Toys::CLI,nil] cli The CLI to use to execute the tool. If `nil`
242
+ # (the default), uses the current CLI.
243
+ # @param [Boolean] exit_on_nonzero_status If true, exit immediately if the
244
+ # run returns a nonzero error code.
245
+ # @return [Integer] The resulting status code
246
+ #
247
+ def run(*args, cli: nil, exit_on_nonzero_status: nil)
248
+ cli ||= @_cli
249
+ exit_on_nonzero_status = @_data[EXIT_ON_NONZERO_STATUS] if exit_on_nonzero_status.nil?
250
+ code = cli.run(args.flatten, verbosity: @_data[VERBOSITY])
251
+ exit(code) if exit_on_nonzero_status && !code.zero?
252
+ code
253
+ end
254
+
255
+ ##
256
+ # Return a new CLI with the same settings as the currnet CLI but no paths.
257
+ # This can be used to run a toys "sub-instance". Add any new paths to the
258
+ # returned CLI, then call {#run}, passing in the CLI, to execute a tool.
259
+ #
260
+ # @return [Toys::CLI]
261
+ #
262
+ def new_cli
263
+ cli = @_cli.empty_clone
264
+ yield cli if block_given?
265
+ cli
266
+ end
267
+
268
+ ##
269
+ # Exit immediately with the given status code
270
+ #
271
+ # @param [Integer] code The status code, which should be 0 for no error,
272
+ # or nonzero for an error condition.
273
+ #
274
+ def exit(code)
275
+ throw :result, code
276
+ end
277
+ end
278
+ end