toys-core 0.9.2 → 0.10.2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +47 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +3 -3
  6. data/lib/toys-core.rb +14 -21
  7. data/lib/toys/acceptor.rb +0 -21
  8. data/lib/toys/arg_parser.rb +1 -22
  9. data/lib/toys/cli.rb +102 -70
  10. data/lib/toys/compat.rb +49 -41
  11. data/lib/toys/completion.rb +0 -21
  12. data/lib/toys/context.rb +0 -23
  13. data/lib/toys/core.rb +1 -22
  14. data/lib/toys/dsl/flag.rb +0 -21
  15. data/lib/toys/dsl/flag_group.rb +0 -21
  16. data/lib/toys/dsl/positional_arg.rb +0 -21
  17. data/lib/toys/dsl/tool.rb +136 -51
  18. data/lib/toys/errors.rb +1 -22
  19. data/lib/toys/flag.rb +0 -21
  20. data/lib/toys/flag_group.rb +0 -21
  21. data/lib/toys/input_file.rb +0 -21
  22. data/lib/toys/loader.rb +42 -78
  23. data/lib/toys/middleware.rb +146 -77
  24. data/lib/toys/mixin.rb +0 -21
  25. data/lib/toys/module_lookup.rb +3 -26
  26. data/lib/toys/positional_arg.rb +0 -21
  27. data/lib/toys/source_info.rb +49 -38
  28. data/lib/toys/standard_middleware/add_verbosity_flags.rb +0 -23
  29. data/lib/toys/standard_middleware/apply_config.rb +42 -0
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +7 -28
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +0 -23
  32. data/lib/toys/standard_middleware/show_help.rb +0 -23
  33. data/lib/toys/standard_middleware/show_root_version.rb +0 -23
  34. data/lib/toys/standard_mixins/bundler.rb +89 -0
  35. data/lib/toys/standard_mixins/exec.rb +478 -128
  36. data/lib/toys/standard_mixins/fileutils.rb +0 -21
  37. data/lib/toys/standard_mixins/gems.rb +2 -24
  38. data/lib/toys/standard_mixins/highline.rb +0 -21
  39. data/lib/toys/standard_mixins/terminal.rb +0 -21
  40. data/lib/toys/template.rb +0 -21
  41. data/lib/toys/tool.rb +22 -34
  42. data/lib/toys/utils/completion_engine.rb +0 -21
  43. data/lib/toys/utils/exec.rb +142 -71
  44. data/lib/toys/utils/gems.rb +181 -63
  45. data/lib/toys/utils/help_text.rb +0 -21
  46. data/lib/toys/utils/terminal.rb +46 -37
  47. data/lib/toys/wrappable_string.rb +0 -21
  48. metadata +25 -9
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
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
3
  module Toys
25
4
  module StandardMiddleware
26
5
  ##
@@ -28,8 +7,6 @@ module Toys
28
7
  # `--version` flag is given.
29
8
  #
30
9
  class ShowRootVersion
31
- include Middleware
32
-
33
10
  ##
34
11
  # Default version flags
35
12
  # @return [Array<String>]
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toys
4
+ module StandardMixins
5
+ ##
6
+ # Ensures that a bundle is installed and set up when this tool is run.
7
+ #
8
+ # The following parameters can be passed when including this mixin:
9
+ #
10
+ # * `:groups` (Array<String>) The groups to include in setup
11
+ #
12
+ # * `:search_dirs` (Array<String,Symbol>) Directories to search for a
13
+ # Gemfile.
14
+ #
15
+ # You can pass full directory paths, and/or any of the following:
16
+ # * `:context` - the current context directory
17
+ # * `:current` - the current working directory
18
+ # * `:toys` - the Toys directory containing the tool definition
19
+ #
20
+ # The default is to search `[:toys, :context, :current]` in that order.
21
+ #
22
+ # * `:on_missing` (Symbol) What to do if a needed gem is not installed.
23
+ #
24
+ # Supported values:
25
+ # * `:confirm` - prompt the user on whether to install (default)
26
+ # * `:error` - raise an exception
27
+ # * `:install` - just install the gem
28
+ #
29
+ # * `:on_conflict` (Symbol) What to do if bundler has already been run
30
+ # with a different Gemfile.
31
+ #
32
+ # Supported values:
33
+ # * `:error` - raise an exception (default)
34
+ # * `:ignore` - just silently proceed without bundling again
35
+ # * `:warn` - print a warning and proceed without bundling again
36
+ #
37
+ # * `:terminal` (Toys::Utils::Terminal) Terminal to use (optional)
38
+ # * `:input` (IO) Input IO (optional, defaults to STDIN)
39
+ # * `:output` (IO) Output IO (optional, defaults to STDOUT)
40
+ #
41
+ module Bundler
42
+ include Mixin
43
+
44
+ on_initialize do
45
+ |groups: nil,
46
+ search_dirs: nil,
47
+ on_missing: nil,
48
+ on_conflict: nil,
49
+ terminal: nil,
50
+ input: nil,
51
+ output: nil|
52
+ require "toys/utils/gems"
53
+ search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(search_dirs, self)
54
+ gems = ::Toys::Utils::Gems.new(on_missing: on_missing, on_conflict: on_conflict,
55
+ terminal: terminal, input: input, output: output)
56
+ gems.bundle(groups: groups, search_dirs: search_dirs)
57
+ end
58
+
59
+ ## @private
60
+ def self.resolve_search_dirs(search_dirs, context)
61
+ search_dirs ||= [:toys, :context, :current]
62
+ Array(search_dirs).flat_map do |dir|
63
+ case dir
64
+ when :context
65
+ context[::Toys::Context::Key::CONTEXT_DIRECTORY]
66
+ when :current
67
+ ::Dir.getwd
68
+ when :toys
69
+ toys_dir_stack(context[::Toys::Context::Key::TOOL_SOURCE])
70
+ when ::String
71
+ dir
72
+ else
73
+ raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
74
+ end
75
+ end
76
+ end
77
+
78
+ ## @private
79
+ def self.toys_dir_stack(source_info)
80
+ dirs = []
81
+ while source_info
82
+ dirs << source_info.source_path if source_info.source_type == :directory
83
+ source_info = source_info.parent
84
+ end
85
+ dirs
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
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
3
  module Toys
