backspin 0.14.0 → 0.15.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.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +2 -1
- data/CONTRIBUTING.md +9 -13
- data/Gemfile.lock +1 -1
- data/README.md +34 -7
- data/fixtures/projects/dummy_cli_gem/Gemfile.lock +2 -2
- data/lib/backspin/snapshot.rb +15 -11
- data/lib/backspin/version.rb +1 -1
- data/lib/backspin.rb +29 -15
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34e60a632038ea59c7a0e95c64068032a5d3cf79584c99d622e36c6f5c610ec7
|
|
4
|
+
data.tar.gz: 49c6fc2e2902dc796072c6032c96862abf2111ad1220bd327ecc615ddb50f388
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de757488de5502e56a41e9910d7e118e93bcb1e77ef93c79014d048d59c5ac83b01644dd67c5a61a10996aa5d3ee3f372f957672cafecddcf7af9e6a4c329843
|
|
7
|
+
data.tar.gz: 805494ee3a1e0e40de7f3b1b05373d73d067798e0aed698989e5662d80ed6ef1d3b0af6a24bb6a3f1f3bc3a3affc609867c4b6daffe62c7291ee881b59499803
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
* Added `stdin:` to `Backspin.run` and `Backspin.compare` to pipe a String into the command, replacing `sh -c` wrappers. For `compare`, give a side as `{command:, stdin:}`. Stdin is saved on every snapshot like stdout and stderr (empty when not given), so re-recorded files gain a `stdin` line, but it is not compared.
|
|
5
|
+
* Changed `Backspin.compare` to skip credential scrubbing, so filters, matchers, and diffs see raw output and scrubbing can no longer hide mismatches (generic patterns could turn different paths into identical star runs). `run` and `capture` still scrub.
|
|
6
|
+
* Added `env:` to `Backspin.compare` side Hashes (`{command:, env:, stdin:}`), so each command can get its own environment. A side's env replaces the top-level `env:` for that side, and each snapshot records the env it ran with.
|
|
7
|
+
|
|
3
8
|
## 0.14.0
|
|
4
9
|
* Improved verification errors with per-field summaries for stdout, stderr, and exit status, plus three lines of context around output changes. Long diffs are limited to 50 lines; set `BACKSPIN_FULL_DIFF=1` to show the full diff.
|
|
5
10
|
* Preserved custom matcher failure reasons and made missing final newlines visible in output diffs.
|
data/CLAUDE.md
CHANGED
|
@@ -27,7 +27,8 @@ bin/rake install # Install gem locally for testing
|
|
|
27
27
|
bin/rake build # Build the gem locally
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Use the [shared release guide](https://github.com/rsanheim/.github#readme) and
|
|
31
|
+
[Backspin quick reference](CONTRIBUTING.md#release-maintainer-setup) for releases.
|
|
31
32
|
|
|
32
33
|
### Code Quality
|
|
33
34
|
```bash
|
data/CONTRIBUTING.md
CHANGED
|
@@ -164,22 +164,18 @@ end
|
|
|
164
164
|
|
|
165
165
|
## Release Maintainer Setup
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Setup, trusted publishing, workflow updates, verification, and recovery live in
|
|
168
|
+
[the shared release guide](https://github.com/rsanheim/.github#readme).
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
- Repository name: `backspin`
|
|
171
|
-
- Workflow filename: `gem-release.yml`
|
|
172
|
-
- Workflow Repository Owner: `rsanheim`
|
|
173
|
-
- Workflow Repository Name: `.github`
|
|
174
|
-
- Environment: `release`
|
|
170
|
+
Backspin quick reference:
|
|
175
171
|
|
|
176
|
-
|
|
172
|
+
1. Update `lib/backspin/version.rb` and add matching notes to `CHANGELOG.md`.
|
|
173
|
+
2. Synchronize Backspin's version in `Gemfile.lock` and
|
|
174
|
+
`fixtures/projects/dummy_cli_gem/Gemfile.lock` (including its checksum entry).
|
|
175
|
+
3. Run the normal checks and `BUNDLE_FROZEN=true bundle exec rake build`.
|
|
176
|
+
4. Merge the reviewed PR to `main` to publish.
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
If publication fails before the gem is uploaded, fix the cause and manually run the Release workflow on `main`, setting `previous-ref` to the tag before the version increase. Confirm the target version is still unpublished before retrying.
|
|
181
|
-
|
|
182
|
-
The shared release workflow is pinned in `.github/workflows/release.yml`. Update that commit deliberately when adopting shared release changes. Bundler's standard `build`, `install`, and `release` tasks remain available; the automated workflow is the normal publishing path.
|
|
178
|
+
The caller is [`.github/workflows/release.yml`](.github/workflows/release.yml).
|
|
183
179
|
|
|
184
180
|
## Code Style
|
|
185
181
|
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -92,6 +92,26 @@ result.verified? # true when they match
|
|
|
92
92
|
|
|
93
93
|
Only stdout, stderr, and exit status are compared, so the two commands can differ in argv and environment without any normalization. `filter` and `matcher` work exactly as they do for `Backspin.run`, and a mismatch raises `Backspin::VerificationError` unless `raise_on_verification_failure` is disabled.
|
|
94
94
|
|
|
95
|
+
To pipe data into a command's stdin, give that side as a Hash with `command:` and `stdin:`:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Backspin.compare(
|
|
99
|
+
reference: {command: ["jq", "."], stdin: '{"a": 1}'},
|
|
100
|
+
actual: {command: ["my-jq", "."], stdin: '{"a": 1}'}
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A side Hash can also take `env:`, which replaces the top-level `env:` for that side:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
Backspin.compare(
|
|
108
|
+
reference: {command: ["jq", "."], env: {"HOME" => reference_home}},
|
|
109
|
+
actual: {command: ["my-jq", "."], env: {"HOME" => actual_home}}
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`compare` records nothing, so it does not scrub credentials. Filters, matchers, and failure diffs see raw output.
|
|
114
|
+
|
|
95
115
|
If the reference command produces no output at all - usually a sign it failed to start rather than that its output is genuinely empty - `compare` raises `Backspin::ReferenceCommandError` instead of comparing empty to empty.
|
|
96
116
|
|
|
97
117
|
### Recording Modes
|
|
@@ -143,6 +163,7 @@ record_count: 3 # increments on each write
|
|
|
143
163
|
snapshot:
|
|
144
164
|
command_type: "Open3::Capture3"
|
|
145
165
|
args: ["echo", "hello"]
|
|
166
|
+
stdin: ""
|
|
146
167
|
stdout: "hello\n"
|
|
147
168
|
stderr: ""
|
|
148
169
|
status: 0
|
|
@@ -164,6 +185,14 @@ Backspin.run(
|
|
|
164
185
|
|
|
165
186
|
If `env:` is not provided, it is not passed to `Open3.capture3` and is not recorded.
|
|
166
187
|
|
|
188
|
+
### Standard Input
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
Backspin.run(["cat"], name: "with_stdin", stdin: "hello from stdin")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`stdin:` is passed to `Open3.capture3` as `stdin_data:` and saved in the record, but only stdout, stderr, and exit status are verified. `stdin:` is not supported with the block form.
|
|
195
|
+
|
|
167
196
|
### Custom Matchers
|
|
168
197
|
|
|
169
198
|
For cases where full matching isn't suitable, you can override via `matcher:`. **NOTE**: If you provide
|
|
@@ -278,7 +307,7 @@ You can configure Backspin's behavior globally:
|
|
|
278
307
|
Backspin.configure do |config|
|
|
279
308
|
config.raise_on_verification_failure = false # default is true
|
|
280
309
|
config.backspin_dir = "spec/fixtures/cli_records" # default is "fixtures/backspin"
|
|
281
|
-
config.scrub_credentials = false # default is true
|
|
310
|
+
config.scrub_credentials = false # default is true; scrubs run/capture records
|
|
282
311
|
end
|
|
283
312
|
```
|
|
284
313
|
|
|
@@ -331,7 +360,7 @@ end
|
|
|
331
360
|
|
|
332
361
|
If the CLI interaction you are recording contains sensitive data in stdout/stderr, you should be careful to make sure it is not recorded to YAML.
|
|
333
362
|
|
|
334
|
-
|
|
363
|
+
Backspin scrubs credentials from everything it records: by default it tries to scrub common credential patterns from stdout, stderr, args, env, and stdin before `run` and `capture` save them. `compare` records nothing, so it does not scrub. Always review your record files before commiting them to source control.
|
|
335
364
|
|
|
336
365
|
```ruby
|
|
337
366
|
# This will automatically scrub AWS keys, API tokens, passwords, etc.
|
|
@@ -374,11 +403,9 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
374
403
|
|
|
375
404
|
### Releasing
|
|
376
405
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
The release implementation lives in [`rsanheim/.github`](https://github.com/rsanheim/.github) and is pinned to a commit in `.github/workflows/release.yml`. See [the maintainer setup](CONTRIBUTING.md#release-maintainer-setup) before enabling releases.
|
|
406
|
+
See the [shared release guide](https://github.com/rsanheim/.github#readme) for
|
|
407
|
+
setup and operation, and the [Backspin quick reference](CONTRIBUTING.md#release-maintainer-setup)
|
|
408
|
+
for the files to update in a release PR.
|
|
382
409
|
|
|
383
410
|
## Contributing
|
|
384
411
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../../..
|
|
3
3
|
specs:
|
|
4
|
-
backspin (0.
|
|
4
|
+
backspin (0.15.0)
|
|
5
5
|
logger
|
|
6
6
|
|
|
7
7
|
PATH
|
|
@@ -40,7 +40,7 @@ DEPENDENCIES
|
|
|
40
40
|
rspec (~> 3)
|
|
41
41
|
|
|
42
42
|
CHECKSUMS
|
|
43
|
-
backspin (0.
|
|
43
|
+
backspin (0.15.0)
|
|
44
44
|
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
|
|
45
45
|
dummy_cli_gem (0.1.0)
|
|
46
46
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
data/lib/backspin/snapshot.rb
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
module Backspin
|
|
4
4
|
# Represents a single captured execution snapshot.
|
|
5
5
|
class Snapshot
|
|
6
|
-
attr_reader :command_type, :args, :env, :stdout, :stderr, :status, :recorded_at
|
|
6
|
+
attr_reader :command_type, :args, :env, :stdin, :stdout, :stderr, :status, :recorded_at
|
|
7
7
|
|
|
8
|
-
def initialize(command_type:, args:, env: nil, stdout: "", stderr: "", status: 0, recorded_at: nil)
|
|
8
|
+
def initialize(command_type:, args:, env: nil, stdin: "", stdout: "", stderr: "", status: 0, recorded_at: nil, scrub: Backspin.configuration.scrub_credentials)
|
|
9
|
+
@scrub = scrub
|
|
9
10
|
@command_type = command_type
|
|
10
11
|
@args = sanitize_args(args)
|
|
11
12
|
@env = env.nil? ? nil : sanitize_env(env)
|
|
12
|
-
@
|
|
13
|
-
@
|
|
13
|
+
@stdin = scrub_string((stdin || "").dup).freeze
|
|
14
|
+
@stdout = scrub_string((stdout || "").dup).freeze
|
|
15
|
+
@stderr = scrub_string((stderr || "").dup).freeze
|
|
14
16
|
@status = status || 0
|
|
15
17
|
@recorded_at = recorded_at.nil? ? nil : recorded_at.dup.freeze
|
|
16
18
|
@serialized_hash = build_serialized_hash
|
|
@@ -42,6 +44,7 @@ module Backspin
|
|
|
42
44
|
command_type: command_type,
|
|
43
45
|
args: data["args"],
|
|
44
46
|
env: data["env"],
|
|
47
|
+
stdin: data["stdin"],
|
|
45
48
|
stdout: data["stdout"],
|
|
46
49
|
stderr: data["stderr"],
|
|
47
50
|
status: data["status"],
|
|
@@ -55,6 +58,7 @@ module Backspin
|
|
|
55
58
|
data = {
|
|
56
59
|
"command_type" => command_type.name,
|
|
57
60
|
"args" => args,
|
|
61
|
+
"stdin" => stdin,
|
|
58
62
|
"stdout" => stdout,
|
|
59
63
|
"stderr" => stderr,
|
|
60
64
|
"status" => status,
|
|
@@ -64,25 +68,25 @@ module Backspin
|
|
|
64
68
|
deep_freeze(data)
|
|
65
69
|
end
|
|
66
70
|
|
|
67
|
-
def
|
|
68
|
-
|
|
71
|
+
def scrub_string(text)
|
|
72
|
+
@scrub ? Backspin.scrub_text(text) : text
|
|
73
|
+
end
|
|
69
74
|
|
|
75
|
+
def scrub_args(value)
|
|
70
76
|
case value
|
|
71
77
|
when String
|
|
72
|
-
|
|
78
|
+
scrub_string(value)
|
|
73
79
|
when Array
|
|
74
80
|
value.map { |entry| scrub_args(entry) }
|
|
75
81
|
when Hash
|
|
76
|
-
value.transform_values { |entry| entry.is_a?(String) ?
|
|
82
|
+
value.transform_values { |entry| entry.is_a?(String) ? scrub_string(entry) : entry }
|
|
77
83
|
else
|
|
78
84
|
value
|
|
79
85
|
end
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
def scrub_env(value)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
value.transform_values { |entry| entry.is_a?(String) ? Backspin.scrub_text(entry) : entry }
|
|
89
|
+
value.transform_values { |entry| entry.is_a?(String) ? scrub_string(entry) : entry }
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
def sanitize_args(value)
|
data/lib/backspin/version.rb
CHANGED
data/lib/backspin.rb
CHANGED
|
@@ -76,17 +76,19 @@ module Backspin
|
|
|
76
76
|
# @param command [String, Array] Command to execute via Open3.capture3
|
|
77
77
|
# @param name [String] Name for the record file
|
|
78
78
|
# @param env [Hash] Environment variables to pass to Open3.capture3
|
|
79
|
+
# @param stdin [String] Data written to the command's stdin (recorded, not compared)
|
|
79
80
|
# @param mode [Symbol] Recording mode - :auto, :record, :verify
|
|
80
81
|
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
81
82
|
# @param filter [Proc] Custom filter for recorded data/canonicalization
|
|
82
83
|
# @param filter_on [Symbol] Filter application mode - :both (default), :record
|
|
83
84
|
# @return [BackspinResult] Aggregate result for this run
|
|
84
|
-
def run(command = nil, name:, env: nil, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
|
|
85
|
+
def run(command = nil, name:, env: nil, stdin: nil, mode: :auto, matcher: nil, filter: nil, filter_on: :both, &block)
|
|
85
86
|
validate_filter_on!(filter_on)
|
|
86
87
|
|
|
87
88
|
if block_given?
|
|
88
89
|
raise ArgumentError, "command must be omitted when using a block" unless command.nil?
|
|
89
90
|
raise ArgumentError, "env is not supported when using a block" unless env.nil?
|
|
91
|
+
raise ArgumentError, "stdin is not supported when using a block" unless stdin.nil?
|
|
90
92
|
|
|
91
93
|
return perform_capture(name, mode: mode, matcher: matcher, filter: filter, filter_on: filter_on, &block)
|
|
92
94
|
end
|
|
@@ -97,6 +99,7 @@ module Backspin
|
|
|
97
99
|
command,
|
|
98
100
|
name: name,
|
|
99
101
|
env: env,
|
|
102
|
+
stdin: stdin,
|
|
100
103
|
mode: mode,
|
|
101
104
|
matcher: matcher,
|
|
102
105
|
filter: filter,
|
|
@@ -126,22 +129,31 @@ module Backspin
|
|
|
126
129
|
# Only stdout, stderr, and status are compared, so the two commands may
|
|
127
130
|
# differ in argv and env without any normalization.
|
|
128
131
|
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
132
|
+
# Either side may be a Hash of {command:, env:, stdin:} to give that command
|
|
133
|
+
# its own env or stdin. A side's env replaces the top-level env for that
|
|
134
|
+
# side. Stdin is recorded on each snapshot, not compared.
|
|
135
|
+
#
|
|
136
|
+
# Credentials are not scrubbed, so the filter, matcher, and diff see raw output.
|
|
137
|
+
#
|
|
138
|
+
# @param reference [String, Array, Hash] Command that defines the correct output
|
|
139
|
+
# @param actual [String, Array, Hash] Command under test
|
|
131
140
|
# @param env [Hash] Environment variables passed to both commands
|
|
132
141
|
# @param matcher [Proc, Hash] Custom matcher for verification
|
|
133
142
|
# @param filter [Proc] Custom filter applied to both sides before comparing
|
|
134
143
|
# @return [BackspinResult] expected = reference, actual = command under test
|
|
135
144
|
def compare(reference:, actual:, env: nil, matcher: nil, filter: nil)
|
|
136
|
-
|
|
145
|
+
reference_command, reference_env, reference_stdin = reference.is_a?(Hash) ? reference.values_at(:command, :env, :stdin) : [reference, nil, nil]
|
|
146
|
+
actual_command, actual_env, actual_stdin = actual.is_a?(Hash) ? actual.values_at(:command, :env, :stdin) : [actual, nil, nil]
|
|
147
|
+
reference_env = normalize_env(reference_env || env)
|
|
148
|
+
actual_env = normalize_env(actual_env || env)
|
|
137
149
|
|
|
138
|
-
expected_snapshot, = capture_command_snapshot(
|
|
150
|
+
expected_snapshot, = capture_command_snapshot(reference_command, reference_env, reference_stdin, scrub: false)
|
|
139
151
|
if expected_snapshot.stdout.empty? && expected_snapshot.stderr.empty?
|
|
140
152
|
raise ReferenceCommandError,
|
|
141
153
|
"Reference command produced no output (exit status #{expected_snapshot.status}): #{reference.inspect}"
|
|
142
154
|
end
|
|
143
155
|
|
|
144
|
-
actual_snapshot, = capture_command_snapshot(
|
|
156
|
+
actual_snapshot, = capture_command_snapshot(actual_command, actual_env, actual_stdin, scrub: false)
|
|
145
157
|
|
|
146
158
|
command_diff = CommandDiff.new(
|
|
147
159
|
expected: expected_snapshot,
|
|
@@ -188,7 +200,7 @@ module Backspin
|
|
|
188
200
|
result
|
|
189
201
|
end
|
|
190
202
|
|
|
191
|
-
def perform_command_run(command, name:, env:, mode:, matcher:, filter:, filter_on:)
|
|
203
|
+
def perform_command_run(command, name:, env:, stdin:, mode:, matcher:, filter:, filter_on:)
|
|
192
204
|
record_path = Record.build_record_path(name)
|
|
193
205
|
mode = determine_mode(mode, record_path)
|
|
194
206
|
validate_mode!(mode)
|
|
@@ -199,7 +211,7 @@ module Backspin
|
|
|
199
211
|
|
|
200
212
|
result = case mode
|
|
201
213
|
when :record
|
|
202
|
-
actual_snapshot, output = capture_command_snapshot(command, normalized_env)
|
|
214
|
+
actual_snapshot, output = capture_command_snapshot(command, normalized_env, stdin)
|
|
203
215
|
record.set_snapshot(actual_snapshot)
|
|
204
216
|
record.save(filter: filter)
|
|
205
217
|
BackspinResult.new(
|
|
@@ -217,7 +229,7 @@ module Backspin
|
|
|
217
229
|
raise RecordFormatError, "Invalid record format: expected Open3::Capture3 for run"
|
|
218
230
|
end
|
|
219
231
|
|
|
220
|
-
actual_snapshot, output = capture_command_snapshot(command, normalized_env)
|
|
232
|
+
actual_snapshot, output = capture_command_snapshot(command, normalized_env, stdin)
|
|
221
233
|
command_diff = CommandDiff.new(
|
|
222
234
|
expected: expected_snapshot,
|
|
223
235
|
actual: actual_snapshot,
|
|
@@ -243,16 +255,18 @@ module Backspin
|
|
|
243
255
|
result
|
|
244
256
|
end
|
|
245
257
|
|
|
246
|
-
def capture_command_snapshot(command, env)
|
|
247
|
-
stdout, stderr, status = execute_command(command, env)
|
|
258
|
+
def capture_command_snapshot(command, env, stdin, scrub: configuration.scrub_credentials)
|
|
259
|
+
stdout, stderr, status = execute_command(command, env, stdin)
|
|
248
260
|
snapshot = Snapshot.new(
|
|
249
261
|
command_type: Open3::Capture3,
|
|
250
262
|
args: command,
|
|
251
263
|
env: env,
|
|
264
|
+
stdin: stdin,
|
|
252
265
|
stdout: stdout,
|
|
253
266
|
stderr: stderr,
|
|
254
267
|
status: status.exitstatus,
|
|
255
|
-
recorded_at: Time.now.utc.iso8601
|
|
268
|
+
recorded_at: Time.now.utc.iso8601,
|
|
269
|
+
scrub: scrub
|
|
256
270
|
)
|
|
257
271
|
[snapshot, [stdout, stderr, status]]
|
|
258
272
|
end
|
|
@@ -264,13 +278,13 @@ module Backspin
|
|
|
264
278
|
env.empty? ? nil : env
|
|
265
279
|
end
|
|
266
280
|
|
|
267
|
-
def execute_command(command, env)
|
|
281
|
+
def execute_command(command, env, stdin)
|
|
268
282
|
case command
|
|
269
283
|
when String
|
|
270
|
-
env ? Open3.capture3(env, command) : Open3.capture3(command)
|
|
284
|
+
env ? Open3.capture3(env, command, stdin_data: stdin) : Open3.capture3(command, stdin_data: stdin)
|
|
271
285
|
when Array
|
|
272
286
|
raise ArgumentError, "command array cannot be empty" if command.empty?
|
|
273
|
-
env ? Open3.capture3(env, *command) : Open3.capture3(*command)
|
|
287
|
+
env ? Open3.capture3(env, *command, stdin_data: stdin) : Open3.capture3(*command, stdin_data: stdin)
|
|
274
288
|
else
|
|
275
289
|
raise ArgumentError, "command must be a String or Array"
|
|
276
290
|
end
|