toys 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1303b77289f7e37c45866ec22513df6187556c2d7188b97d2706731b73377c5
4
- data.tar.gz: 31c4013f235e7fb83575c9b031798dc410ab0bdee2395303f3f54e54fc870c05
3
+ metadata.gz: 1dcbd6fabf227072fda60d4acebcc09ae1a8af3d10db580bcea3d4a56a6623fd
4
+ data.tar.gz: bdc7a717793c190af3cb0c470cfbd97078779478e1528297397303c88af1b6a3
5
5
  SHA512:
6
- metadata.gz: 8389b529719c6135483a52a65ec2521e90964be02fac0da56fa7016672da8f4f9a8f7ad24c8a0493db8558040c3c80e47553656f3a567524f4a7e310669cd05a
7
- data.tar.gz: 708270b18a969b0adb5aa16c9b517966d82a3531a4a88604b7ee3cae27ecb67c839ef40318378c89d6a3347658c6d3fe590a6482177d0a899d96eaf0847f6366
6
+ metadata.gz: 74b91bdd53c8ca7aec7ba1b1d5ed9d98e2f3a826846ea9d41975391d504e14047d25f6db9551318568fabb81194dd3bf896183316a50e2fe63f92a4e0811ee9c
7
+ data.tar.gz: 81617b1a8aff51c05cae72cc79e4fffd5030af0176f50f389e3e235334161efbbb9f6ca60e5273f15a9a00cf76bbbfc675aec0a906c87110991e760c9ff1e2da
@@ -0,0 +1,11 @@
1
+ --no-private
2
+ --title=Toys
3
+ --markup=markdown
4
+ --embed-mixin=ClassMethods
5
+ --main=README.md
6
+ --exclude=lib/toys/builtins
7
+ ./lib/**/*.rb
8
+ -
9
+ README.md
10
+ LICENSE.md
11
+ CHANGELOG.md
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Toys
2
2
 