25
4
  module StandardMixins
26
5
  ##
@@ -37,54 +16,211 @@ module Toys
37
16
  # This is a frontend for {Toys::Utils::Exec}. More information is
38
17
  # available in that class's documentation.
39
18
  #
19
+ # ## Features
20
+ #
21
+ # ### Controlling processes
22
+ #
23
+ # A process can be started in the *foreground* or the *background*. If you
24
+ # start a foreground process, it will "take over" your standard input and
25
+ # output streams by default, and it will keep control until it completes.
26
+ # If you start a background process, its streams will be redirected to null
27
+ # by default, and control will be returned to you immediately.
28
+ #
29
+ # When a process is running, you can control it using a
30
+ # {Toys::Utils::Exec::Controller} object. Use a controller to interact with
31
+ # the process's input and output streams, send it signals, or wait for it
32
+ # to complete.
33
+ #
34
+ # When running a process in the foreground, the controller will be yielded
35
+ # to an optional block. For example, the following code starts a process in
36
+ # the foreground and passes its output stream to a controller.
37
+ #
38
+ # exec(["git", "init"], out: :controller) do |controller|
39
+ # loop do
40
+ # line = controller.out.gets
41
+ # break if line.nil?
42
+ # puts "Got line: #{line}"
43
+ # end
44
+ # end
45
+ #
46
+ # When running a process in the background, the controller is returned from
47
+ # the method that starts the process:
48
+ #
49
+ # controller = exec_service.exec(["git", "init"], background: true)
50
+ #
51
+ # ### Stream handling
52
+ #
53
+ # By default, subprocess streams are connected to the corresponding streams
54
+ # in the parent process. You can change this behavior, redirecting streams
55
+ # or providing ways to control them, using the `:in`, `:out`, and `:err`
56
+ # options.
57
+ #
58
+ # Three general strategies are available for custom stream handling. First,
59
+ # you may redirect to other streams such as files, IO objects, or Ruby
60
+ # strings. Some of these options map directly to options provided by the
61
+ # `Process#spawn` method. Second, you may use a controller to manipulate
62
+ # the streams programmatically. Third, you may capture output stream data
63
+ # and make it available in the result.
64
+ #
65
+ # Following is a full list of the stream handling options, along with how
66
+ # to specify them using the `:in`, `:out`, and `:err` options.
67
+ #
68
+ # * **Inherit parent stream:** You may inherit the corresponding stream
69
+ # in the parent process by passing `:inherit` as the option value. This
70
+ # is the default if the subprocess is *not* run in the background.
71
+ # * **Redirect to null:** You may redirect to a null stream by passing
72
+ # `:null` as the option value. This connects to a stream that is not
73
+ # closed but contains no data, i.e. `/dev/null` on unix systems. This
74
+ # is the default if the subprocess is run in the background.
75
+ # * **Close the stream:** You may close the stream by passing `:close` as
76
+ # the option value. This is the same as passing `:close` to
77
+ # `Process#spawn`.
78
+ # * **Redirect to a file:** You may redirect to a file. This reads from
79
+ # an existing file when connected to `:in`, and creates or appends to a
80
+ # file when connected to `:out` or `:err`. To specify a file, use the
81
+ # setting `[:file, "/path/to/file"]`. You may also, when writing a
82
+ # file, append an optional mode and permission code to the array. For
83
+ # example, `[:file, "/path/to/file", "a", 0644]`.
84
+ # * **Redirect to an IO object:** You may redirect to an IO object in the
85
+ # parent process, by passing the IO object as the option value. You may
86
+ # use any IO object. For example, you could connect the child's output
87
+ # to the parent's error using `out: $stderr`, or you could connect to
88
+ # an existing File stream. Unlike `Process#spawn`, this works for IO
89
+ # objects that do not have a corresponding file descriptor (such as
90
+ # StringIO objects). In such a case, a thread will be spawned to pipe
91
+ # the IO data through to the child process.
92
+ # * **Combine with another child stream:** You may redirect one child
93
+ # output stream to another, to combine them. To merge the child's error
94
+ # stream into its output stream, use `err: [:child, :out]`.
95
+ # * **Read from a string:** You may pass a string to the input stream by
96
+ # setting `[:string, "the string"]`. This works only for `:in`.
97
+ # * **Capture output stream:** You may capture a stream and make it
98
+ # available on the {Toys::Utils::Exec::Result} object, using the
99
+ # setting `:capture`. This works only for the `:out` and `:err`
100
+ # streams.
101
+ # * **Use the controller:** You may hook a stream to the controller using
102
+ # the setting `:controller`. You can then manipulate the stream via the
103
+ # controller. If you pass a block to {Toys::StandardMixins::Exec#exec},
104
+ # it yields the {Toys::Utils::Exec::Controller}, giving you access to
105
+ # streams.
106
+ #
107
+ # ### Result handling
108
+ #
109
+ # A subprocess result is represented by a {Toys::Utils::Exec::Result}
110
+ # object, which includes the exit code, the content of any captured output
111
+ # streams, and any exeption raised when attempting to run the process.
112
+ # When you run a process in the foreground, the method will return a result
113
+ # object. When you run a process in the background, you can obtain the
114
+ # result from the controller once the process completes.
115
+ #
116
+ # The following example demonstrates running a process in the foreground
117
+ # and getting the exit code:
118
+ #
119
+ # result = exec(["git", "init"])
120
+ # puts "exit code: #{result.exit_code}"
121
+ #
122
+ # The following example demonstrates starting a process in the background,
123
+ # waiting for it to complete, and getting its exit code:
124
+ #
125
+ # controller = exec(["git", "init"], background: true)
126
+ # result = controller.result(timeout: 1.0)
127
+ # if result
128
+ # puts "exit code: #{result.exit_code}"
129
+ # else
130
+ # puts "timed out"
131
+ # end
132
+ #
133
+ # You can also provide a callback that is executed once a process
134
+ # completes. This callback can be specified as a method name or a `Proc`
135
+ # object, and will be passed the result object. For example:
136
+ #
137
+ # def run
138
+ # exec(["git", "init"], result_callback: :handle_result)
139
+ # end
140
+ # def handle_result(result)
141
+ # puts "exit code: #{result.exit_code}"
142
+ # end
143
+ #
144
+ # Finally, you can force your tool to exit if a subprocess fails, similar
145
+ # to setting the `set -e` option in bash, by setting the
146
+ # `:exit_on_nonzero_status` option. This is often set as a default
147
+ # configuration for all subprocesses run in a tool, by passing it as an
148
+ # argument to the `include` directive:
149
+ #
150
+ # include :exec, exit_on_nonzero_status: true
151
+ #
40
152
  # ## Configuration Options
