toys-core 0.3.7.1 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +1 -2
  4. data/lib/toys-core.rb +23 -8
  5. data/lib/toys/cli.rb +62 -23
  6. data/lib/toys/core_version.rb +1 -1
  7. data/lib/toys/definition/arg.rb +0 -2
  8. data/lib/toys/definition/flag.rb +2 -4
  9. data/lib/toys/definition/tool.rb +38 -36
  10. data/lib/toys/dsl/arg.rb +4 -0
  11. data/lib/toys/dsl/flag.rb +9 -5
  12. data/lib/toys/dsl/tool.rb +35 -28
  13. data/lib/toys/input_file.rb +2 -1
  14. data/lib/toys/loader.rb +97 -51
  15. data/lib/toys/middleware.rb +61 -87
  16. data/lib/toys/runner.rb +19 -2
  17. data/lib/toys/{middleware → standard_middleware}/add_verbosity_flags.rb +24 -8
  18. data/lib/toys/{middleware → standard_middleware}/handle_usage_errors.rb +4 -6
  19. data/lib/toys/{middleware → standard_middleware}/set_default_descriptions.rb +4 -4
  20. data/lib/toys/{middleware → standard_middleware}/show_help.rb +32 -16
  21. data/lib/toys/{middleware → standard_middleware}/show_root_version.rb +4 -5
  22. data/lib/toys/{helpers → standard_mixins}/exec.rb +8 -8
  23. data/lib/toys/{helpers → standard_mixins}/fileutils.rb +1 -1
  24. data/lib/toys/{helpers → standard_mixins}/highline.rb +2 -3
  25. data/lib/toys/{helpers → standard_mixins}/terminal.rb +2 -4
  26. data/lib/toys/tool.rb +3 -2
  27. data/lib/toys/utils/exec.rb +255 -82
  28. data/lib/toys/utils/gems.rb +3 -3
  29. data/lib/toys/utils/help_text.rb +0 -2
  30. data/lib/toys/utils/module_lookup.rb +60 -40
  31. data/lib/toys/utils/wrappable_string.rb +0 -2
  32. metadata +11 -19
  33. data/lib/toys/helpers.rb +0 -54
  34. data/lib/toys/middleware/base.rb +0 -99
  35. data/lib/toys/templates.rb +0 -55
  36. data/lib/toys/templates/clean.rb +0 -85
  37. data/lib/toys/templates/gem_build.rb +0 -125
  38. data/lib/toys/templates/minitest.rb +0 -125
  39. data/lib/toys/templates/rubocop.rb +0 -86
  40. data/lib/toys/templates/yardoc.rb +0 -101
data/lib/toys/runner.rb CHANGED
@@ -31,15 +31,32 @@ require "optparse"
31
31
 
32
32
  module Toys
33
33
  ##
34
- # An internal class that manages execution of a tool
35
- # @private
34
+ # An internal class that orchestrates execution of a tool.
35
+ #
36
+ # Generaly, you should not need to use this class directly. Instead, run a
37
+ # tool using {Toys::CLI#run}.
36
38
  #
37
39
  class Runner
40
+ ##
41
+ # Create a runner for a particular tool in a particular CLI.
42
+ #
43
+ # @param [Toys::CLI] cli The CLI that is running the tool. This will
44
+ # provide needed context information.
45
+ # @param [Toys::Definition::Tool] tool_definition The tool to run.
46
+ #
38
47
  def initialize(cli, tool_definition)
39
48
  @cli = cli
40
49
  @tool_definition = tool_definition
41
50
  end
42
51
 
52
+ ##
53
+ # Run the tool, provided given arguments.
54
+ #
55
+ # @param [Array<String>] args Command line arguments passed to the tool.
56
+ # @param [Integer] verbosity Initial verbosity. Default is 0.
57
+ #
58
+ # @return [Integer] The resulting status code
59
+ #
43
60
  def run(args, verbosity: 0)
44
61
  data = create_data(args, verbosity)
45
62
  parse_args(args, data)
@@ -27,10 +27,8 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/middleware/base"
31
-
32
30
  module Toys
33
- module Middleware
31
+ module StandardMiddleware
34
32
  ##
35
33
  # A middleware that provides flags for editing the verbosity.
36
34
  #
@@ -38,7 +36,9 @@ module Toys
38
36
  # not already defined by the tool. These flags affect the setting of
39
37
  # {Toys::Tool::Keys::VERBOSITY}, and, thus, the logger level.
40
38
  #
41
- class AddVerbosityFlags < Base
39
+ class AddVerbosityFlags
40
+ include Middleware
41
+
42
42
  ##
43
43
  # Default verbose flags
44
44
  # @return [Array<String>]
@@ -56,12 +56,15 @@ module Toys
56
56
  #
57
57
  # @param [Boolean,Array<String>,Proc] verbose_flags Specify flags
