1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
|
# The ProcessExecuter Gem
[](https://badge.fury.io/rb/process_executer)
[](https://rubydoc.info/gems/process_executer/)
[](https://rubydoc.info/gems/process_executer/file/CHANGELOG.md)
[](https://github.com/main-branch/process_executer/actions/workflows/continuous-integration.yml)
[](https://conventionalcommits.org)
[](https://main-branch.slack.com/archives/C07NG2BPG8Y)
ProcessExecuter provides an enhanced API for executing commands in subprocesses,
extending Ruby's built-in `Process.spawn` functionality.
It has additional features like capturing output, handling timeouts, streaming output
to multiple destinations, and providing detailed result information.
This README documents the HEAD version of process_executer which may contain
unreleased information. To see the README for the version you are using, consult
RubyGems.org. Go to the [process_executer page in
RubyGems.org](https://rubygems.org/gems/process_executer), select your version, and
then click the "Documentation" link.
## Table of contents
- [Table of contents](#table-of-contents)
- [Usage](#usage)
- [Key methods](#key-methods)
- [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
- [Encoding](#encoding)
- [Encoding summary](#encoding-summary)
- [Encoding details](#encoding-details)
- [Ruby version support policy](#ruby-version-support-policy)
- [Breaking Changes](#breaking-changes)
- [2.x](#2x)
- [`ProcessExecuter.spawn`](#processexecuterspawn)
- [`ProcessExecuter.run`](#processexecuterrun)
- [`ProcessExecuter::Result`](#processexecuterresult)
- [Other](#other)
- [3.x](#3x)
- [`ProcessExecuter.run`](#processexecuterrun-1)
- [4.x](#4x)
- [`ProcessExecuter.spawn_and_wait`](#processexecuterspawn_and_wait)
- [`ProcessExecuter::Result`](#processexecuterresult-1)
- [`ProcessExecuter.spawn_and_wait_with_options`](#processexecuterspawn_and_wait_with_options)
- [`ProcessExecuter.run_with_options`](#processexecuterrun_with_options)
- [Other](#other-1)
- [Installation](#installation)
- [Contributing](#contributing)
- [Reporting Issues](#reporting-issues)
- [Developing](#developing)
- [Commit message guidelines](#commit-message-guidelines)
- [Pull request guidelines](#pull-request-guidelines)
- [Releasing](#releasing)
- [License](#license)
## Usage
[Full YARD documentation](https://rubydoc.info/gems/process_executer/) for this gem
is hosted on RubyGems.org. Read below for an overview and several examples.
### Key methods
ℹ️ See [the ProcessExecuter module
documentation](https://rubydoc.info/gems/process_executer/ProcessExecuter) for
more details and examples of using the methods described here.
The `ProcessExecuter` module provides extended versions of
[Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) that
block while the command is executing. These methods provide enhanced features such as
timeout handling, more flexible redirection options, logging, error raising, and
output capturing.
The interface of these methods is the same as the standard library
[Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn)
method but with additional options.
These methods are:
| Method | Description |
|--------|-------------|
| `ProcessExecuter.spawn_with_timeout` | Extends [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) to run a command and wait (with timeout) for it to finish |
| `ProcessExecuter.run` | Extends `spawn_with_timeout` with more flexible redirection and other options. |
| `ProcessExecuter.run_with_capture` | Extends `run` with capture of stdout and stderr |
See the `ProcessExecuter::Error` class for the error architecture for this module.
### ProcessExecuter::MonitoredPipe
ℹ️ See [the ProcessExecuter::MonitoredPipe class
documentation](https://rubydoc.info/gems/process_executer/ProcessExecuter/MonitoredPipe)
for more details and examples of using this class.
`ProcessExecuter::MonitoredPipe` was created to expand the output redirection options
for `Process.spawn` and methods derived from it within the `ProcessExecuter` module.
This class's initializer accepts any compatible redirection destination supported by
`Process.spawn` (this is the `value` part of the file redirection option described in
[the File Redirection section of
`Process.spawn`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-File+Redirection+-28File+Descriptor-29).
In addition to the standard redirection destinations, `MonitoredPipe` also
supports these additional types of destinations:
- **Arbitrary writers**
You can redirect subprocess output to any Ruby object that implements the
`#write` method. This is particularly useful for:
- capturing command output in in-memory buffers like `StringIO`,
- sending command output to custom logging objects that do not have a file descriptor, and
- processing with a streaming parser to parse and process command output as the
command runs
- **Multiple destinations**
MonitoredPipe supports duplicating (or "teeing") output to multiple
destinations simultaneously. This is achieved by providing an array in the
format `[:tee, destination1, destination2, ...]`, where each `destination` can
be any value that `MonitoredPipe` itself supports (including another tee or
MonitoredPipe).
### Encoding
#### Encoding summary
The gem's core (`MonitoredPipe`) passes through raw bytes from the subprocess without
attempting to interpret or transcode them. `ProcessExecuter.run_with_capture` allows
text encodings to be specified for the captured stdout and stderr (defaulting to
`UTF-8`). For these outputs, the raw bytes are interpreted as being in that specified
encoding. The original byte sequence is preserved and the resulting captured string
is tagged with the target encoding. No transcoding between different text encodings
(e.g., `Latin-1` to `UTF-8`) is performed.
#### Encoding details
`ProcessExecuter::MonitoredPipe` is encoding agnostic. Bytes pass through this class
from the subprocesses output to the destination object as a stream of unaltered
bytes. No transcoding is applied. Strings written to the destination are tagged for
the ASCII-8BIT (aka BINARY) encoding.
`ProcessExecuter` methods `.spawn_with_timeout`, `.run`, and `.run_with_capture` are
also encoding agnostic except with one exception: the user can specify the assumed
encoding for strings returned from `ResultWithCapture#stdout` and
`ResultWithCapture#stderr`.
As a convenience, the captured output is assumed to be UTF-8 by default:
```ruby
result = ProcessExecuter.run_with_capture('pwd')
result.stdout #=> "/Users/James/projects/process_executer\n"
result.stdout.encoding #=> #<Encoding::UTF-8>
```
You can changed the assumed encoding for the captured stdout and stderr via options
passed to `#run_with_capture`:
```ruby
# Set the assumed encoding for both stdout and stderr
result = ProcessExecuter.run_with_capture('pwd', encoding: Encoding::BINARY)
result.stdout #=> "/Users/James/projects/process_executer\n"
result.stdout.encoding #=> #<Encoding:BINARY (ASCII-8BIT)>
# You can set the assumed encoding separately for stdout and stderr
# Encoding may be different for each
result = ProcessExecuter.run_with_capture('pwd', stdout_encoding: 'BINARY', stderr_encoding: 'UTF-8')
result.stdout.encoding #=> #<Encoding:BINARY (ASCII-8BIT)>
result.stderr.encoding #=> #<Encoding:UTF-8>
```
It is possible that the bytes captured are not valid in the given encoding. The user
will need to check the `#valid_encoding?` method to know for sure.
```ruby
File.binwrite('output.txt', "\xFF\xFE") # little-endian BOM marker is not valid UTF-8
result = ProcessExecuter.run_with_capture('cat output.txt')
result.stdout #=> "\xFF\xFE"
result.stdout.encoding #=> #<Encoding:UTF-8>
result.stdout.valid_encoding? #=> false
```
Encoding options accept any encoding objects returned by `Encoding.list` or their
String equivalent given by `#to_s`:
```ruby
Encoding::UTF_8.to_s #=> 'UTF-8'
```
Changing the assumed encoding DOES NOT cause transcoding. It simply interprets the
bytes captured as the given encoding.
These encoding options ONLY affect the internally captured stdout and stderr for
`ProcessExecuter::run_with_capture`. If you give an `out:` or `err:` option, these
will result in BINARY encoded strings and you will need to handle setting the right
encoding or transcoding after collecting the output.
## Ruby version support policy
This gem will be expected to function correctly on:
- All [non-EOL versions](https://www.ruby-lang.org/en/downloads/branches/) of the MRI
Ruby on Mac, Linux, and Windows
- The latest version of JRuby 9.4+ on Linux
- The latest version of TruffleRuby 24+ on Linux
It is this project's intent to support the latest version of JRuby on Windows once
Process.wait2 and Process.wait work correctly on this platform.
Currently, JRuby on Windows does not capture and report the subprocess status via $?
(or $CHILD_STATUS), Process.wait, or Process.wait2. These values always return `nil`
for the status, preventing this gem from properly detecting command failures and
timeouts.
This repository includes a separate test suite in the `process_spawn_test/` directory
that specifically validates JRuby's subprocess behavior. The [Process Spawn Test
workflow](https://github.com/main-branch/process_executer/actions/workflows/process-spawn-test.yml)
can be run manually to verify the status of this issue.
## Breaking Changes
### 2.x
This major release focused on changes to the interface to make it more understandable.
#### `ProcessExecuter.spawn`
- This method was renamed to `ProcessExecuter.spawn_with_timeout`
- The `:timeout` option was renamed to `:timeout_after`
#### `ProcessExecuter.run`
- The `:timeout` option was renamed to `:timeout_after`
#### `ProcessExecuter::Result`
- The `#timeout` method was renamed to `#timed_out`
#### Other
- Dropped support for Ruby 3.0
### 3.x
#### `ProcessExecuter.run`
- The `:merge` option was removed
This was removed because `Process.spawn` already provides this functionality but in
a different way. To merge, you will need to define a redirection where the source
is an array of the file descriptors you want to merge. For instance:
```Ruby
[:out, :err] => 'output.txt'
```
will merge stdout and stderr from the subprocess into the file output.txt.
- Stdout and stderr redirections no longer default to new instances of `StringIO`
Calls to `ProcessExecuter.run` that do not define a redirection for stdout or
stderr will have to add explicit redirection(s) in order to capture the output.
This is to align with the functionality in `Process.spawn`. In `Process.spawn`, when
an explicit redirection is not given for stdout and stderr, this output will be
passed through to the parent process's stdout and stderr.
### 4.x
#### `ProcessExecuter.spawn_and_wait`
`ProcessExecuter.spawn_and_wait` has been renamed to `ProcessExecuter.spawn_with_timeout`.
#### `ProcessExecuter::Result`
`Result#stdout` and `Result#stderr` were removed. Users depending on these methods
will either have to capture this output themselves or change from using
`.spawn_and_wait`/`.run` to `.run_with_capture` which returns a `ResultWithCapture`
object.
#### `ProcessExecuter.spawn_and_wait_with_options`
`ProcessExecuter.spawn_and_wait_with_options` has been removed. Instead call
`ProcessExecuter.spawn_with_timeout` which is overloaded to take the same method
arguments.
#### `ProcessExecuter.run_with_options`
`ProcessExecuter.run_with_options` has been removed. Instead call
`ProcessExecuter.run` which is overloaded to take the same method arguments.
#### Other
In places where users of this gem rescued `::ArgumentError`, they will have to change
the rescued class to `ProcessExecuter::ArgumentError`.
## Installation
Install the gem and add to the application's Gemfile by executing:
```shell
bundle add process_executer
```
If bundler is not being used to manage dependencies, install the gem by executing:
```shell
gem install process_executer
```
## Contributing
### Reporting Issues
Bug reports and other support requests are welcome on [this project's
GitHub issue tracker](https://github.com/main-branch/process_executer)
### Developing
Clone the repo, run `bin/setup` to install dependencies, and then run `rake spec` to
run the tests. You can also run `bin/console` for an interactive prompt that will
allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`.
### Commit message guidelines
All commit messages must follow the [Conventional Commits
standard](https://www.conventionalcommits.org/en/v1.0.0/). This helps us maintain a
clear and structured commit history, automate versioning, and generate changelogs
effectively.
To ensure compliance, this project includes:
- A git commit-msg hook that validates your commit messages before they are accepted.
To activate the hook, you must have Node.js installed and run `npm install`.
- A GitHub Actions workflow that will enforce the Conventional Commit standard as
part of the continuous integration pipeline.
Any commit message that does not conform to the Conventional Commits standard will
cause the workflow to fail and not allow the PR to be merged.
### Pull request guidelines
All pull requests must be merged using rebase merges. This ensures that commit
messages from the feature branch are preserved in the release branch, keeping the
history clean and meaningful.
### Releasing
In the root directory of this project with the `main` branch checked out, run
the following command:
```shell
create-github-release {major|minor|patch}
```
Follow the directions given by the `create-github-release` to publish the new version
of the gem.
## License
The gem is available as open source under the terms of the [MIT
License](https://opensource.org/licenses/MIT).
|