41
153
  #
42
- # Subprocesses may be configured using the options in the
43
- # {Toys::Utils::Exec} class. These include a variety of options supported
44
- # by `Process#spawn`, and some options supported by {Toys::Utils::Exec}
45
- # itself.
154
+ # A variety of options can be used to control subprocesses. These can be
155
+ # provided to any method that starts a subprocess. You can also set
156
+ # defaults by passing them as keyword arguments when you `include` the
157
+ # mixin.
158
+ #
159
+ # Options that affect the behavior of subprocesses:
160
+ #
161
+ # * `:env` (Hash) Environment variables to pass to the subprocess.
162
+ # Keys represent variable names and should be strings. Values should be
163
+ # either strings or `nil`, which unsets the variable.
164
+ #
165
+ # * `:background` (Boolean) Runs the process in the background if `true`.
166
+ #
167
+ # Options related to handling results
168
+ #
169
+ # * `:result_callback` (Proc,Symbol) A procedure that is called, and
170
+ # passed the result object, when the subprocess exits. You can provide
171
+ # a `Proc` object, or the name of a method as a `Symbol`.
172
+ #
173
+ # * `:exit_on_nonzero_status` (Boolean) If set to true, a nonzero exit
174
+ # code will cause the tool to exit immediately with that same code.
175
+ #
176
+ # * `:e` (Boolean) A short name for `:exit_on_nonzero_status`.
177
+ #
178
+ # Options for connecting input and output streams. See the section above on
179
+ # stream handling for info on the values that can be passed.
180
+ #
181
+ # * `:in` Connects the input stream of the subprocess. See the section on
182
+ # stream handling.
183
+ #
184
+ # * `:out` Connects the standard output stream of the subprocess. See the
185
+ # section on stream handling.
46
186
  #