58
58
  # to increase verbosity. The value may be any of the following:
59
+ #
59
60
  # * An array of flags that increase verbosity.
60
61
  # * The `true` value to use {DEFAULT_VERBOSE_FLAGS}. (Default)
61
62
  # * The `false` value to disable verbose flags.
62
63
  # * A proc that takes a tool and returns any of the above.
64
+ #
63
65
  # @param [Boolean,Array<String>,Proc] quiet_flags Specify flags
64
66
  # to decrease verbosity. The value may be any of the following:
67
+ #
65
68
  # * An array of flags that decrease verbosity.
66
69
  # * The `true` value to use {DEFAULT_QUIET_FLAGS}. (Default)
67
70
  # * The `false` value to disable quiet flags.
@@ -84,8 +87,8 @@ module Toys
84
87
  private
85
88
 
86
89
  def add_verbose_flags(tool_definition)
87
- verbose_flags = Middleware.resolve_flags_spec(@verbose_flags, tool_definition,
88
- DEFAULT_VERBOSE_FLAGS)
90
+ verbose_flags = resolve_flags_spec(@verbose_flags, tool_definition,
91
+ DEFAULT_VERBOSE_FLAGS)
89
92
  unless verbose_flags.empty?
90
93
  tool_definition.add_flag(
91
94
  Tool::Keys::VERBOSITY, verbose_flags,
@@ -98,8 +101,7 @@ module Toys
98
101
  end
99
102
 
100
103
  def add_quiet_flags(tool_definition)
101
- quiet_flags = Middleware.resolve_flags_spec(@quiet_flags, tool_definition,
102
- DEFAULT_QUIET_FLAGS)
104
+ quiet_flags = resolve_flags_spec(@quiet_flags, tool_definition, DEFAULT_QUIET_FLAGS)
103
105
  unless quiet_flags.empty?
104
106
  tool_definition.add_flag(
105
107
  Tool::Keys::VERBOSITY, quiet_flags,
@@ -110,6 +112,20 @@ module Toys
110
112
  )
111
113
  end
112
114
  end
115
+
116
+ def resolve_flags_spec(flags, tool, defaults)
117
+ flags = flags.call(tool) if flags.respond_to?(:call)
118
+ case flags
119
+ when true, :default
120
+ Array(defaults)
121
+ when ::String
122
+ [flags]
123
+ when ::Array
124
+ flags
125
+ else
126
+ []
127
+ end
128
+ end
113
129
  end
114
130
  end
115
131
  end
@@ -27,19 +27,17 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/middleware/base"
31
- require "toys/utils/help_text"
32
- require "toys/utils/terminal"
33
-
34
30
  module Toys
35
- module Middleware
31
+ module StandardMiddleware
36
32
  ##
37
33
  # This middleware handles the case of a usage error. If a usage error, such
38
34
  # as an unrecognized flag or an unfulfilled required argument, is detected,
39
35
  # this middleware intercepts execution and displays the error along with
40
36
  # the short help string, and terminates execution with an error code.
41
37
  #
42
- class HandleUsageErrors < Base
38
+ class HandleUsageErrors
39
+ include Middleware
40
+
43
41
  ##
44
42
  # Create a HandleUsageErrors middleware.
45
43
  #
@@ -27,10 +27,8 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/middleware/base"
31
-
32
30
  module Toys
33
- module Middleware
31
+ module StandardMiddleware
34
32
  ##
35
33
  # This middleware sets default description fields for tools and command
36
34
  # line arguments and flags that do not have them set otherwise.
@@ -39,7 +37,9 @@ module Toys
39
37
  # root tool by passing parameters to this middleware. For finer control,
40
38
  # you can override methods to modify the description generation logic.
41
39
  #
42
- class SetDefaultDescriptions < Base
40
+ class SetDefaultDescriptions
41
+ include Middleware
42
+
43
43
  ##
44
44
  # The default description for tools.
45
45
  # @return [String]
@@ -27,13 +27,8 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/middleware/base"
31
- require "toys/utils/exec"
32
- require "toys/utils/help_text"
33
- require "toys/utils/terminal"
34
-
35
30
  module Toys
36
- module Middleware
31
+ module StandardMiddleware
37
32
  ##
38
33
  # A middleware that shows help text for the tool when a flag (typically
39
34
  # `--help`) is provided. It can also be configured to show help by
@@ -44,7 +39,9 @@ module Toys
44
39
  # all subtools recursively rather than only immediate subtools. This
45
40
  # middleware can also search for keywords in its subtools.
46
41
  #
47
- class ShowHelp < Base
42
+ class ShowHelp
43
+ include Middleware
44
+
48
45
  ##
49
46
  # Default help flags
50
47
  # @return [Array<String>]
@@ -74,30 +71,38 @@ module Toys
74
71
  #
75
72
  # @param [Boolean,Array<String>,Proc] help_flags Specify flags to
76
73
  # display help. The value may be any of the following:
74
+ #
77
75
  # * An array of flags.
78
76
  # * The `true` value to use {DEFAULT_HELP_FLAGS}.
79
77
  # * The `false` value for no flags. (Default)
80
78
  # * A proc that takes a tool and returns any of the above.
79
+ #
81
80
  # @param [Boolean,Array<String>,Proc] usage_flags Specify flags to
82
81
  # display usage. The value may be any of the following:
82
+ #
83
83
  # * An array of flags.
84
84
  # * The `true` value to use {DEFAULT_USAGE_FLAGS}.
85
85
  # * The `false` value for no flags. (Default)
86
86
  # * A proc that takes a tool and returns any of the above.
87
+ #
87
88
  # @param [Boolean,Array<String>,Proc] recursive_flags Specify flags
88
89
  # to control recursive subtool search. The value may be any of the
89
90
  # following:
91
+ #
90
92
  # * An array of flags.
91
93
  # * The `true` value to use {DEFAULT_RECURSIVE_FLAGS}.
92
94
  # * The `false` value for no flags. (Default)
93
95
  # * A proc that takes a tool and returns any of the above.
96
+ #
94
97
  # @param [Boolean,Array<String>,Proc] search_flags Specify flags
95
98
  # to search subtools for a search term. The value may be any of
96
99
  # the following:
100
+ #
97
101
  # * An array of flags.
98
102
  # * The `true` value to use {DEFAULT_SEARCH_FLAGS}.
99
103
  # * The `false` value for no flags. (Default)
100
104
  # * A proc that takes a tool and returns any of the above.
105
+ #
101
106
  # @param [Boolean] default_recursive Whether to search recursively for
102
107
  # subtools by default. Default is `false`.
103
108
  # @param [Boolean] fallback_execution Cause the tool to display its own
@@ -191,7 +196,7 @@ module Toys
191
196
 
192
197
  def output_help(str)
193
198
  if less_path
194
- Utils::Exec.new.exec([less_path, "-R"], in_from: str)
199
+ Utils::Exec.new.exec([less_path, "-R"], in: [:string, str])
195
200
  else
196
201
  terminal.puts(str)
197
202
  end
@@ -226,8 +231,7 @@ module Toys
226
231
  end
227
232
 
228
233
  def add_help_flags(tool_definition)
229
- help_flags = Middleware.resolve_flags_spec(@help_flags, tool_definition,
230
- DEFAULT_HELP_FLAGS)
234
+ help_flags = resolve_flags_spec(@help_flags, tool_definition, DEFAULT_HELP_FLAGS)
231
235
  unless help_flags.empty?
232
236
  tool_definition.add_flag(
233
237
  :_show_help, help_flags,
@@ -239,8 +243,7 @@ module Toys
239
243
  end
240
244
 
241
245
  def add_usage_flags(tool_definition)
242
- usage_flags = Middleware.resolve_flags_spec(@usage_flags, tool_definition,
243
- DEFAULT_USAGE_FLAGS)
246
+ usage_flags = resolve_flags_spec(@usage_flags, tool_definition, DEFAULT_USAGE_FLAGS)
244
247
  unless usage_flags.empty?
245
248
  tool_definition.add_flag(
246
249
  :_show_usage, usage_flags,
@@ -252,8 +255,8 @@ module Toys
252
255
  end
253
256
 
254
257
  def add_recursive_flags(tool_definition)
255
- recursive_flags = Middleware.resolve_flags_spec(@recursive_flags, tool_definition,
256
- DEFAULT_RECURSIVE_FLAGS)
258
+ recursive_flags = resolve_flags_spec(@recursive_flags, tool_definition,
259
+ DEFAULT_RECURSIVE_FLAGS)
257
260
  unless recursive_flags.empty?
258
261
  tool_definition.add_flag(
259
262
  :_recursive_subtools, recursive_flags,
@@ -264,8 +267,7 @@ module Toys
264
267
  end
265
268
 
266
269
  def add_search_flags(tool_definition)
267
- search_flags = Middleware.resolve_flags_spec(@search_flags, tool_definition,
268
- DEFAULT_SEARCH_FLAGS)
270
+ search_flags = resolve_flags_spec(@search_flags, tool_definition, DEFAULT_SEARCH_FLAGS)
269
271
  unless search_flags.empty?
270
272
  tool_definition.add_flag(
271
273
  :_search_subtools, search_flags,
@@ -274,6 +276,20 @@ module Toys
274
276
  )
275
277
  end
276
278
  end
279
+
280
+ def resolve_flags_spec(flags, tool, defaults)
281
+ flags = flags.call(tool) if flags.respond_to?(:call)
282
+ case flags
283
+ when true, :default
284
+ Array(defaults)
285
+ when ::String
286
+ [flags]
287
+ when ::Array
288
+ flags
289
+ else
290
+ []
291
+ end
292
+ end
277
293
  end
278
294
  end
279
295
  end
@@ -27,16 +27,15 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/middleware/base"
31
- require "toys/utils/terminal"
32
-
33
30
  module Toys
34
- module Middleware
31
+ module StandardMiddleware
35
32
  ##
36
33
  # A middleware that displays a version string for the root tool if the
37
34
  # `--version` flag is given.
38
35
  #
39
- class ShowRootVersion < Base
36
+ class ShowRootVersion
37
+ include Middleware
38
+
40
39
  ##
41
40
  # Default version flags
42
41
  # @return [Array<String>]
@@ -27,10 +27,8 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/utils/exec"
31
-
32
30
  module Toys
33
- module Helpers
31
+ module StandardMixins
34
32
  ##
35
33
  # A set of helper methods for invoking subcommands. Provides shortcuts for
36
34
  # common cases such as invoking Ruby in a subprocess or capturing output
@@ -69,8 +67,8 @@ module Toys
69
67
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
70
68
  # the subprocess streams.
71
69
  #
72
- # @return [Toys::Utils::Result] The subprocess result, including the exit
73
- # code and any captured output.
70
+ # @return [Toys::Utils::Exec::Result] The subprocess result, including
71
+ # the exit code and any captured output.
74
72
  #
75
73
  def exec(cmd, opts = {}, &block)
76
74
  Exec._exec(self).exec(cmd, Exec._setup_exec_opts(opts, self), &block)
@@ -88,8 +86,8 @@ module Toys
88
86
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
89
87
  # for the subprocess streams.
90
88
  #
91
- # @return [Toys::Utils::Result] The subprocess result, including the exit
92
- # code and any captured output.
89
+ # @return [Toys::Utils::Exec::Result] The subprocess result, including
90
+ # the exit code and any captured output.
93
91
  #
94
92
  def ruby(args, opts = {}, &block)
95
93
  Exec._exec(self).ruby(args, Exec._setup_exec_opts(opts, self), &block)
@@ -150,7 +148,9 @@ module Toys
150
148
  if opts[:exit_on_nonzero_status]
151
149
  proc { |s| tool.exit(s.exitstatus) }
152
150
  end
153
- opts.merge(nonzero_status_handler: nonzero_status_handler)
151
+ opts = opts.merge(nonzero_status_handler: nonzero_status_handler)
152
+ opts.delete(:exit_on_nonzero_status)
153
+ opts
154
154
  end
155
155
  end
156
156
  end
@@ -30,7 +30,7 @@
30
30
  require "fileutils"
31
31
 
32
32
  module Toys
33
- module Helpers
33
+ module StandardMixins
34
34
  ##
35
35
  # A module that provides all methods in the "fileutils" standard library.
36
36
  #
@@ -27,14 +27,13 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/utils/gems"
31
30
  Toys::Utils::Gems.activate("highline", "~> 1.7")
32
31
  require "highline"
33
32
 
34
33
  module Toys
35
- module Helpers
34
+ module StandardMixins
36
35
  ##
37
- # A module that provides access to the capabilities of the highline gem.
36
+ # A mixin that provides access to the capabilities of the highline gem.
38
37
  #
39
38
  # You may make these methods available to your tool by including the
40
39
  # following directive in your tool configuration:
@@ -27,12 +27,10 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "toys/utils/terminal"
31
-
32
30
  module Toys
33
- module Helpers
31
+ module StandardMixins
34
32
  ##
35
- # A helper that provides a simple terminal. It includes a set of methods
33
+ # A mixin that provides a simple terminal. It includes a set of methods
36
34
  # that produce styled output, get user input, and otherwise interact with
37
35
  # the user's terminal.
38
36
  #
data/lib/toys/tool.rb CHANGED
@@ -39,6 +39,7 @@ module Toys
39
39
  #
40
40
  # Keys that are neither strings nor symbols are by convention used for other
41
41
  # context information, including:
42
+ #
42
43
  # * Common information such as the {Toys::Definition::Tool} object being
43
44
  # executed, the arguments originally passed to it, or the usage error
44
45
  # string. These well-known keys can be accessed via constants in the
@@ -46,7 +47,7 @@ module Toys
46
47
  # * Common settings such as the verbosity level, and whether to exit
47
48
  # immediately if a subprocess exits with a nonzero result. These keys are
48
49
  # also present as {Toys::Context} constants.
49
- # * Private information used internally by middleware and helpers.
50
+ # * Private information used internally by middleware and mixins.
50
51
  #
51
52
  # This class provides convenience accessors for common keys and settings, and
52
53
  # you can retrieve argument-set keys using the {#options} hash.
@@ -246,7 +247,7 @@ module Toys
246
247
  # Returns the subset of the context that uses string or symbol keys. By
247
248
  # convention, this includes keys that are set by tool flags and arguments,
248
249
  # but does not include well-known context values such as verbosity or
249
- # private context values used by middleware or helpers.
250
+ # private context values used by middleware or mixins.
250
251
  #
251
252
  # @return [Hash]
252
253
  #
@@ -38,22 +38,6 @@ module Toys
38
38
  # processes and their streams. It also provides shortcuts for common cases
39
39
  # such as invoking Ruby in a subprocess or capturing output in a string.
40
40
  #
41
- # ## Stream handling
42
- #
43
- # By default, subprocess streams are connected to the corresponding streams
44
- # in the parent process.
45
- #
46
- # Alternately, input streams may be read from a string you provide, and
47
- # you may direct output streams to be captured and their contents exposed
48
- # in the result object.
49
- #
50
- # You may also connect subprocess streams to a controller, which you can
51
- # then manipulate by providing a block. Your block may read and write
52
- # connected streams to interact with the process. For example, to redirect
53
- # data into a subprocess you can connect its input stream to the controller
54
- # using the `:in_from` option (see below). Then, in your block, you can
55
- # write to that stream via the controller.
56
- #
57
41
  # ## Configuration options
58
42
  #
59
43
  # A variety of options can be used to control subprocesses. These include:
@@ -63,23 +47,12 @@ module Toys
63
47
  # If not present, the command is not logged.
64
48
  # * **:log_level** (Integer) Log level for logging the actual command.
65
49
  # Defaults to Logger::INFO if not present.
66
- # * **:in_from** (`:controller`,String) Connects the input stream of the
67
- # subprocess. If set to `:controller`, the controller will control the
68
- # input stream. If set to a string, that string will be written to the
69
- # input stream. If not set, the input stream will be connected to the
70
- # STDIN for the Toys process itself.
71
- # * **:out_to** (`:controller`,`:capture`) Connects the standard output
72
- # stream of the subprocess. If set to `:controller`, the controller
73
- # will control the output stream. If set to `:capture`, the output will
74
- # be captured in a string that is available in the
75
- # {Toys::Utils::Exec::Result} object. If not set, the subprocess
76
- # standard out is connected to STDOUT of the Toys process.
77
- # * **:err_to** (`:controller`,`:capture`) Connects the standard error
78
- # stream of the subprocess. If set to `:controller`, the controller
79
- # will control the output stream. If set to `:capture`, the output will
80
- # be captured in a string that is available in the
81
- # {Toys::Utils::Exec::Result} object. If not set, the subprocess
82
- # standard out is connected to STDERR of the Toys process.
50
+ # * **:in** Connects the input stream of the subprocess. See the section
51
+ # on stream handling.
52
+ # * **:out** Connects the standard output stream of the subprocess. See
53
+ # the section on stream handling.
54
+ # * **:err** Connects the standard error stream of the subprocess. See the
55
+ # section on stream handling.
83
56
  #
84
57
  # In addition, the following options recognized by `Process#spawn` are
85
58
  # supported.
@@ -95,7 +68,57 @@ module Toys
95
68
  #
96
69
  # Configuration options may be provided to any method that starts a
97
70
  # subprocess. You may also modify default values by calling
98
- # {Toys::Utils::Exec#config_defaults}.
71
+ # {Toys::Utils::Exec#configure_defaults}.
72
+ #
73
+ # ## Stream handling
74
+ #
75
+ # By default, subprocess streams are connected to the corresponding streams
76
+ # in the parent process. You can change this behavior, redirecting streams
77
+ # or providing ways to control them, using the `:in`, `:out`, and `:err`
78
+ # options.
79
+ #
80
+ # Three general strategies are available for custom stream handling. First,
81
+ # you may redirect to other streams such as files, IO objects, or Ruby
82
+ # strings. Some of these options map directly to options provided by the
83
+ # `Process#spawn` method. Second, you may use a controller to manipulate
84
+ # the streams programmatically. Third, you may capture output stream data
85
+ # and make it available in the result.
86
+ #
87
+ # Following is a full list of the stream handling options, along with how
88
+ # to specify them using the `:in`, `:out`, and `:err` options.
89
+ #
90
+ # * **Close the stream:** You may close the stream by passing `:close` as
91
+ # the option value. This is the same as passing `:close` to
92
+ # `Process#spawn`.
93
+ # * **Redirect to null:** You may redirect to a null stream by passing
94
+ # `:null` as the option value. This connects to a stream that is not
95
+ # closed but contains no data, i.e. `/dev/null` on unix systems.
96
+ # * **Redirect to a file:** You may redirect to a file. This reads from an
97
+ # existing file when connected to `:in`, and creates or appends to a
98
+ # file when connected to `:out` or `:err`. To specify a file, use the
99
+ # setting `[:file, "/path/to/file"]`. You may also, when writing a file,
100
+ # append an optional mode and permission code to the array. For
101
+ # example, `[:file, "/path/to/file", "a", 0644]`.
102
+ # * **Redirect to an IO object:** You may redirect to an IO object in the
103
+ # parent process, by passing the IO object as the option value. You may
104
+ # use any IO object. For example, you could connect the child's output
105
+ # to the parent's error using `out: $stderr`, or you could connect to an
106
+ # existing File stream. Unlike `Process#spawn`, this works for IO
107
+ # objects that do not have a corresponding file descriptor (such as
108
+ # StringIO objects). In such a case, a thread will be spawned to pipe
109
+ # the IO data through to the child process.
110
+ # * **Combine with another child stream:** You may redirect one child
111
+ # output stream to another, to combine them. To merge the child's error
112
+ # stream into its output stream, use `err: [:child, :out]`.
113
+ # * **Read from a string:** You may pass a string to the input stream by
114
+ # setting `[:string, "the string"]`. This works only for `:in`.
115
+ # * **Capture output stream:** You may capture a stream and make it
116
+ # available on the {Toys::Utils::Exec::Result} object, using the setting
117
+ # `:capture`. This works only for the `:out` and `:err` streams.
118
+ # * **Use the controller:** You may hook a stream to the controller using
119
+ # the setting `:controller`. You can then manipulate the stream via the
120
+ # controller. If you pass a block to {Toys::Utils::Exec#exec}, it yields
121
+ # the {Toys::Utils::Exec::Controller}, giving you access to streams.
99
122
  #
100
123
  class Exec
101
124
  ##
@@ -134,17 +157,17 @@ module Toys
134
157
  # exit code and any captured output.
135
158
  #
136
159
  def exec(cmd, opts = {}, &block)
160
+ exec_opts = Opts.new(@default_opts).add(opts)
137
161
  spawn_cmd =
138
162
  if cmd.is_a?(::Array)
139
163
  if cmd.size == 1 && cmd.first.is_a?(::String)
140
- [[cmd.first, opts[:argv0] || cmd.first]]
164
+ [[cmd.first, exec_opts.config_opts[:argv0] || cmd.first]]
141
165
  else
142
166
  cmd
143
167
  end
144
168
  else
145
169
  [cmd]
146
170
  end
147
- exec_opts = Opts.new(@default_opts).add(opts)
148
171
  executor = Executor.new(exec_opts, spawn_cmd)
149
172
  executor.execute(&block)
150
173
  end
@@ -161,7 +184,7 @@ module Toys
161
184
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
162
185
  # for the subprocess streams.
163
186
  #
164
- # @return [Toys::Utils::Result] The subprocess result, including
187
+ # @return [Toys::Utils::Exec::Result] The subprocess result, including
165
188
  # exit code and any captured output.
166
189
  #
167
190
  def ruby(args, opts = {}, &block)
@@ -195,7 +218,7 @@ module Toys
195
218
  # @return [String] What was written to standard out.
196
219
  #
197
220
  def capture(cmd, opts = {})
198
- exec(cmd, opts.merge(out_to: :capture)).captured_out
221
+ exec(cmd, opts.merge(out: :capture)).captured_out
199
222
  end
200
223
 
201
224
  ##
@@ -208,13 +231,14 @@ module Toys
208
231
  # @private
209
232
  #
210
233
  CONFIG_KEYS = %i[
234
+ argv0
211
235
  env
212
- err_to
213
- in_from
236
+ err
237
+ in
214
238
  logger
215
239
  log_level
216
240
  nonzero_status_handler
217
- out_to
241
+ out
218
242
  ].freeze
219
243
 
220
244
  ##
@@ -247,8 +271,10 @@ module Toys
247
271
  config.each do |k, v|
248
272
  if CONFIG_KEYS.include?(k)
249
273
  @config_opts[k] = v
250
- elsif SPAWN_KEYS.include?(k)
274
+ elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
251
275
  @spawn_opts[k] = v
276
+ else
277
+ raise ::ArgumentError, "Unknown key: #{k.inspect}"
252
278
  end
253
279
  end
254
280
  self
@@ -258,8 +284,10 @@ module Toys
258
284
  keys.each do |k|
259
285
  if CONFIG_KEYS.include?(k)
260
286
  @config_opts.delete(k)
261
- elsif SPAWN_KEYS.include?(k)
287
+ elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
262
288
  @spawn_opts.delete(k)
289
+ else
290
+ raise ::ArgumentError, "Unknown key: #{k.inspect}"
263
291
  end
264
292
  end
265
293
  self
@@ -285,7 +313,7 @@ module Toys
285
313
 
286
314
  ##
287
315
  # Return the subcommand's standard input stream (which can be written
288
- # to), if the command was configured with `in_from: :controller`.
316
+ # to), if the command was configured with `in: :controller`.
289
317
  # Returns `nil` otherwise.
290
318
  # @return [IO,nil]
291
319
  #
@@ -293,7 +321,7 @@ module Toys
293
321
 
294
322
  ##
295
323
  # Return the subcommand's standard output stream (which can be read
296
- # from), if the command was configured with `out_to: :controller`.
324
+ # from), if the command was configured with `out: :controller`.
297
325
  # Returns `nil` otherwise.
298
326
  # @return [IO,nil]
299
327
  #
@@ -301,7 +329,7 @@ module Toys
301
329
 
302
330
  ##
303
331
  # Return the subcommand's standard error stream (which can be read
304
- # from), if the command was configured with `err_to: :controller`.
332
+ # from), if the command was configured with `err: :controller`.
305
333
  # Returns `nil` otherwise.
306
334
  # @return [IO,nil]
307
335
  #
@@ -337,14 +365,14 @@ module Toys
337
365
 
338
366
  ##
339
367
  # Returns the captured output string, if the command was configured
340
- # with `out_to: :capture`. Returns `nil` otherwise.
368
+ # with `out: :capture`. Returns `nil` otherwise.
341
369
  # @return [String,nil]
342
370
  #
343
371
  attr_reader :captured_out
344
372
 
345
373
  ##
346
374
  # Returns the captured error string, if the command was configured
347
- # with `err_to: :capture`. Returns `nil` otherwise.
375
+ # with `err: :capture`. Returns `nil` otherwise.
348
376
  # @return [String,nil]
349
377
  #
350
378
  attr_reader :captured_err
@@ -397,8 +425,8 @@ module Toys
397
425
 
398
426
  def execute(&block)
399
427
  setup_in_stream
400
- setup_out_stream(:out, :out_to, :out)
401
- setup_out_stream(:err, :err_to, :err)
428
+ setup_out_stream(:out)
429
+ setup_out_stream(:err)
402
430
  log_command
403
431
  wait_thread = start_process
404
432
  status = control_process(wait_thread, &block)
@@ -447,54 +475,199 @@ module Toys
447
475
  end
448
476
 
449
477
  def setup_in_stream
450
- setting = @config_opts[:in_from]
451
- if setting
452
- r, w = ::IO.pipe
453
- @spawn_opts[:in] = r
454
- w.sync = true
455
- @child_streams << r
456
- case setting
457
- when :controller
458
- @controller_streams[:in] = w
459
- when String
460
- write_string_thread(w, setting)
461
- else
462
- raise "Unknown type for in_from"
478
+ setting = @config_opts[:in]
479
+ return unless setting
480
+ case setting
481
+ when ::Symbol
482
+ setup_in_stream_of_type(setting, [])
483
+ when ::Integer
484
+ setup_in_stream_of_type(:parent, [setting])
485
+ when ::String
486
+ setup_in_stream_of_type(:file, [setting])
487
+ when ::IO, ::StringIO
488
+ interpret_in_io(setting)
489
+ when ::Array
490
+ interpret_in_array(setting)
491
+ else
492
+ raise "Unknown value for in: #{setting.inspect}"
493
+ end
494
+ end
495
+
496
+ def interpret_in_io(setting)
497
+ if setting.fileno.is_a?(::Integer)
498
+ setup_in_stream_of_type(:parent, [setting.fileno])
499
+ else
500
+ setup_in_stream_of_type(:copy_io, [setting])
501
+ end
502
+ end
503
+
504
+ def interpret_in_array(setting)
505
+ case setting.first
506
+ when ::Symbol
507
+ setup_in_stream_of_type(setting.first, setting[1..-1])
508
+ when ::String
509
+ setup_in_stream_of_type(:file, setting)
510
+ else
511
+ raise "Unknown value for in: #{setting.inspect}"
512
+ end
513
+ end
514
+
515
+ def setup_in_stream_of_type(type, args)
516
+ case type
517
+ when :controller
518
+ @controller_streams[:in] = make_in_pipe
519
+ when :null
520
+ make_null_stream(:in, "r")
521
+ when :close
522
+ @spawn_opts[:in] = type
523
+ when :parent
524
+ @spawn_opts[:in] = args.first
525
+ when :child
526
+ @spawn_opts[:in] = [:child, args.first]
527
+ when :string
528
+ write_string_thread(args.first.to_s)
529
+ when :copy_io
530
+ copy_to_in_thread(args.first)
531
+ when :file
532
+ interpret_in_file(args)
533
+ else
534
+ raise "Unknown type for in: #{type.inspect}"
535
+ end
536
+ end
537
+
538
+ def interpret_in_file(args)
539
+ raise "Expected only file name" unless args.size == 1 && args.first.is_a?(::String)
540
+ @spawn_opts[:in] = args + [::File::RDONLY]
541
+ end
542
+
543
+ def setup_out_stream(key)
544
+ setting = @config_opts[key]
545
+ return unless setting
546
+ case setting
547
+ when ::Symbol
548
+ setup_out_stream_of_type(key, setting, [])
549
+ when ::Integer
550
+ setup_out_stream_of_type(key, :parent, [setting])
551
+ when ::String
552
+ setup_out_stream_of_type(key, :file, [setting])
553
+ when ::IO, ::StringIO
554
+ interpret_out_io(key, setting)
555
+ when ::Array
556
+ interpret_out_array(key, setting)
557
+ else
558
+ raise "Unknown value for #{key}: #{setting.inspect}"
559
+ end
560
+ end
561
+
562
+ def interpret_out_io(key, setting)
563
+ if setting.fileno.is_a?(::Integer)
564
+ setup_out_stream_of_type(key, :parent, [setting.fileno])
565
+ else
566
+ setup_out_stream_of_type(key, :copy_io, [setting])
567
+ end
568
+ end
569
+
570
+ def interpret_out_array(key, setting)
571
+ case setting.first
572
+ when ::Symbol
573
+ setup_out_stream_of_type(key, setting.first, setting[1..-1])
574
+ when ::String
575
+ setup_out_stream_of_type(key, :file, setting)
576
+ else
577
+ raise "Unknown value for #{key}: #{setting.inspect}"
578
+ end
579
+ end
580
+
581
+ def setup_out_stream_of_type(key, type, args)
582
+ case type
583
+ when :controller
584
+ @controller_streams[key] = make_out_pipe(key)
585
+ when :null
586
+ make_null_stream(key, "w")
587
+ when :close, :out, :err
588
+ @spawn_opts[key] = type
589
+ when :parent
590
+ @spawn_opts[key] = args.first
591
+ when :child
592
+ @spawn_opts[key] = [:child, args.first]
593
+ when :capture
594
+ capture_stream_thread(key)
595
+ when :copy_io
596
+ copy_from_out_thread(key, args.first)
597
+ when :file
598
+ interpret_out_file(key, args)
599
+ else
600
+ raise "Unknown type for #{key}: #{type.inspect}"
601
+ end
602
+ end
603
+
604
+ def interpret_out_file(key, args)
605
+ raise "Expected file name" if args.empty? || !args.first.is_a?(::String)
606
+ raise "Too many file arguments" if args.size > 3
607
+ @spawn_opts[key] = args.size == 1 ? args.first : args
608
+ end
609
+
610
+ def make_null_stream(key, mode)
611
+ f = ::File.open(::File::NULL, mode)
612
+ @spawn_opts[key] = f
613
+ @child_streams << f
614
+ end
615
+
616
+ def make_in_pipe
617
+ r, w = ::IO.pipe
618
+ @spawn_opts[:in] = r
619
+ @child_streams << r
620
+ w.sync = true
621
+ w
622
+ end
623
+
624
+ def make_out_pipe(key)
625
+ r, w = ::IO.pipe
626
+ @spawn_opts[key] = w
627
+ @child_streams << w
628
+ r
629
+ end
630
+
631
+ def write_string_thread(string)
632
+ stream = make_in_pipe
633
+ @join_threads << ::Thread.new do
634
+ begin
635
+ stream.write string
636
+ ensure
637
+ stream.close
463
638
  end
464
639
  end
465
640
  end
466
641
 
467
- def setup_out_stream(stream_name, config_key, spawn_key)
468
- setting = @config_opts[config_key]
469
- if setting
470
- r, w = ::IO.pipe
471
- @spawn_opts[spawn_key] = w
472
- @child_streams << w
473
- case setting
474
- when :controller
475
- @controller_streams[stream_name] = r
476
- when :capture
477
- @join_threads << capture_stream_thread(r, stream_name)
478
- else
479
- raise "Unknown type for #{config_key}"
642
+ def copy_to_in_thread(io, close: false)
643
+ stream = make_in_pipe
644
+ @join_threads << ::Thread.new do
645
+ begin
646
+ ::IO.copy_stream(io, stream)
647
+ ensure
648
+ stream.close
649
+ io.close if close
480
650
  end
481
651
  end
482
652
  end
483
653
 
484
- def write_string_thread(stream, string)
485
- ::Thread.new do
654
+ def copy_from_out_thread(key, io, close: false)
655
+ stream = make_out_pipe(key)
656
+ @join_threads << ::Thread.new do
486
657
  begin
487
- stream.write string
658
+ ::IO.copy_stream(stream, io)
488
659
  ensure
489
660
  stream.close
661
+ io.close if close
490
662
  end
491
663
  end
492
664
  end
493
665
 
494
- def capture_stream_thread(stream, name)
495
- ::Thread.new do
666
+ def capture_stream_thread(key)
667
+ stream = make_out_pipe(key)
668
+ @join_threads << ::Thread.new do
496
669
  begin
497
- @captures[name] = stream.read
670
+ @captures[key] = stream.read
498
671
  ensure
499
672
  stream.close
500
673
  end