toys-core 0.7.0 → 0.8.0

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 (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]