47
- # You can set default configuration by passing options to the `include`
48
- # directive. For example, to log commands at the debug level for all
49
- # subprocesses spawned by this tool:
187
+ # * `:err` Connects the standard error stream of the subprocess. See the
188
+ # section on stream handling.
50
189
  #
51
- # include :exec, log_level: Logger::DEBUG
190
+ # Options related to logging and reporting:
52
191
  #
53
- # Two special options are also recognized by the mixin.
192
+ # * `:logger` (Logger) Logger to use for logging the actual command. If
193
+ # not present, the command is not logged.
54
194
  #
55
- # * A **:result_callback** proc may take a second argument. If it does,
56
- # the context object is passed as the second argument. This is useful
57
- # if a `:result_callback` is applied to the entire tool by passing it
58
- # to the `include` directive. In that case, `self` is not set to the
59
- # context object as it normally would be in a tool's `run` method, so
60
- # you cannot access it otherwise. For example, here is how to log the
61
- # exit code for every subcommand:
195
+ # * `:log_level` (Integer,false) Level for logging the actual command.
196
+ # Defaults to Logger::INFO if not present. You may also pass `false` to
197
+ # disable logging of the command.
62
198
  #
63
- # tool "mytool" do
64
- # callback = proc do |result, context|
65
- # context.logger.info "Exit code: #{result.exit_code}"
66
- # end
67
- # include :exec, result_callback: callback
68
- # # ...
69
- # end
199
+ # * `:log_cmd` (String) The string logged for the actual command.
200
+ # Defaults to the `inspect` representation of the command.
70
201
  #
71
- # You may also pass a symbol as the `:result_callback`. The method with
72
- # that name is then called as the callback. The method must take one
73
- # argument, the result object.
202
+ # * `:name` (Object) An optional object that can be used to identify this
203
+ # subprocess. It is available in the controller and result objects.
74
204
  #
75
- # * If **:exit_on_nonzero_status** is set to true, a nonzero exit code
76
- # returned by the subprocess will also cause the tool to exit
77
- # immediately with that same code.
205
+ # In addition, the following options recognized by
206
+ # [`Process#spawn`](https://ruby-doc.org/core/Process.html#method-c-spawn)
207
+ # are supported.
78
208
  #
79
- # This is particularly useful as an option to the `include` directive,
80
- # where it causes any subprocess failure to abort the tool, similar to
81
- # setting `set -e` in a bash script.
209
+ # * `:chdir` (String) Set the working directory for the command.
82
210
  #
83
- # include :exec, exit_on_nonzero_status: true
211
+ # * `:close_others` (Boolean) Whether to close non-redirected
212
+ # non-standard file descriptors.
84
213
  #
85
- # **:e** can be used as a shortcut for **:exit_on_nonzero_status**
214
+ # * `:new_pgroup` (Boolean) Create new process group (Windows only).
86
215
  #
87
- # include :exec, e: true
216
+ # * `:pgroup` (Integer,true,nil) The process group setting.
217
+ #
218
+ # * `:umask` (Integer) Umask setting for the new process.
219
+ #
220
+ # * `:unsetenv_others` (Boolean) Clear environment variables except those
221
+ # explicitly set.
222
+ #
223
+ # Any other option key will result in an `ArgumentError`.
88
224
  #
89
225
  module Exec
90
226
  include Mixin
@@ -110,10 +246,10 @@ module Toys
110
246
  end
111
247
 
112
248
  ##
113
- # Set default configuration keys.
249
+ # Set default configuration options.
114
250
  #
115
- # All options listed in the {Toys::Utils::Exec} documentation are
116
- # supported, plus the `exit_on_nonzero_status` option.
251
+ # See the {Toys::StandardMixins::Exec} module documentation for a
252
+ # description of the options.
117
253
  #
118
254
  # @param opts [keywords] The default options.
119
255
  # @return [self]
@@ -131,12 +267,25 @@ module Toys
131
267
  # If the process is not set to run in the background, and a block is