3
+ [![Travis-CI Build Status](https://travis-ci.org/dazuma/toys.svg)](https://travis-ci.org/dazuma/toys/)
4
+
3
5
  Toys is a Ruby library and command line tool that lets you build your own
4
6
  command line suite of tools (with commands and subcommands) using a Ruby DSL.
5
7
  You can define commands globally or configure special commands scoped to
@@ -29,11 +31,12 @@ The source can be found on Github at
29
31
 
30
32
  ### TODO items
31
33
 
32
- * Document methods
33
- * Write overall documentation
34
34
  * Decide about highline integration
35
35
  * Output formats middleware
36
36
  * System paths tool
37
+ * Search function in group help
38
+ * Split out toys-core gem
39
+ * Write overall documentation
37
40
 
38
41
  ## License
39
42
 
@@ -35,13 +35,13 @@
35
35
  #
36
36
  module Toys
37
37
  ##
38
- # Namespace for common utility classes
38
+ # Namespace for common utility classes.
39
39
  #
40
40
  module Utils; end
41
41
  end
42
42
 
43
- require "toys/builder"
44
43
  require "toys/cli"
44
+ require "toys/config_dsl"
45
45
  require "toys/context"
46
46
  require "toys/errors"
47
47
  require "toys/helpers"
@@ -43,6 +43,7 @@ tool "update" do
43
43
  use :exec
44
44
 
45
45
  execute do
46
+ logger.info "Checking rubygems for the latest toys release..."
46
47
  version_info = capture("gem query -q -r -e toys")
47
48
  if version_info =~ /toys\s\((.+)\)/
48
49
  latest_version = ::Gem::Version.new($1)
@@ -50,6 +51,9 @@ tool "update" do
50
51
  if latest_version > cur_version
51
52
  logger.warn("Updating toys from #{cur_version} to #{latest_version}...")
52
53
  sh("gem install toys")
54
+ elsif latest_version < cur_version
55
+ logger.warn("Toys is already at experimental version #{cur_version}, which is later than" \
56
+ " the latest released version #{latest_version}")
53
57
  else
54
58
  logger.warn("Toys is already at the latest version: #{latest_version}")
55
59
  end
@@ -31,7 +31,9 @@ require "logger"
31
31
 
32
32
  module Toys
33
33
  ##
34
- # A Toys-based CLI
34
+ # A Toys-based CLI.
35
+ #
36
+ # Use this class to implement a CLI using Toys.
35
37
  #
36
38
  class CLI
37
39
  ##
@@ -75,6 +77,38 @@ module Toys
75
77
  " globally or scoped to specific directories that you choose." \
76
78
  " For detailed information, see https://www.rubydoc.info/gems/toys".freeze
77
79
 
80
+ ##
81
+ # Create a CLI
82
+ #
83
+ # @param [String,nil] binary_name The binary name displayed in help text.
84
+ # Optional. Defaults to the ruby program name.
85
+ # @param [Logger,nil] logger The logger to use. If not provided, a default
86
+ # logger that writes to `STDERR` is used.
87
+ # @param [String,nil] config_dir_name A directory with this name that
88
+ # appears in the loader path, is treated as a configuration directory
89
+ # whose contents are loaded into the toys configuration. Optional.
90
+ # If not provided, toplevel configuration directories are disabled.
91
+ # The default toys CLI sets this to `".toys"`.
92
+ # @param [String,nil] config_file_name A file with this name that appears
93
+ # in the loader path, is treated as a toplevel configuration file
94
+ # whose contents are loaded into the toys configuration. Optional.
95
+ # If not provided, toplevel configuration files are disabled.
96
+ # The default toys CLI sets this to `".toys.rb"`.
97
+ # @param [String,nil] index_file_name A file with this name that appears
98
+ # in any configuration directory (not just a toplevel directory) is
99
+ # loaded first as a standalone configuration file. If not provided,
100
+ # standalone configuration files are disabled.
101
+ # The default toys CLI sets this to `".toys.rb"`.
102
+ # @param [String,nil] preload_file_name A file with this name that appears
103
+ # in any configuration directory (not just a toplevel directory) is
104
+ # loaded before any configuration files. It is not treated as a
105
+ # configuration file in that the configuration DSL is not honored. You
106
+ # may use such a file to define auxiliary Ruby modules and classes that
107
+ # used by the tools defined in that directory.
108
+ # @param [Array] middleware An array of middleware that will be used by
109
+ # default for all tools loaded by this CLI.
110
+ # @param [String] root_desc The description of the root tool.
111
+ #
78
112
  def initialize(
79
113
  binary_name: nil,
80
114
  logger: nil,
@@ -97,16 +131,41 @@ module Toys
97
131
  @context_base = Context::Base.new(@loader, binary_name, logger)
98
132
  end
99
133
 
134
+ ##
135
+ # Add one or more configuration files/directories to the loader.
136
+ #
137
+ # If a CLI has a default tool set, it might use this to point to the
138
+ # directory that defines those tools. For example, the default Toys CLI
139
+ # uses this to load the builtin tools from the `builtins` directory.
140
+ #
141
+ # @param [String,Array<String>] paths One or more paths to add.
142
+ #
100
143
  def add_config_paths(paths)
101
144
  @loader.add_config_paths(paths)
102
145
  self
103
146
  end
104
147
 
148
+ ##
149
+ # Add one or more path directories to the loader. These directories are
150
+ # searched for config directories and config files. Typically a CLI may
151
+ # include the current directory, or the user's home directory, `/etc` or
152
+ # other configuration-centric directories here.
153
+ #
154
+ # @param [String,Array<String>] paths One or more paths to add.
155
+ #
105
156
  def add_paths(paths)
106
157
  @loader.add_paths(paths)
107
158
  self
108
159
  end
109
160
 
161
+ ##
162
+ # Add the given path and all ancestor directories to the loader as paths.
163
+ # You may optionally provide a stopping point using the `base` argument,
164
+ # which, if present, will be the _last_ directory added.
165
+ #
166
+ # @param [String] path The first directory to add
167
+ # @param [String] base The last directory to add. Defaults to `"/"`.
168
+ #
110
169
  def add_path_hierarchy(path = nil, base = "/")
111
170
  path ||= ::Dir.pwd
112
171
  paths = []
@@ -121,6 +180,11 @@ module Toys
121
180
  self
122
181
  end
123
182
 
183
+ ##
184
+ # Add a standard set of paths. This includes the contents of the
185
+ # `TOYS_PATH` environment variable if present, the current user's home
186
+ # directory, and any system configuration directories such as `/etc`.
187
+ #
124
188
  def add_standard_paths
125
189
  toys_path = ::ENV["TOYS_PATH"].to_s.split(::File::PATH_SEPARATOR)
126
190
  if toys_path.empty?
@@ -131,11 +195,19 @@ module Toys
131
195
  self
132
196
  end
133
197
 
198
+ ##
199
+ # Run the CLI with the given command line arguments.
200
+ #
134
201
  def run(*args)
135
202
  exit(@context_base.run(args.flatten, verbosity: 0))
136
203
  end
137
204
 
138
205
  class << self
206
+ ##
207
+ # Configure and create the standard Toys CLI.
208
+ #
209
+ # @return [Toys::CLI]
210
+ #
139
211
  def create_standard
140
212
  cli = new(
141
213
  binary_name: DEFAULT_BINARY_NAME,
@@ -152,15 +224,30 @@ module Toys
152
224
  cli
153
225
  end
154
226
 
227
+ ##
228
+ # Returns a default set of middleware used by the standard Toys CLI.
229
+ # This middleware handles usage errors, provides a behavior for groups
230
+ # that displays the group command list, provides a `--help` option for
231
+ # showing individual tool documentation, and provides `--verbose` and
232
+ # `--quiet` switches for setting the verbosity, which in turn controls
233
+ # the logger level.
234
+ #
235
+ # @return [Array]
236
+ #
155
237
  def default_middleware_stack
156
238
  [
157
239
  Middleware.lookup(:show_usage_errors).new,
158
- Middleware.lookup(:group_default).new,
159
- Middleware.lookup(:show_tool_help).new,
240
+ Middleware.lookup(:show_group_usage).new,
241
+ Middleware.lookup(:show_tool_usage).new,
160
242
  Middleware.lookup(:set_verbosity).new
161
243
  ]
162
244
  end
163
245
 
246
+ ##
247
+ # Returns a default logger that logs to `STDERR`.
248
+ #
249
+ # @return [Logger]
250
+ #
164
251
  def default_logger
165
252
  logger = ::Logger.new(::STDERR)
166
253
  logger.formatter = proc do |severity, time, _progname, msg|
@@ -0,0 +1,432 @@
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 [String] path The path to the config file being evaluated
71
+ # @param [Toys::Tool] tool The tool being defined at the top level
72
+ # @param [Array<String>,nil] remaining_words Arguments remaining in the
73
+ # current lookup.
74
+ # @param [Integer] priority Priority of this configuration
75
+ # @param [Toys::Loader] loader Current active loader
76
+ # @param [:tool,:append,:group] type Type of tool being configured
77
+ #
78
+ # @return [Toys::ConfigDSL]
79
+ #
80
+ def initialize(path, tool, remaining_words, priority, loader, type)
81
+ @path = path
82
+ @tool = tool
83
+ @remaining_words = remaining_words
84
+ @priority = priority
85
+ @loader = loader
86
+ @type = type
87
+ end
88
+
89
+ ##
90
+ # Create a subtool.
91
+ #
92
+ # If the subtool is not an alias, you must provide a block defining the
93
+ # subtool.
94
+ #
95
+ # If the subtool is already defined (either as a tool or a group), the old
96
+ # definition is discarded and replaced with the new definition. If the old
97
+ # tool was a group, all its descendants are also discarded, recursively.
98
+ #
99
+ # @param [String] word The name of the subtool
100
+ # @param [String,nil] alias_of If set, this subtool is set to be an alias
101
+ # of the given subtool name. Defaults to `nil`, indicating the subtool
102
+ # is not an alias.
103
+ #
104
+ def tool(word, alias_of: nil, &block)
105
+ word = word.to_s
106
+ subtool = @loader.get_or_create_tool(@tool.full_name + [word], @priority, assume_parent: true)
107
+ return self if subtool.nil?
108
+ if alias_of
109
+ if block
110
+ raise ToolDefinitionError, "Cannot take a block with alias_of"
111
+ end
112
+ subtool.make_alias_of_word(alias_of.to_s)
113
+ return self
114
+ end
115
+ next_remaining = Loader.next_remaining_words(@remaining_words, word)
116
+ ConfigDSL.evaluate(@path, subtool, next_remaining, @priority, @loader, :tool, block)
117
+ self
118
+ end
119
+ alias name tool
120
+
121
+ ##
122
+ # Append subtools to an existing group.
123
+ #
124
+ # Pass a group name to this method to "reopen" that group. You must provide
125
+ # a block. In that block, you may not modify any properties of the group
126
+ # itself, but you may add or replace subtools within the group.
127
+ #
128
+ # @param [String] word The name of the group.
129
+ #
130
+ def append(word, &block)
131
+ word = word.to_s
132
+ subtool = @loader.get_or_create_tool(@tool.full_name + [word], nil, assume_parent: true)
133
+ next_remaining = Loader.next_remaining_words(@remaining_words, word)
134
+ ConfigDSL.evaluate(@path, subtool, next_remaining, @priority, @loader, :append, block)
135
+ self
136
+ end
137
+
138
+ ##
139
+ # Create a group subtool. You must provide a block defining the group's
140
+ # properties and contents.
141
+ #
142
+ # If the subtool is already defined (either as a tool or a group), the old
143
+ # definition is discarded and replaced with the new definition.
144
+ #
145
+ # @param [String] word The name of the group
146
+ #
147
+ def group(word, &block)
148
+ word = word.to_s
149
+ subtool = @loader.get_or_create_tool(@tool.full_name + [word], @priority, assume_parent: true)
150
+ return self if subtool.nil?
151
+ next_remaining = Loader.next_remaining_words(@remaining_words, word)
152
+ ConfigDSL.evaluate(@path, subtool, next_remaining, @priority, @loader, :group, block)
153
+ self
154
+ end
155
+
156
+ ##
157
+ # Create an alias of the current tool.
158
+ #
159
+ # @param [String] word The name of the alias
160
+ #
161
+ def alias_as(word)
162
+ if @tool.root?
163
+ raise ToolDefinitionError, "Cannot make an alias of the root tool"
164
+ end
165
+ if @type == :group || @type == :append
166
+ raise ToolDefinitionError, "Cannot make an alias of a group"
167
+ end
168
+ alias_name = @tool.full_name.slice(0..-2) + [word.to_s]
169
+ alias_tool = @loader.get_or_create_tool(alias_name, @priority)
170
+ alias_tool.make_alias_of(@tool.simple_name) if alias_tool
171
+ self
172
+ end
173
+
174
+ ##
175
+ # Make the current tool an alias of the given sibling
176
+ #
177
+ # @param [String] word The name of the sibling tool to alias
178
+ #
179
+ def alias_of(word)
180
+ if @tool.root?
181
+ raise ToolDefinitionError, "Cannot make the root tool an alias"
182
+ end
183
+ if @type == :group || @type == :append
184
+ raise ToolDefinitionError, "Cannot make a group an alias"
185
+ end
186
+ @tool.make_alias_of(word.to_s)
187
+ self
188
+ end
189
+
190
+ ##
191
+ # Include another config file or directory at the current location.
192
+ #
193
+ # @param [String] path The file or directory to include.
194
+ #
195
+ def include(path)
196
+ @tool.yield_definition do
197
+ @loader.include_path(path, @tool.full_name, @remaining_words, @priority)
198
+ end
199
+ self
200
+ end
201
+
202
+ ##
203
+ # Expand the given template in the current location.
204
+ #
205
+ # The template may be specified as a class or a well-known template name.
206
+ # You may also provide arguments to pass to the template.
207
+ #
208
+ # @param [Class,String,Symbol] template_class The template, either as a
209
+ # class or a well-known name.
210
+ # @param [Object...] args Template arguments
211
+ #
212
+ def expand(template_class, *args)
213
+ unless template_class.is_a?(::Class)
214
+ name = template_class.to_s
215
+ template_class = Templates.lookup(name)
216
+ if template_class.nil?
217
+ raise ToolDefinitionError, "Template not found: #{name.inspect}"
218
+ end
219
+ end
220
+ template = template_class.new(*args)
221
+ yield template if block_given?
222
+ instance_exec(template, &template_class.expander)
223
+ self
224
+ end
225
+
226
+ ##
227
+ # Set the long description for the current tool. The long description is
228
+ # displayed in the usage documentation for the tool itself.
229
+ #
230
+ # @param [String] desc The long description string.
231
+ #
232
+ def long_desc(desc)
233
+ if @type == :append
234
+ raise ToolDefinitionError, "Cannot set the description when appending"
235
+ end
236
+ @tool.long_desc = desc
237
+ self
238
+ end
239
+
240
+ ##
241
+ # Set the short description for the current tool. The short description is
242
+ # displayed with the tool in a command list. You may also use the
243
+ # equivalent method `short_desc`.
244
+ #
245
+ # @param [String] desc The short description string.
246
+ #
247
+ def desc(desc)
248
+ if @type == :append
249
+ raise ToolDefinitionError, "Cannot set the description when appending"
250
+ end
251
+ @tool.desc = desc
252
+ self
253
+ end
254
+ alias short_desc desc
255
+
256
+ ##
257
+ # Add a switch to the current tool. Each switch must specify a key which
258
+ # the executor may use to obtain the switch value from the context.
259
+ # You may then provide the switches themselves in `OptionParser` form.
260
+ #
261
+ # @param [Symbol] key The key to use to retrieve the value from the
262
+ # execution context.
263
+ # @param [String...] switches The switches in OptionParser format.
264
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
265
+ # @param [Object] default The default value. This is the value that will
266
+ # be set in the context if this switch is not provided on the command
267
+ # line. Defaults to `nil`.
268
+ # @param [String,nil] doc The documentation for the switch, which appears
269
+ # in the usage documentation. Defaults to `nil` for no documentation.
270
+ # @param [Boolean] only_unique If true, any switches that are already
271
+ # defined in this tool are removed from this switch. For example, if
272
+ # an earlier switch uses `-a`, and this switch wants to use both
273
+ # `-a` and `-b`, then only `-b` will be assigned to this switch.
274
+ # Defaults to false.
275
+ # @param [Proc,nil] handler An optional handler for setting/updating the
276
+ # value. If given, it should take two arguments, the new given value
277
+ # and the previous value, and it should return the new value that
278
+ # should be set. The default handler simply replaces the previous
279
+ # value. i.e. the default is effectively `-> (val, _prev) { val }`.
280
+ #
281
+ def switch(key, *switches,
282
+ accept: nil, default: nil, doc: nil, only_unique: false, handler: nil)
283
+ if @type == :append
284
+ raise ToolDefinitionError, "Cannot add a switch when appending"
285
+ end
286
+ @tool.add_switch(key, *switches,
287
+ accept: accept, default: default, doc: doc,
288
+ only_unique: only_unique, handler: handler)
289
+ self
290
+ end
291
+
292
+ ##
293
+ # Add a required positional argument to the current tool. You must specify
294
+ # a key which the executor may use to obtain the argument value from the
295
+ # context.
296
+ #
297
+ # @param [Symbol] key The key to use to retrieve the value from the
298
+ # execution context.
299
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
300
+ # @param [String,nil] doc The documentation for the switch, which appears
301
+ # in the usage documentation. Defaults to `nil` for no documentation.
302
+ #
303
+ def required_arg(key, accept: nil, doc: nil)
304
+ if @type == :append
305
+ raise ToolDefinitionError, "Cannot add an argument when appending"
306
+ end
307
+ @tool.add_required_arg(key, accept: accept, doc: doc)
308
+ self
309
+ end
310
+
311
+ ##
312
+ # Add an optional positional argument to the current tool. You must specify
313
+ # a key which the executor may use to obtain the argument value from the
314
+ # context. If an optional argument is not given on the command line, the
315
+ # value is set to the given default.
316
+ #
317
+ # @param [Symbol] key The key to use to retrieve the value from the
318
+ # execution context.
319
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
320
+ # @param [Object] default The default value. This is the value that will
321
+ # be set in the context if this argument is not provided on the command
322
+ # line. Defaults to `nil`.
323
+ # @param [String,nil] doc The documentation for the argument, which appears
324
+ # in the usage documentation. Defaults to `nil` for no documentation.
325
+ #
326
+ def optional_arg(key, accept: nil, default: nil, doc: nil)
327
+ if @type == :append
328
+ raise ToolDefinitionError, "Cannot add an argument when appending"
329
+ end
330
+ @tool.add_optional_arg(key, accept: accept, default: default, doc: doc)
331
+ self
332
+ end
333
+
334
+ ##
335
+ # Specify what should be done with unmatched positional arguments. You must
336
+ # specify a key which the executor may use to obtain the remaining args
337
+ # from the context.
338
+ #
339
+ # @param [Symbol] key The key to use to retrieve the value from the
340
+ # execution context.
341
+ # @param [Object,nil] accept An OptionParser acceptor. Optional.
342
+ # @param [Object] default The default value. This is the value that will
343
+ # be set in the context if no unmatched arguments are provided on the
344
+ # command line. Defaults to the empty array `[]`.
345
+ # @param [String,nil] doc The documentation for the remaining arguments,
346
+ # which appears in the usage documentation. Defaults to `nil` for no
347
+ # documentation.
348
+ #
349
+ def remaining_args(key, accept: nil, default: [], doc: nil)
350
+ if @type == :append
351
+ raise ToolDefinitionError, "Cannot add an argument when appending"
352
+ end
353
+ @tool.set_remaining_args(key, accept: accept, default: default, doc: doc)
354
+ self
355
+ end
356
+
357
+ ##
358
+ # Specify the executor for this tool. This is a block that will be called,
359
+ # with `self` set to a {Toys::Context}.
360
+ #
361
+ def execute(&block)
362
+ if @type == :group || @type == :append
363
+ raise ToolDefinitionError, "Cannot set the executor of a group"
364
+ end
365
+ @tool.executor = block
366
+ self
367
+ end
368
+
369
+ ##
370
+ # Define a helper method that may be called from this tool's executor.
371
+ # You must provide a name for the method, and a block for the method
372
+ # definition.
373
+ #
374
+ # @param [String,Symbol] name Name of the method. May not begin with an
375
+ # underscore.
376
+ #
377
+ def helper(name, &block)
378
+ if @type == :group || @type == :append
379
+ raise ToolDefinitionError, "Cannot define a helper method to a group"
380
+ end
381
+ @tool.add_helper(name, &block)
382
+ self
383
+ end
384
+
385
+ ##
386
+ # Specify that the given module should be mixed in to this tool's executor.
387
+ # Effectively, the module is added to the {Toys::Context} object.
388
+ # You may either provide a module directly, or specify the name of a
389
+ # well-known module.
390
+ #
391
+ # @param [Module,Symbol] mod Module or name of well-known module.
392
+ #
393
+ def use(mod)
394
+ if @type == :group || @type == :append
395
+ raise ToolDefinitionError, "Cannot use a helper module in a group"
396
+ end
397
+ @tool.use_module(mod)
398
+ self
399
+ end
400
+
401
+ ## @private
402
+ def _binding
403
+ binding
404
+ end
405
+
406
+ ## @private
407
+ def self.evaluate(path, tool, remaining_words, priority, loader, type, source)
408
+ dsl = new(path, tool, remaining_words, priority, loader, type)
409
+ if type == :append
410
+ eval_source(dsl, path, source)
411
+ else
412
+ tool.defining_from(path) do
413
+ eval_source(dsl, path, source)
414
+ tool.finish_definition
415
+ end
416
+ end
417
+ tool
418
+ end
419
+
420
+ ## @private
421
+ def self.eval_source(dsl, path, source)
422
+ case source
423
+ when String
424
+ # rubocop:disable Security/Eval
425
+ eval(source, dsl._binding, path, 1)
426
+ # rubocop:enable Security/Eval
427
+ when ::Proc
428
+ dsl.instance_eval(&source)
429
+ end
430
+ end
431
+ end
432
+ end