toys-core 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.md +16 -24
  4. data/README.md +307 -59
  5. data/docs/guide.md +44 -4
  6. data/lib/toys-core.rb +58 -49
  7. data/lib/toys/acceptor.rb +672 -0
  8. data/lib/toys/alias.rb +106 -0
  9. data/lib/toys/arg_parser.rb +624 -0
  10. data/lib/toys/cli.rb +422 -181
  11. data/lib/toys/compat.rb +83 -0
  12. data/lib/toys/completion.rb +442 -0
  13. data/lib/toys/context.rb +354 -0
  14. data/lib/toys/core_version.rb +18 -26
  15. data/lib/toys/dsl/flag.rb +213 -56
  16. data/lib/toys/dsl/flag_group.rb +237 -51
  17. data/lib/toys/dsl/positional_arg.rb +210 -0
  18. data/lib/toys/dsl/tool.rb +968 -317
  19. data/lib/toys/errors.rb +46 -28
  20. data/lib/toys/flag.rb +821 -0
  21. data/lib/toys/flag_group.rb +282 -0
  22. data/lib/toys/input_file.rb +18 -26
  23. data/lib/toys/loader.rb +110 -100
  24. data/lib/toys/middleware.rb +24 -31
  25. data/lib/toys/mixin.rb +90 -59
  26. data/lib/toys/module_lookup.rb +125 -0
  27. data/lib/toys/positional_arg.rb +184 -0
  28. data/lib/toys/source_info.rb +192 -0
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
  32. data/lib/toys/standard_middleware/show_help.rb +130 -113
  33. data/lib/toys/standard_middleware/show_root_version.rb +29 -35
  34. data/lib/toys/standard_mixins/exec.rb +116 -78
  35. data/lib/toys/standard_mixins/fileutils.rb +16 -24
  36. data/lib/toys/standard_mixins/gems.rb +29 -30
  37. data/lib/toys/standard_mixins/highline.rb +34 -41
  38. data/lib/toys/standard_mixins/terminal.rb +72 -26
  39. data/lib/toys/template.rb +51 -35
  40. data/lib/toys/tool.rb +1161 -206
  41. data/lib/toys/utils/completion_engine.rb +171 -0
  42. data/lib/toys/utils/exec.rb +279 -182
  43. data/lib/toys/utils/gems.rb +58 -49
  44. data/lib/toys/utils/help_text.rb +117 -111
  45. data/lib/toys/utils/terminal.rb +69 -62
  46. data/lib/toys/wrappable_string.rb +162 -0
  47. metadata +24 -22
  48. data/lib/toys/definition/acceptor.rb +0 -191
  49. data/lib/toys/definition/alias.rb +0 -112
  50. data/lib/toys/definition/arg.rb +0 -140
  51. data/lib/toys/definition/flag.rb +0 -370
  52. data/lib/toys/definition/flag_group.rb +0 -205
  53. data/lib/toys/definition/source_info.rb +0 -190
  54. data/lib/toys/definition/tool.rb +0 -842
  55. data/lib/toys/dsl/arg.rb +0 -132
  56. data/lib/toys/runner.rb +0 -188
  57. data/lib/toys/standard_middleware.rb +0 -47
  58. data/lib/toys/utils/module_lookup.rb +0 -135
  59. data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Daniel Azuma
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
22
+ ;
23
+
24
+ require "shellwords"
25
+
26
+ module Toys
27
+ module Utils
28
+ ##
29
+ # Implementations of tab completion.
30
+ #
31
+ # This module is not loaded by default. Before using it directly, you must
32
+ # `require "toys/utils/completion_engine"`
33
+ #
34
+ module CompletionEngine
35
+ ##
36
+ # A completion engine for bash.
37
+ #
38
+ class Bash
39
+ ##
40
+ # Create a bash completion engine.
41
+ #
42
+ # @param cli [Toys::CLI] The CLI.
43
+ #
44
+ def initialize(cli)
45
+ @cli = cli
46
+ end
47
+
48
+ ##
49
+ # Perform completion in the current shell environment, which must
50
+ # include settings for the `COMP_LINE` and `COMP_POINT` environment
51
+ # variables. Prints out completion candidates, one per line, and
52
+ # returns a status code indicating the result.
53
+ #
54
+ # * **0** for success.
55
+ # * **1** if completion failed.
56
+ # * **2** if the environment is incorrect (e.g. expected environment
57
+ # variables not found)
58
+ #
59
+ # @return [Integer] status code
60
+ #
61
+ def run
62
+ return 2 if !::ENV.key?("COMP_LINE") || !::ENV.key?("COMP_POINT")
63
+ line = ::ENV["COMP_LINE"].to_s
64
+ point = ::ENV["COMP_POINT"].to_i
65
+ point = line.length if point.negative?
66
+ line = line[0, point]
67
+ completions = run_internal(line)
68
+ if completions
69
+ completions.each { |completion| puts completion }
70
+ 0
71
+ else
72
+ 1
73
+ end
74
+ end
75
+
76
+ ##
77
+ # Internal completion method designed for testing.
78
+ # @private
79
+ #
80
+ def run_internal(line)
81
+ words = CompletionEngine.split(line)
82
+ quote_type, last = words.pop
83
+ return nil unless words.shift
84
+ words.map! { |_type, word| word }
85
+ prefix = ""
86
+ if (match = /\A(.*[=:])(.*)\z/.match(last))
87
+ prefix = match[1]
88
+ last = match[2]
89
+ end
90
+ context = Completion::Context.new(
91
+ cli: @cli, previous_words: words, fragment_prefix: prefix, fragment: last,
92
+ params: {shell: :bash, quote_type: quote_type}
93
+ )
94
+ candidates = @cli.completion.call(context)
95
+ candidates.uniq.sort.map do |candidate|
96
+ CompletionEngine.format_candidate(candidate, quote_type)
97
+ end
98
+ end
99
+ end
100
+
101
+ class << self
102
+ ## @private
103
+ def split(line)
104
+ words = []
105
+ field = ::String.new
106
+ quote_type = nil
107
+ line.scan(split_regex) do |word, sqw, dqw, esc, garbage, sep|
108
+ raise ArgumentError, "Didn't expect garbage: #{line.inspect}" if garbage
109
+ field << field_str(word, sqw, dqw, esc)
110
+ quote_type = update_quote_type(quote_type, sqw, dqw)
111
+ if sep
112
+ words << [quote_type, field]
113
+ quote_type = nil
114
+ field = sep.empty? ? nil : ::String.new
115
+ end
116
+ end
117
+ words << [quote_type, field] if field
118
+ words
119
+ end
120
+
121
+ ## @private
122
+ def format_candidate(candidate, quote_type)
123
+ str = candidate.to_s
124
+ partial = candidate.is_a?(Completion::Candidate) ? candidate.partial? : false
125
+ quote_type = nil if candidate.string.include?("'") && quote_type == :single
126
+ case quote_type
127
+ when :single
128
+ partial ? "'#{str}" : "'#{str}' "
129
+ when :double
130
+ str = str.gsub(/[$`"\\\n]/, '\\\\\\1')
131
+ partial ? "\"#{str}" : "\"#{str}\" "
132
+ else
133
+ str = ::Shellwords.escape(str)
134
+ partial ? str : "#{str} "
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def split_regex
141
+ word_re = "([^\\s\\\\\\'\\\"]+)"
142
+ sq_re = "'([^\\']*)(?:'|\\z)"
143
+ dq_re = "\"((?:[^\\\"\\\\]|\\\\.)*)(?:\"|\\z)"
144
+ esc_re = "(\\\\.?)"
145
+ sep_re = "(\\s|\\z)"
146
+ /\G\s*(?>#{word_re}|#{sq_re}|#{dq_re}|#{esc_re}|(\S))#{sep_re}?/m
147
+ end
148
+
149
+ def field_str(word, sqw, dqw, esc)
150
+ word ||
151
+ sqw ||
152
+ dqw&.gsub(/\\([$`"\\\n])/, '\\1') ||
153
+ esc&.gsub(/\\(.)/, '\\1') ||
154
+ ""
155
+ end
156
+
157
+ def update_quote_type(quote_type, sqw, dqw)
158
+ if quote_type
159
+ :multi
160
+ elsif sqw
161
+ :single
162
+ elsif dqw
163
+ :double
164
+ else
165
+ :bare
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -1,32 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2018 Daniel Azuma
3
+ # Copyright 2019 Daniel Azuma
4
4
  #
5
- # All rights reserved.
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
6
11
  #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
9
14
  #
10
- # * Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # * Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- # * Neither the name of the copyright holder, nor the names of any other
16
- # contributors to this software, may be used to endorse or promote products
17
- # derived from this software without specific prior written permission.
18
- #
19
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
- # POSSIBILITY OF SUCH DAMAGE.
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
30
22
  ;
31
23
 
32
24
  require "logger"
@@ -41,36 +33,43 @@ module Toys
41
33
  # processes and their streams. It also provides shortcuts for common cases
42
34
  # such as invoking Ruby in a subprocess or capturing output in a string.
43
35
  #
36
+ # This class is not loaded by default. Before using it directly, you should
37
+ # `require "toys/utils/exec"`
38
+ #
44
39
  # ## Configuration options
45
40
  #
46
41
  # A variety of options can be used to control subprocesses. These include:
47
42
  #
48
- # * **:env** (Hash) Environment variables to pass to the subprocess
49
- # * **:logger** (Logger) Logger to use for logging the actual command.
50
- # If not present, the command is not logged.
51
- # * **:log_level** (Integer,false) Level for logging the actual command.
52
- # Defaults to Logger::INFO if not present. You may also pass `false` to
53
- # disable logging of the command.
54
- # * **:log_cmd** (String) The string logged for the actual command.
55
- # Defaults to the `inspect` representation of the command.
56
- # * **:background** (Boolean) Runs the process in the background,
57
- # returning a controller object instead of a result object.
58
- # * **:in** Connects the input stream of the subprocess. See the section
59
- # on stream handling.
60
- # * **:out** Connects the standard output stream of the subprocess. See
61
- # the section on stream handling.
62
- # * **:err** Connects the standard error stream of the subprocess. See the
63
- # section on stream handling.
43
+ # * `:name` (Object) An optional object that can be used to identify this
44
+ # subprocess. It is available in the controller and result objects.
45
+ # * `:env` (Hash) Environment variables to pass to the subprocess
46
+ # * `:logger` (Logger) Logger to use for logging the actual command. If
47
+ # not present, the command is not logged.
48
+ # * `:log_level` (Integer,false) Level for logging the actual command.
49
+ # Defaults to Logger::INFO if not present. You may also pass `false` to
50
+ # disable logging of the command.
51
+ # * `:log_cmd` (String) The string logged for the actual command.
52
+ # Defaults to the `inspect` representation of the command.
53
+ # * `:background` (Boolean) Runs the process in the background, returning
54
+ # a controller object instead of a result object.
55
+ # * `:result_callback` (Proc) Called and passed the result object when a
56
+ # subprocess exits.
57
+ # * `:in` Connects the input stream of the subprocess. See the section on
58
+ # stream handling.
59
+ # * `:out` Connects the standard output stream of the subprocess. See the
60
+ # section on stream handling.
61
+ # * `:err` Connects the standard error stream of the subprocess. See the
62
+ # section on stream handling.
64
63
  #
65
64
  # In addition, the following options recognized by `Process#spawn` are
66
65
  # supported.
67
66
  #
68
- # * `:chdir`
69
- # * `:close_others`
70
- # * `:new_pgroup`
71
- # * `:pgroup`
72
- # * `:umask`
73
- # * `:unsetenv_others`
67
+ # * `:chdir`
68
+ # * `:close_others`
69
+ # * `:new_pgroup`
70
+ # * `:pgroup`
71
+ # * `:umask`
72
+ # * `:unsetenv_others`
74
73
  #
75
74
  # Any other options are ignored.
76
75
  #
@@ -95,48 +94,53 @@ module Toys
95
94
  # Following is a full list of the stream handling options, along with how
96
95
  # to specify them using the `:in`, `:out`, and `:err` options.
97
96
  #
98
- # * **Close the stream:** You may close the stream by passing `:close` as
99
- # the option value. This is the same as passing `:close` to
100
- # `Process#spawn`.
101
- # * **Redirect to null:** You may redirect to a null stream by passing
102
- # `:null` as the option value. This connects to a stream that is not
103
- # closed but contains no data, i.e. `/dev/null` on unix systems. This is
104
- # the default if the subprocess is run in the background.
105
- # * **Inherit parent stream:** You may inherit the corresponding stream in
106
- # the parent process by passing `:inherit` as the option value. This is
107
- # the default if the subprocess is *not* run in the background.
108
- # * **Redirect to a file:** You may redirect to a file. This reads from an
109
- # existing file when connected to `:in`, and creates or appends to a
110
- # file when connected to `:out` or `:err`. To specify a file, use the
111
- # setting `[:file, "/path/to/file"]`. You may also, when writing a file,
112
- # append an optional mode and permission code to the array. For
113
- # example, `[:file, "/path/to/file", "a", 0644]`.
114
- # * **Redirect to an IO object:** You may redirect to an IO object in the
115
- # parent process, by passing the IO object as the option value. You may
116
- # use any IO object. For example, you could connect the child's output
117
- # to the parent's error using `out: $stderr`, or you could connect to an
118
- # existing File stream. Unlike `Process#spawn`, this works for IO
119
- # objects that do not have a corresponding file descriptor (such as
120
- # StringIO objects). In such a case, a thread will be spawned to pipe
121
- # the IO data through to the child process.
122
- # * **Combine with another child stream:** You may redirect one child
123
- # output stream to another, to combine them. To merge the child's error
124
- # stream into its output stream, use `err: [:child, :out]`.
125
- # * **Read from a string:** You may pass a string to the input stream by
126
- # setting `[:string, "the string"]`. This works only for `:in`.
127
- # * **Capture output stream:** You may capture a stream and make it
128
- # available on the {Toys::Utils::Exec::Result} object, using the setting
129
- # `:capture`. This works only for the `:out` and `:err` streams.
130
- # * **Use the controller:** You may hook a stream to the controller using
131
- # the setting `:controller`. You can then manipulate the stream via the
132
- # controller. If you pass a block to {Toys::Utils::Exec#exec}, it yields
133
- # the {Toys::Utils::Exec::Controller}, giving you access to streams.
97
+ # * **Close the stream:** You may close the stream by passing `:close` as
98
+ # the option value. This is the same as passing `:close` to
99
+ # `Process#spawn`.
100
+ # * **Redirect to null:** You may redirect to a null stream by passing
101
+ # `:null` as the option value. This connects to a stream that is not
102
+ # closed but contains no data, i.e. `/dev/null` on unix systems. This
103
+ # is the default if the subprocess is run in the background.
104
+ # * **Inherit parent stream:** You may inherit the corresponding stream
105
+ # in the parent process by passing `:inherit` as the option value. This
106
+ # is the default if the subprocess is *not* run in the background.
107
+ # * **Redirect to a file:** You may redirect to a file. This reads from
108
+ # an existing file when connected to `:in`, and creates or appends to a
109
+ # file when connected to `:out` or `:err`. To specify a file, use the
110
+ # setting `[:file, "/path/to/file"]`. You may also, when writing a
111
+ # file, append an optional mode and permission code to the array. For
112
+ # example, `[:file, "/path/to/file", "a", 0644]`.
113
+ # * **Redirect to an IO object:** You may redirect to an IO object in the
114
+ # parent process, by passing the IO object as the option value. You may
115
+ # use any IO object. For example, you could connect the child's output
116
+ # to the parent's error using `out: $stderr`, or you could connect to
117
+ # an existing File stream. Unlike `Process#spawn`, this works for IO
118
+ # objects that do not have a corresponding file descriptor (such as
119
+ # StringIO objects). In such a case, a thread will be spawned to pipe
120
+ # the IO data through to the child process.
121
+ # * **Combine with another child stream:** You may redirect one child
122
+ # output stream to another, to combine them. To merge the child's error
123
+ # stream into its output stream, use `err: [:child, :out]`.
124
+ # * **Read from a string:** You may pass a string to the input stream by
125
+ # setting `[:string, "the string"]`. This works only for `:in`.
126
+ # * **Capture output stream:** You may capture a stream and make it
127
+ # available on the {Toys::Utils::Exec::Result} object, using the
128
+ # setting `:capture`. This works only for the `:out` and `:err`
129
+ # streams.
130
+ # * **Use the controller:** You may hook a stream to the controller using
131
+ # the setting `:controller`. You can then manipulate the stream via the
132
+ # controller. If you pass a block to {Toys::Utils::Exec#exec}, it
133
+ # yields the {Toys::Utils::Exec::Controller}, giving you access to
134
+ # streams.
134
135
  #
135
136
  class Exec
136
137
  ##
137
138
  # Create an exec service.
138
139
  #
139
- # @param [Hash] opts Initial default options.
140
+ # @param block [Proc] A block that is called if a key is not found. It is
141
+ # passed the unknown key, and expected to return a default value
142
+ # (which can be nil).
143
+ # @param opts [Hash] Initial default options.
140
144
  #
141
145
  def initialize(opts = {}, &block)
142
146
  @default_opts = Opts.new(&block).add(opts)
@@ -145,7 +149,8 @@ module Toys
145
149
  ##
146
150
  # Set default options
147
151
  #
148
- # @param [Hash] opts New default options to set
152
+ # @param opts [Hash] New default options to set
153
+ # @return [self]
149
154
  #
150
155
  def configure_defaults(opts = {})
151
156
  @default_opts.add(opts)
@@ -159,15 +164,16 @@ module Toys
159
164
  # If the process is not set to run in the background, and a block is
160
165
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
161
166
  #
162
- # @param [String,Array<String>] cmd The command to execute.
163
- # @param [Hash] opts The command options. See the section on
167
+ # @param cmd [String,Array<String>] The command to execute.
168
+ # @param opts [Hash] The command options. See the section on
164
169
  # configuration options in the {Toys::Utils::Exec} module docs.
165
170
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
166
171
  # for the subprocess streams.
167
172
  #
168
- # @return [Toys::Utils::Exec::Controller,Toys::Utils::Exec::Result] The
169
- # subprocess controller or result, depending on whether the process
170
- # is running in the background or foreground.
173
+ # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
174
+ # the process is running in the background.
175
+ # @return [Toys::Utils::Exec::Result] The result, if the process ran in
176
+ # the foreground.
171
177
  #
172
178
  def exec(cmd, opts = {}, &block)
173
179
  exec_opts = Opts.new(@default_opts).add(opts)
@@ -191,15 +197,16 @@ module Toys
191
197
  # If the process is not set to run in the background, and a block is
192
198
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
193
199
  #
194
- # @param [String,Array<String>] args The arguments to ruby.
195
- # @param [Hash] opts The command options. See the section on
200
+ # @param args [String,Array<String>] The arguments to ruby.
201
+ # @param opts [Hash] The command options. See the section on
196
202
  # configuration options in the {Toys::Utils::Exec} module docs.
197
203
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
198
204
  # for the subprocess streams.
199
205
  #
200
- # @return [Toys::Utils::Exec::Controller,Toys::Utils::Exec::Result] The
201
- # subprocess controller or result, depending on whether the process
202
- # is running in the background or foreground.
206
+ # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
207
+ # the process is running in the background.
208
+ # @return [Toys::Utils::Exec::Result] The result, if the process ran in
209
+ # the foreground.
203
210
  #
204
211
  def exec_ruby(args, opts = {}, &block)
205
212
  cmd = args.is_a?(::Array) ? [::RbConfig.ruby] + args : "#{::RbConfig.ruby} #{args}"
@@ -214,15 +221,16 @@ module Toys
214
221
  # If the process is not set to run in the background, and a block is
215
222
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
216
223
  #
217
- # @param [Proc] func The proc to call.
218
- # @param [Hash] opts The command options. See the section on
224
+ # @param func [Proc] The proc to call.
225
+ # @param opts [Hash] The command options. See the section on
219
226
  # configuration options in the {Toys::Utils::Exec} module docs.
220
227
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
221
228
  # for the subprocess streams.
222
229
  #
223
- # @return [Toys::Utils::Exec::Controller,Toys::Utils::Exec::Result] The
224
- # subprocess controller or result, depending on whether the process
225
- # is running in the background or foreground.
230
+ # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
231
+ # the process is running in the background.
232
+ # @return [Toys::Utils::Exec::Result] The result, if the process ran in
233
+ # the foreground.
226
234
  #
227
235
  def exec_proc(func, opts = {}, &block)
228
236
  exec_opts = Opts.new(@default_opts).add(opts)
@@ -240,8 +248,8 @@ module Toys
240
248
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
241
249
  # yielded to it.
242
250
  #
243
- # @param [String,Array<String>] cmd The command to execute.
244
- # @param [Hash] opts The command options. See the section on
251
+ # @param cmd [String,Array<String>] The command to execute.
252
+ # @param opts [Hash] The command options. See the section on
245
253
  # configuration options in the {Toys::Utils::Exec} module docs.
246
254
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
247
255
  # for the subprocess streams.
@@ -261,8 +269,8 @@ module Toys
261
269
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
262
270
  # yielded to it.
263
271
  #
264
- # @param [String,Array<String>] args The arguments to ruby.
265
- # @param [Hash] opts The command options. See the section on
272
+ # @param args [String,Array<String>] The arguments to ruby.
273
+ # @param opts [Hash] The command options. See the section on
266
274
  # configuration options in the {Toys::Utils::Exec} module docs.
267
275
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
268
276
  # for the subprocess streams.
@@ -282,8 +290,8 @@ module Toys
282
290
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
283
291
  # yielded to it.
284
292
  #
285
- # @param [Proc] func The proc to call.
286
- # @param [Hash] opts The command options. See the section on
293
+ # @param func [Proc] The proc to call.
294
+ # @param opts [Hash] The command options. See the section on
287
295
  # configuration options in the {Toys::Utils::Exec} module docs.
288
296
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
289
297
  # for the subprocess streams.
@@ -298,8 +306,11 @@ module Toys
298
306
  # Execute the given string in a shell. Returns the exit code.
299
307
  # Cannot be run in the background.
300
308
  #
301
- # @param [String] cmd The shell command to execute.
302
- # @param [Hash] opts The command options. See the section on
309
+ # If a block is provided, a {Toys::Utils::Exec::Controller} will be
310
+ # yielded to it.
311
+ #
312
+ # @param cmd [String] The shell command to execute.
313
+ # @param opts [Hash] The command options. See the section on
303
314
  # configuration options in the {Toys::Utils::Exec} module docs.
304
315
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
305
316
  # for the subprocess streams.
@@ -319,33 +330,35 @@ module Toys
319
330
  # Option keys that belong to exec configuration
320
331
  # @private
321
332
  #
322
- CONFIG_KEYS = %i[
323
- argv0
324
- background
325
- cli
326
- env
327
- err
328
- in
329
- logger
330
- log_cmd
331
- log_level
332
- nonzero_status_handler
333
- out
333
+ CONFIG_KEYS = [
334
+ :argv0,
335
+ :background,
336
+ :cli,
337
+ :env,
338
+ :err,
339
+ :in,
340
+ :logger,
341
+ :log_cmd,
342
+ :log_level,
343
+ :name,
344
+ :out,
345
+ :result_callback,
334
346
  ].freeze
335
347
 
336
348
  ##
337
349
  # Option keys that belong to spawn configuration
338
350
  # @private
339
351
  #
340
- SPAWN_KEYS = %i[
341
- chdir
342
- close_others
343
- new_pgroup
344
- pgroup
345
- umask
346
- unsetenv_others
352
+ SPAWN_KEYS = [
353
+ :chdir,
354
+ :close_others,
355
+ :new_pgroup,
356
+ :pgroup,
357
+ :umask,
358
+ :unsetenv_others,
347
359
  ].freeze
348
360
 
361
+ ## @private
349
362
  def initialize(parent = nil)
350
363
  if parent
351
364
  @config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
@@ -359,6 +372,7 @@ module Toys
359
372
  end
360
373
  end
361
374
 
375
+ ## @private
362
376
  def add(config)
363
377
  config.each do |k, v|
364
378
  if CONFIG_KEYS.include?(k)
@@ -372,6 +386,7 @@ module Toys
372
386
  self
373
387
  end
374
388
 
389
+ ## @private
375
390
  def delete(*keys)
376
391
  keys.each do |k|
377
392
  if CONFIG_KEYS.include?(k)
@@ -385,7 +400,10 @@ module Toys
385
400
  self
386
401
  end
387
402
 
403
+ ## @private
388
404
  attr_reader :config_opts
405
+
406
+ ## @private
389
407
  attr_reader :spawn_opts
390
408
  end
391
409
 
@@ -398,53 +416,84 @@ module Toys
398
416
  #
399
417
  class Controller
400
418
  ## @private
401
- def initialize(controller_streams, captures, pid, join_threads, nonzero_status_handler)
419
+ def initialize(name, controller_streams, captures, pid, join_threads, result_callback)
420
+ @name = name
402
421
  @in = controller_streams[:in]
403
422
  @out = controller_streams[:out]
404
423
  @err = controller_streams[:err]
405
424
  @captures = captures
406
- @pid = pid
425
+ @pid = @exception = @wait_thread = nil
426
+ case pid
427
+ when ::Integer
428
+ @pid = pid
429
+ @wait_thread = ::Process.detach(pid)
430
+ when ::Exception
431
+ @exception = pid
432
+ end
407
433
  @join_threads = join_threads
408
- @nonzero_status_handler = nonzero_status_handler
409
- @wait_thread = ::Process.detach(pid)
434
+ @result_callback = result_callback
410
435
  @result = nil
411
436
  end
412
437
 
413
438
  ##
414
- # Return the subcommand's standard input stream (which can be written
415
- # to), if the command was configured with `in: :controller`.
416
- # Returns `nil` otherwise.
417
- # @return [IO,nil]
439
+ # The subcommand's name.
440
+ # @return [Object]
441
+ #
442
+ attr_reader :name
443
+
444
+ ##
445
+ # The subcommand's standard input stream (which can be written to).
446
+ #
447
+ # @return [IO] if the command was configured with `in: :controller`
448
+ # @return [nil] if the command was not configured with
449
+ # `in: :controller`
418
450
  #
419
451
  attr_reader :in
420
452
 
421
453
  ##
422
- # Return the subcommand's standard output stream (which can be read
423
- # from), if the command was configured with `out: :controller`.
424
- # Returns `nil` otherwise.
425
- # @return [IO,nil]
454
+ # The subcommand's standard output stream (which can be read from).
455
+ #
456
+ # @return [IO] if the command was configured with `out: :controller`
457
+ # @return [nil] if the command was not configured with
458
+ # `out: :controller`
426
459
  #
427
460
  attr_reader :out
428
461
 
429
462
  ##
430
- # Return the subcommand's standard error stream (which can be read
431
- # from), if the command was configured with `err: :controller`.
432
- # Returns `nil` otherwise.
433
- # @return [IO,nil]
463
+ # The subcommand's standard error stream (which can be read from).
464
+ #
465
+ # @return [IO] if the command was configured with `err: :controller`
466
+ # @return [nil] if the command was not configured with
467
+ # `err: :controller`
434
468
  #
435
469
  attr_reader :err
436
470
 
437
471
  ##
438
- # Returns the process ID.
439
- # @return [Integer]
472
+ # The process ID.
473
+ #
474
+ # Exactly one of `exception` and `pid` will be non-nil.
475
+ #
476
+ # @return [Integer] if the process start was successful
477
+ # @return [nil] if the process could not be started.
440
478
  #
441
479
  attr_reader :pid
442
480
 
481
+ ##
482
+ # The exception raised when the process failed to start.
483
+ #
484
+ # Exactly one of `exception` and `pid` will be non-nil.
485
+ #
486
+ # @return [Exception] if the process failed to start.
487
+ # @return [nil] if the process start was successful.
488
+ #
489
+ attr_reader :exception
490
+
443
491
  ##
444
492
  # Captures the remaining data in the given stream.
445
493
  # After calling this, do not read directly from the stream.
446
494
  #
447
- # @param [:out,:err] which Which stream to capture
495
+ # @param which [:out,:err] Which stream to capture
496
+ # @return [self]
448
497
  #
449
498
  def capture(which)
450
499
  stream = stream_for(which)
@@ -462,6 +511,8 @@ module Toys
462
511
  # Captures the remaining data in the standard output stream.
463
512
  # After calling this, do not read directly from the stream.
464
513
  #
514
+ # @return [self]
515
+ #
465
516
  def capture_out
466
517
  capture(:out)
467
518
  end
@@ -470,6 +521,8 @@ module Toys
470
521
  # Captures the remaining data in the standard error stream.
471
522
  # After calling this, do not read directly from the stream.
472
523
  #
524
+ # @return [self]
525
+ #
473
526
  def capture_err
474
527
  capture(:err)
475
528
  end
@@ -484,10 +537,11 @@ module Toys
484
537
  #
485
538
  # After calling this, do not interact directly with the stream.
486
539
  #
487
- # @param [:in,:out,:err] which Which stream to redirect
488
- # @param [IO,StringIO,String,:null] io Where to redirect the stream
489
- # @param [Object...] io_args The mode and permissions for opening the
540
+ # @param which [:in,:out,:err] Which stream to redirect
541
+ # @param io [IO,StringIO,String,:null] Where to redirect the stream
542
+ # @param io_args [Object...] The mode and permissions for opening the
490
543
  # file, if redirecting to/from a file.
544
+ # @return [self]
491
545
  #
492
546
  def redirect(which, io, *io_args)
493
547
  io = ::File::NULL if io == :null
@@ -508,6 +562,7 @@ module Toys
508
562
  io.close
509
563
  end
510
564
  end
565
+ self
511
566
  end
512
567
 
513
568
  ##
@@ -520,9 +575,10 @@ module Toys
520
575
  #
521
576
  # After calling this, do not interact directly with the stream.
522
577
  #
523
- # @param [IO,StringIO,String,:null] io Where to redirect the stream
524
- # @param [Object...] io_args The mode and permissions for opening the
578
+ # @param io [IO,StringIO,String,:null] Where to redirect the stream
579
+ # @param io_args [Object...] The mode and permissions for opening the
525
580
  # file, if redirecting from a file.
581
+ # @return [self]
526
582
  #
527
583
  def redirect_in(io, *io_args)
528
584
  redirect(:in, io, *io_args)
@@ -538,9 +594,10 @@ module Toys
538
594
  #
539
595
  # After calling this, do not interact directly with the stream.
540
596
  #
541
- # @param [IO,StringIO,String,:null] io Where to redirect the stream
542
- # @param [Object...] io_args The mode and permissions for opening the
597
+ # @param io [IO,StringIO,String,:null] Where to redirect the stream
598
+ # @param io_args [Object...] The mode and permissions for opening the
543
599
  # file, if redirecting to a file.
600
+ # @return [self]
544
601
  #
545
602
  def redirect_out(io, *io_args)
546
603
  redirect(:out, io, *io_args)
@@ -555,9 +612,10 @@ module Toys
555
612
  #
556
613
  # After calling this, do not interact directly with the stream.
557
614
  #
558
- # @param [IO,StringIO,String] io Where to redirect the stream
559
- # @param [Object...] io_args The mode and permissions for opening the
615
+ # @param io [IO,StringIO,String] Where to redirect the stream
616
+ # @param io_args [Object...] The mode and permissions for opening the
560
617
  # file, if redirecting to a file.
618
+ # @return [self]
561
619
  #
562
620
  def redirect_err(io, *io_args)
563
621
  redirect(:err, io, *io_args)
@@ -567,10 +625,12 @@ module Toys
567
625
  # Send the given signal to the process. The signal may be specified
568
626
  # by name or number.
569
627
  #
570
- # @param [Integer,String] sig The signal to send.
628
+ # @param sig [Integer,String] The signal to send.
629
+ # @return [self]
571
630
  #
572
631
  def kill(sig)
573
- ::Process.kill(sig, pid)
632
+ ::Process.kill(sig, pid) if pid
633
+ self
574
634
  end
575
635
  alias signal kill
576
636
 
@@ -580,27 +640,24 @@ module Toys
580
640
  # @return [Boolean]
581
641
  #
582
642
  def executing?
583
- @wait_thread.status ? true : false
643
+ @wait_thread&.status ? true : false
584
644
  end
585
645
 
586
646
  ##
587
647
  # Wait for the subcommand to complete, and return a result object.
588
648
  #
589
- # @param [Numeric,nil] timeout The timeout in seconds, or `nil` to
649
+ # @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
590
650
  # wait indefinitely.
591
- # @return [Toys::Utils::Exec::Result,nil] The result object, or `nil`
592
- # if a timeout occurred.
651
+ # @return [Toys::Utils::Exec::Result] The result object
652
+ # @return [nil] if a timeout occurred.
593
653
  #
594
654
  def result(timeout: nil)
595
- return nil unless @wait_thread.join(timeout)
655
+ return nil if @wait_thread && !@wait_thread.join(timeout)
596
656
  @result ||= begin
597
657
  close_streams
598
658
  @join_threads.each(&:join)
599
- status = @wait_thread.value
600
- if @nonzero_status_handler && status.exitstatus != 0
601
- @nonzero_status_handler.call(status)
602
- end
603
- Result.new(@captures[:out], @captures[:err], status)
659
+ Result.new(name, @captures[:out], @captures[:err], @wait_thread&.value, @exception)
660
+ .tap { |result| @result_callback&.call(result) }
604
661
  end
605
662
  end
606
663
 
@@ -640,46 +697,76 @@ module Toys
640
697
  end
641
698
 
642
699
  ##
643
- # The return result from a subcommand
700
+ # The result returned from a subcommand execution.
644
701
  #
645
702
  class Result
646
703
  ## @private
647
- def initialize(out, err, status)
704
+ def initialize(name, out, err, status, exception)
705
+ @name = name
648
706
  @captured_out = out
649
707
  @captured_err = err
650
708
  @status = status
709
+ @exception = exception
651
710
  end
652
711
 
653
712
  ##
654
- # Returns the captured output string, if the command was configured
655
- # with `out: :capture`. Returns `nil` otherwise.
656
- # @return [String,nil]
713
+ # The subcommand's name.
714
+ #
715
+ # @return [Object]
716
+ #
717
+ attr_reader :name
718
+
719
+ ##
720
+ # The captured output string.
721
+ #
722
+ # @return [String] The string captured from stdout.
723
+ # @return [nil] if the command was not configured to capture stdout.
657
724
  #
658
725
  attr_reader :captured_out
659
726
 
660
727
  ##
661
- # Returns the captured error string, if the command was configured
662
- # with `err: :capture`. Returns `nil` otherwise.
663
- # @return [String,nil]
728
+ # The captured error string.
729
+ #
730
+ # @return [String] The string captured from stderr.
731
+ # @return [nil] if the command was not configured to capture stderr.
664
732
  #
665
733
  attr_reader :captured_err
666
734
 
667
735
  ##
668
- # Returns the status code object.
669
- # @return [Process::Status]
736
+ # The status code object.
737
+ #
738
+ # Exactly one of `exception` and `status` will be non-nil.
739
+ #
740
+ # @return [Process::Status] The status code.
741
+ # @return [nil] if the process could not be started.
670
742
  #
671
743
  attr_reader :status
672
744
 
673
745
  ##
674
- # Returns the numeric status code.
746
+ # The exception raised if a process couldn't be started.
747
+ #
748
+ # Exactly one of `exception` and `status` will be non-nil.
749
+ #
750
+ # @return [Exception] The exception raised from process start.
751
+ # @return [nil] if the process started successfully.
752
+ #
753
+ attr_reader :exception
754
+
755
+ ##
756
+ # The numeric status code.
757
+ #
758
+ # This will be a nonzero integer if the process failed to start. That
759
+ # is, `exit_code` will never be `nil`, even if `status` is `nil`.
760
+ #
675
761
  # @return [Integer]
676
762
  #
677
763
  def exit_code
678
- status.exitstatus
764
+ status ? status.exitstatus : 127
679
765
  end
680
766
 
681
767
  ##
682
768
  # Returns true if the subprocess terminated with a zero status.
769
+ #
683
770
  # @return [Boolean]
684
771
  #
685
772
  def success?
@@ -688,6 +775,7 @@ module Toys
688
775
 
689
776
  ##
690
777
  # Returns true if the subprocess terminated with a nonzero status.
778
+ #
691
779
  # @return [Boolean]
692
780
  #
693
781
  def error?
@@ -719,10 +807,7 @@ module Toys
719
807
  setup_out_stream(:out)
720
808
  setup_out_stream(:err)
721
809
  log_command
722
- pid = @fork_func ? start_fork : start_process
723
- @child_streams.each(&:close)
724
- controller = Controller.new(@controller_streams, @captures, pid, @join_threads,
725
- @config_opts[:nonzero_status_handler])
810
+ controller = start_with_controller
726
811
  return controller if @config_opts[:background]
727
812
  begin
728
813
  @block&.call(controller)
@@ -743,6 +828,18 @@ module Toys
743
828
  end
744
829
  end
745
830
 
831
+ def start_with_controller
832
+ pid =
833
+ begin
834
+ @fork_func ? start_fork : start_process
835
+ rescue ::StandardError => e
836
+ e
837
+ end
838
+ @child_streams.each(&:close)
839
+ Controller.new(@config_opts[:name], @controller_streams, @captures, pid,
840
+ @join_threads, @config_opts[:result_callback])
841
+ end
842
+
746
843
  def start_process
747
844
  args = []
748
845
  args << @config_opts[:env] if @config_opts[:env]