132
268
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
133
269
  #
270
+ # ## Examples
271
+ #
272
+ # Run a command without a shell, and print the exit code (0 for success):
273
+ #
274
+ # result = exec(["git", "init"])
275
+ # puts "exit code: #{result.exit_code}"
276
+ #
277
+ # Run a shell command:
278
+ #
279
+ # result = exec("cd mydir && git init")
280
+ # puts "exit code: #{result.exit_code}"
281
+ #
134
282
  # @param cmd [String,Array<String>] The command to execute.
135
- # @param opts [keywords] The command options. All options listed in the
136
- # {Toys::Utils::Exec} documentation are supported, plus the
137
- # `exit_on_nonzero_status` option.
283
+ # @param opts [keywords] The command options. See the section on
284
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
285
+ # documentation.
138
286
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
139
- # the subprocess streams.
287
+ # the subprocess. See the section on Controlling Processes in the
288
+ # {Toys::StandardMixins::Exec} module documentation.
140
289
  #
141
290
  # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
142
291
  # the process is running in the background.
@@ -154,12 +303,19 @@ module Toys
154
303
  # If the process is not set to run in the background, and a block is
155
304
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
156
305
  #
306
+ # ## Example
307
+ #
308
+ # Execute a small script with warnings
309
+ #
310
+ # exec_ruby("-w", "-e", "(1..10).each { |i| puts i }")
311
+ #
157
312
  # @param args [String,Array<String>] The arguments to ruby.
158
- # @param opts [keywords] The command options. All options listed in the
159
- # {Toys::Utils::Exec} documentation are supported, plus the
160
- # `exit_on_nonzero_status` option.
313
+ # @param opts [keywords] The command options. See the section on
314
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
315
+ # documentation.
161
316
  # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
162
- # for the subprocess streams.
317
+ # the subprocess. See the section on Controlling Processes in the
318
+ # {Toys::StandardMixins::Exec} module documentation.
163
319
  #
164
320
  # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
165
321
  # the process is running in the background.
@@ -173,17 +329,31 @@ module Toys
173
329
  alias ruby exec_ruby
174
330
 
175
331
  ##
176
- # Execute a proc in a subprocess.
332
+ # Execute a proc in a forked subprocess.
177
333
  #
178
334
  # If the process is not set to run in the background, and a block is
179
335
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
180
336
  #
337
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
338
+ # do not support this method because they do not support fork.
339
+ #
340
+ # ## Example
341
+ #
342
+ # Run a proc in a forked process.
343
+ #
344
+ # code = proc do
345
+ # puts "Spawned process ID is #{Process.pid}"
346
+ # end
347
+ # puts "Main process ID is #{Process.pid}"
348
+ # exec_proc(code)
349
+ #
181
350
  # @param func [Proc] The proc to call.
182
- # @param opts [keywords] The command options. Most options listed in the
183
- # {Toys::Utils::Exec} documentation are supported, plus the
184
- # `exit_on_nonzero_status` option.
185
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
186
- # for the subprocess streams.
351
+ # @param opts [keywords] The command options. See the section on
352
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
353
+ # documentation.
354
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
355
+ # the subprocess. See the section on Controlling Processes in the
356
+ # {Toys::StandardMixins::Exec} module documentation.
187
357
  #
188
358
  # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
189
359
  # the process is running in the background.
@@ -196,18 +366,30 @@ module Toys
196
366
  end
197
367
 
198
368
  ##
199
- # Execute a tool. The command may be given as a single string or an array
200
- # of strings, representing the tool to run and the arguments to pass.
369
+ # Execute a tool in the current CLI in a forked process.
370
+ #
371
+ # The command may be given as a single string or an array of strings,
372
+ # representing the tool to run and the arguments to pass.
201
373
  #
202
374
  # If the process is not set to run in the background, and a block is
203
375
  # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
204
376
  #
377
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
378
+ # do not support this method because they do not support fork.
379
+ #
380
+ # ## Example
381
+ #
382
+ # Run the "system update" tool and pass it an argument.
383
+ #
384
+ # exec_tool(["system", "update", "--verbose"])
385
+ #
205
386
  # @param cmd [String,Array<String>] The tool to execute.
206
- # @param opts [keywords] The command options. Most options listed in the
207
- # {Toys::Utils::Exec} documentation are supported, plus the
208
- # `exit_on_nonzero_status` option.
209
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
210
- # for the subprocess streams.
387
+ # @param opts [keywords] The command options. See the section on
388
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
389
+ # documentation.
390
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
391
+ # the subprocess. See the section on Controlling Processes in the
392
+ # {Toys::StandardMixins::Exec} module documentation.
211
393
  #
212
394
  # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
