toys-core 0.3.7.1 → 0.3.8

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