213
395
  # the process is running in the background.
@@ -220,6 +402,53 @@ module Toys
220
402
  self[KEY].exec_proc(func, **opts, &block)
221
403
  end
222
404
 
405
+ ##
406
+ # Execute a tool in a separately spawned process.
407
+ #
408
+ # The command may be given as a single string or an array of strings,
409
+ # representing the tool to run and the arguments to pass.
410
+ #
411
+ # If the process is not set to run in the background, and a block is
412
+ # provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
413
+ #
414
+ # An entirely separate spawned process is run for this tool, using the
415
+ # setting of {Toys.executable_path}. Thus, this method can be run only if
416
+ # that setting is present. The normal Toys gem does set it, but if you
417
+ # are writing your own executable using Toys-Core, you will need to set
418
+ # it explicitly for this method to work. Furthermore, Bundler, if
419
+ # present, is reset to its "unbundled" environment. Thus, the tool found,
420
+ # the behavior of the CLI, and the gem environment, might not be the same
421
+ # as those of the calling tool.
422
+ #
423
+ # This method is often used if you are already in a bundle and need to
424
+ # run a tool that uses a different bundle. It may also be necessary on
425
+ # environments without "fork" (such as JRuby or Ruby on Windows).
426
+ #
427
+ # ## Example
428
+ #
429
+ # Run the "system update" tool and pass it an argument.
430
+ #
431
+ # exec_separate_tool(["system", "update", "--verbose"])
432
+ #
433
+ # @param cmd [String,Array<String>] The tool to execute.
434
+ # @param opts [keywords] The command options. See the section on
435
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
436
+ # documentation.
437
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
438
+ # the subprocess. See the section on Controlling Processes in the
439
+ # {Toys::StandardMixins::Exec} module documentation.
440
+ #
441
+ # @return [Toys::Utils::Exec::Controller] The subprocess controller, if
442
+ # the process is running in the background.
443
+ # @return [Toys::Utils::Exec::Result] The result, if the process ran in
444
+ # the foreground.
445
+ #
446
+ def exec_separate_tool(cmd, **opts, &block)
447
+ Exec._setup_clean_process(cmd) do |clean_cmd|
448
+ exec(clean_cmd, **opts, &block)
449
+ end
450
+ end
451
+
223
452
  ##
224
453
  # Execute a command. The command may be given as a single string to pass
225
454
  # to a shell, or an array of strings indicating a posix command.
@@ -230,12 +459,20 @@ module Toys
230
459
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
231
460
  # yielded to it.
232
461
  #
462
+ # ## Example
463
+ #
464
+ # Capture the output of an echo command
465
+ #
466
+ # str = capture(["echo", "hello"])
467
+ # assert_equal("hello\n", str)
468
+ #
233
469
  # @param cmd [String,Array<String>] The command to execute.
234
- # @param opts [keywords] The command options. All options listed in the
235
- # {Toys::Utils::Exec} documentation are supported, plus the
236
- # `exit_on_nonzero_status` option.
237
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
238
- # for the subprocess streams.
470
+ # @param opts [keywords] The command options. See the section on
471
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
472
+ # documentation.
473
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
474
+ # the subprocess. See the section on Controlling Processes in the
475
+ # {Toys::StandardMixins::Exec} module documentation.
239
476
  #
240
477
  # @return [String] What was written to standard out.
241
478
  #
@@ -253,12 +490,20 @@ module Toys
253
490
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
254
491
  # yielded to it.
255
492
  #
493
+ # ## Example
494
+ #
495
+ # Capture the output of a ruby script.
496
+ #
497
+ # str = capture_ruby("-e", "(1..3).each { |i| puts i }")
498
+ # assert_equal "1\n2\n3\n", str
499
+ #
256
500
  # @param args [String,Array<String>] The arguments to ruby.
257
- # @param opts [keywords] The command options. All options listed in the
258
- # {Toys::Utils::Exec} documentation are supported, plus the
259
- # `exit_on_nonzero_status` option.
260
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
261
- # for the subprocess streams.
501
+ # @param opts [keywords] The command options. See the section on
502
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
503
+ # documentation.
504
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
505
+ # the subprocess. See the section on Controlling Processes in the
506
+ # {Toys::StandardMixins::Exec} module documentation.
262
507
  #
263
508
  # @return [String] What was written to standard out.
264
509
  #
@@ -268,7 +513,7 @@ module Toys
268
513
  end
269
514
 
270
515
  ##
271
- # Execute a proc in a subprocess.
516
+ # Execute a proc in a forked subprocess.
272
517
  #
273
518
  # Captures standard out and returns it as a string.
274
519
  # Cannot be run in the background.
@@ -276,12 +521,26 @@ module Toys
276
521
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
277
522
  # yielded to it.
278
523
  #
524
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
525
+ # do not support this method because they do not support fork.
526
+ #
527
+ # ## Example
528
+ #
529
+ # Run a proc in a forked process and capture its output:
530
+ #
531
+ # code = proc do
532
+ # puts Process.pid
533
+ # end
534
+ # forked_pid = capture_proc(code).chomp
535
+ # puts "I forked PID #{forked_pid}"
536
+ #
279
537
  # @param func [Proc] The proc to call.
280
- # @param opts [keywords] The command options. Most options listed in the
281
- # {Toys::Utils::Exec} documentation are supported, plus the
282
- # `exit_on_nonzero_status` option.
283
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
284
- # for the subprocess streams.
538
+ # @param opts [keywords] The command options. See the section on
539
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
540
+ # documentation.
541
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
542
+ # the subprocess. See the section on Controlling Processes in the
543
+ # {Toys::StandardMixins::Exec} module documentation.
285
544
  #
286
545
  # @return [String] What was written to standard out.
287
546
  #
@@ -291,21 +550,34 @@ module Toys
291
550
  end
292
551
 
293
552
  ##
294
- # Execute a tool. The command may be given as a single string or an array
295
- # of strings, representing the tool to run and the arguments to pass.
553
+ # Execute a tool in the current CLI in a forked process.
296
554
  #
297
555
  # Captures standard out and returns it as a string.
298
556
  # Cannot be run in the background.
299
557
  #
558
+ # The command may be given as a single string or an array of strings,
559
+ # representing the tool to run and the arguments to pass.
560
+ #
300
561
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
301
562
  # yielded to it.
302
563
  #
564
+ # Beware that some Ruby environments (e.g. JRuby, and Ruby on Windows)
565
+ # do not support this method because they do not support fork.
566
+ #
567
+ # ## Example
568
+ #
569
+ # Run the "system version" tool and capture its output.
570
+ #
571
+ # str = capture_tool(["system", "version"]).chomp
572
+ # puts "Version was #{str}"
573
+ #
303
574
  # @param cmd [String,Array<String>] The tool to execute.
304
- # @param opts [keywords] The command options. Most options listed in the
305
- # {Toys::Utils::Exec} documentation are supported, plus the
306
- # `exit_on_nonzero_status` option.
307
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
308
- # for the subprocess streams.
575
+ # @param opts [keywords] The command options. See the section on
576
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
577
+ # documentation.
578
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
579
+ # the subprocess. See the section on Controlling Processes in the
580
+ # {Toys::StandardMixins::Exec} module documentation.
309
581
  #
310
582
  # @return [String] What was written to standard out.
311
583
  #
@@ -315,6 +587,54 @@ module Toys
315
587
  self[KEY].capture_proc(func, **opts, &block)
316
588
  end
317
589
 
590
+ ##
591
+ # Execute a tool in a separately spawned process.
592
+ #
593
+ # Captures standard out and returns it as a string.
594
+ # Cannot be run in the background.
595
+ #
596
+ # The command may be given as a single string or an array of strings,
597
+ # representing the tool to run and the arguments to pass.
598
+ #
599
+ # If a block is provided, a {Toys::Utils::Exec::Controller} will be
600
+ # yielded to it.
601
+ #
602
+ # An entirely separate spawned process is run for this tool, using the
603
+ # setting of {Toys.executable_path}. Thus, this method can be run only if
604
+ # that setting is present. The normal Toys gem does set it, but if you
605
+ # are writing your own executable using Toys-Core, you will need to set
606
+ # it explicitly for this method to work. Furthermore, Bundler, if
607
+ # present, is reset to its "unbundled" environment. Thus, the tool found,
608
+ # the behavior of the CLI, and the gem environment, might not be the same
609
+ # as those of the calling tool.
610
+ #
611
+ # This method is often used if you are already in a bundle and need to
612
+ # run a tool that uses a different bundle. It may also be necessary on
613
+ # environments without "fork" (such as JRuby or Ruby on Windows).
614
+ #
615
+ # ## Example
616
+ #
617
+ # Run the "system version" tool and capture its output.
618
+ #
619
+ # str = capture_separate_tool(["system", "version"]).chomp
620
+ # puts "Version was #{str}"
621
+ #
622
+ # @param cmd [String,Array<String>] The tool to execute.
623
+ # @param opts [keywords] The command options. See the section on
624
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
625
+ # documentation.
626
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
627
+ # the subprocess. See the section on Controlling Processes in the
628
+ # {Toys::StandardMixins::Exec} module documentation.
629
+ #
630
+ # @return [String] What was written to standard out.
631
+ #
632
+ def capture_separate_tool(cmd, **opts, &block)
633
+ Exec._setup_clean_process(cmd) do |clean_cmd|
634
+ capture(clean_cmd, **opts, &block)
635
+ end
636
+ end
637
+
318
638
  ##
319
639
  # Execute the given string in a shell. Returns the exit code.
320
640
  # Cannot be run in the background.
@@ -322,12 +642,20 @@ module Toys
322
642
  # If a block is provided, a {Toys::Utils::Exec::Controller} will be
323
643
  # yielded to it.
324
644
  #
645
+ # ## Example
646
+ #
647
+ # Run a shell script
648
+ #
649
+ # exit_code = sh("cd mydir && git init")
650
+ # puts exit_code == 0 ? "Success!" : "Failed!"
651
+ #
325
652
  # @param cmd [String] The shell command to execute.
326
- # @param opts [keywords] The command options. All options listed in the
327
- # {Toys::Utils::Exec} documentation are supported, plus the
328
- # `exit_on_nonzero_status` option.
329
- # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
330
- # for the subprocess streams.
653
+ # @param opts [keywords] The command options. See the section on
654
+ # Configuration Options in the {Toys::StandardMixins::Exec} module
655
+ # documentation.
656
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
657
+ # the subprocess. See the section on Controlling Processes in the
658
+ # {Toys::StandardMixins::Exec} module documentation.
331
659
  #
332
660
  # @return [Integer] The exit code
333
661
  #
@@ -357,37 +685,59 @@ module Toys
357
685
 
358
686
  ## @private
359
687
  def self._setup_exec_opts(opts, context)
688
+ count = 0
689
+ result_callback = nil
360
690
  if opts.key?(:result_callback)
361
- opts = _setup_result_callback_option(opts, context)
691
+ result_callback = _interpret_result_callback(opts[:result_callback], context)
692
+ count += 1
693
+ end
694
+ [:exit_on_nonzero_status, :e].each do |sym|
695
+ if opts.key?(sym)
696
+ result_callback = _interpret_e(opts[sym], context)
697
+ count += 1
698
+ opts = opts.reject { |k, _v| k == sym }
699
+ end
362
700
  end
363
- if opts.key?(:exit_on_nonzero_status) || opts.key?(:e)
364
- opts = _setup_e_option(opts, context)
701
+ if count > 1
702
+ raise ::ArgumentError,
703
+ "You can provide at most one of: result_callback, exit_on_nonzero_status, e"
365
704
  end
705
+ opts = opts.merge(result_callback: result_callback) if count == 1
366
706
  opts
367
707
  end
368
708
 
369
709
  ## @private
370
- def self._setup_e_option(opts, context)
371
- result_callback =
372
- if opts[:exit_on_nonzero_status] || opts[:e]
373
- proc { |r| context.exit(r.exit_code) if r.error? }
374
- end
375
- opts = opts.merge(result_callback: result_callback)
376
- opts.delete(:exit_on_nonzero_status)
377
- opts.delete(:e)
378
- opts
710
+ def self._interpret_e(value, context)
711
+ value ? proc { |r| context.exit(r.exit_code) if r.error? } : nil
712
+ end
713
+
714
+ ## @private
715
+ def self._interpret_result_callback(value, context)
716
+ if value.is_a?(::Symbol)
717
+ context.method(value)
718
+ elsif value.respond_to?(:call)
719
+ proc { |r| context.instance_eval { value.call(r, context) } }
720
+ elsif value.nil?
721
+ nil
722
+ else
723
+ raise ::ArgumentError, "Bad value for result_callback"
724
+ end
379
725
  end
380
726
 
381
727
  ## @private
382
- def self._setup_result_callback_option(opts, context)
383
- orig_callback = opts[:result_callback]
384
- result_callback =
385
- if orig_callback.is_a?(::Symbol)
386
- context.method(orig_callback)
387
- elsif orig_callback.respond_to?(:call)
388
- proc { |r| orig_callback.call(r, context) }
728
+ def self._setup_clean_process(cmd)
729
+ raise ::ArgumentError, "Toys process is unknown" unless ::Toys.executable_path
730
+ cmd = ::Shellwords.split(cmd) if cmd.is_a?(::String)
731
+ cmd = Array(::Toys.executable_path) + cmd
732
+ if defined?(::Bundler)
733
+ if ::Bundler.respond_to?(:with_unbundled_env)
734
+ ::Bundler.with_unbundled_env { yield(cmd) }
735
+ else
736
+ ::Bundler.with_clean_env { yield(cmd) }
389
737
  end
390
- opts.merge(result_callback: result_callback)
738
+ else
739
+ yield(cmd)
740
+ end
391
741
  end
392
742
  end
393
743
  end