Skip to content

Commit 4b97628

Browse files
authored
Collapse run() overloads behind a generic Execution type (#262)
Reduce the 14+ run() overloads to two (one executable-based, one configuration-based) by making Execution generic over its input, output, and error types. The body closure now takes a single Execution<Input, Output, Error>, and type-conditional extensions expose standardInputWriter, standardOutput, and standardError only for the matching stream types. Companion changes: - Merge ExecutionOutcome into ExecutionResult<ClosureResult, Output, Error>, which now carries the body closure's return value via closureOutput. Now all run() methods return ExecutionResult. ExecutionOutcome remains as an internal helper. - Expose CustomWriteInput and SequenceOutput publicly, along with the .inputWriter and .sequence factories. Callers opt in per stream: input: .inputWriter unlocks execution.standardInputWriter; output: .sequence unlocks execution.standardOutput; error: .sequence unlocks execution.standardError.
1 parent 4bc3fea commit 4b97628

18 files changed

Lines changed: 783 additions & 1073 deletions

README.md

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -63,27 +63,37 @@ print(result.terminationStatus) // e.g. exited(0)
6363
print(result.standardOutput) // e.g. Optional("LICENSE\nPackage.swift\n...")
6464
```
6565

66-
This returns an `ExecutionRecord` containing the process identifier, termination status, and collected standard output and standard error.
66+
This returns an `ExecutionResult` containing the process identifier, termination status, and collected standard output and standard error.
6767

6868

6969
### Run with a Custom Closure
7070

71-
For more control, pass a closure that runs while the child process is active. The closure receives an `Execution` handle and, depending on the variant, streams for standard output, standard error, and a writer for standard input.
71+
For more control, pass a closure that runs while the child process is active. The closure receives a single `Execution` value that you use to send signals, write to standard input, and stream standard output and standard error.
7272

7373
> [!CAUTION]
74-
> All closure arguments,`Execution`, `AsyncBufferSequence`, and `StandardInputWriter`, are valid only for the duration of the closure's execution and must not be escaped.
74+
> The `Execution`, `AsyncBufferSequence`, and `StandardInputWriter` values are valid only for the duration of the closure. Don't let them escape the closure.
7575
76+
You opt into each interactive stream by choosing the matching input or output type:
77+
78+
| To do this... | Pass this... | Then read from... |
79+
| --- | --- | --- |
80+
| Write to standard input from the closure | `input: .inputWriter` | `execution.standardInputWriter` |
81+
| Stream standard output | `output: .sequence` | `execution.standardOutput` |
82+
| Stream standard error | `error: .sequence` | `execution.standardError` |
7683

7784
Stream standard output line by line:
7885

7986
```swift
8087
import Subprocess
8188

82-
let outcome = try await run(
89+
let result = try await run(
8390
.path("/usr/bin/tail"),
84-
arguments: ["-f", "/path/to/nginx.log"]
85-
) { execution, outputSequence in
86-
for try await line in outputSequence.lines() {
91+
arguments: ["-f", "/path/to/nginx.log"],
92+
input: .none,
93+
output: .sequence,
94+
error: .discarded
95+
) { execution in
96+
for try await line in execution.standardOutput.strings() {
8797
if line.contains("500") {
8898
// Oh no, 500 error
8999
}
@@ -94,73 +104,65 @@ let outcome = try await run(
94104
Write to standard input and read from standard output:
95105

96106
```swift
97-
let outcome = try await run(.name("cat")) { execution, inputWriter, outputSequence in
98-
try await inputWriter.write("Hello, Subprocess!\n")
99-
try await inputWriter.finish()
100-
for try await line in outputSequence.lines() {
101-
print(line) // "Hello, Subprocess!"
102-
}
107+
let result = try await run(
108+
.name("cat"),
109+
input: .inputWriter,
110+
output: .sequence,
111+
error: .discarded
112+
) { execution in
113+
async let reading: Void = {
114+
for try await line in execution.standardOutput.strings() {
115+
print(line) // "Hello, Subprocess!"
116+
}
117+
}()
118+
119+
try await execution.standardInputWriter.write("Hello, Subprocess!\n")
120+
try await execution.standardInputWriter.finish()
121+
try await reading
103122
}
104123
```
105124

106-
The closure-based `run` returns an `ExecutionOutcome` containing both the closure's return value and the termination status.
125+
The closure-based `run` returns an `ExecutionResult`. Access the closure's return value with `result.closureOutput`, and the termination status with `result.terminationStatus`.
107126

108-
`Subprocess` provides several closure variants depending on which streams you need:
127+
Because `input`, `output`, and `error` are separate parameters, you can mix streaming and capturing in the same call. For example, stream standard output from the closure while collecting standard error as a string, and return the closure's own value through `closureOutput`:
109128

110-
* Manage the runnning process without streaming
111129
```swift
112-
run(.path("/my/app")) { execution in
113-
...
114-
}
115-
```
116-
117-
* Manage the running process and stream standard output or standard error
118-
```swift
119-
run(.path("/my/app"), error: .discarded) { execution, outputStream in
120-
for try await item in outputStream { ... }
130+
let result = try await run(
131+
.path("/my/app"),
132+
input: .none,
133+
output: .sequence,
134+
error: .string(limit: 4096)
135+
) { execution in
136+
var lineCount = 0
137+
for try await _ in execution.standardOutput.lines() {
138+
lineCount += 1
139+
}
140+
return lineCount
121141
}
122142

123-
run(.path("/my/app"), output: .discarded) { execution, errorStream in
124-
for try await item in errorStream { ... }
125-
}
143+
print(result.closureOutput) // The line count returned from the closure.
144+
print(result.standardError ?? "") // The captured standard error.
126145
```
127146

147+
Stream both standard output and standard error, writing to standard input from the same closure:
128148

129-
* Write to standard input and stream standard output or standard error
130149
```swift
131-
run(.path("/my/app"), output: .discarded) { execution, inputWriter, outputStream in
150+
try await run(
151+
.path("/my/app"),
152+
input: .inputWriter,
153+
output: .sequence,
154+
error: .sequence
155+
) { execution in
132156
try await withThrowingTaskGroup { group in
133-
group.addTask { for try await item in outputStream { ... } }
134157
group.addTask {
135-
_ = try await inputWriter.write("Hello Subprocess")
136-
try await inputWriter.finish()
158+
for try await line in execution.standardOutput.lines() { /* ... */ }
137159
}
138-
try await group.waitForAll()
139-
}
140-
}
141-
142-
143-
run(.path("/my/app"), error: .discarded) { execution, inputWriter, errorStream in
144-
try await withThrowingTaskGroup { group in
145-
group.addTask { for try await item in errorStream { ... } }
146160
group.addTask {
147-
_ = try await inputWriter.write("Hello Subprocess")
148-
try await inputWriter.finish()
161+
for try await line in execution.standardError.lines() { /* ... */ }
149162
}
150-
try await group.waitForAll()
151-
}
152-
}
153-
```
154-
155-
* Write to standard input and stream both standard output and standard error
156-
```swift
157-
run(.path("/my/app")) { execution, inputWriter, outputStream, errorStream in
158-
try await withThrowingTaskGroup { group in
159-
group.addTask { for try await item in outputStream { ... } }
160-
group.addTask { for try await item in errorStream { ... } }
161163
group.addTask {
162-
_ = try await inputWriter.write("Hello Subprocess")
163-
try await inputWriter.finish()
164+
_ = try await execution.standardInputWriter.write("Hello Subprocess")
165+
try await execution.standardInputWriter.finish()
164166
}
165167
try await group.waitForAll()
166168
}
@@ -169,10 +171,10 @@ run(.path("/my/app")) { execution, inputWriter, outputStream, errorStream in
169171

170172
In the closure-based API, output streams are delivered as an `AsyncBufferSequence` — an asynchronous sequence of `Buffer` values. Each `Buffer` provides access to its bytes via `withUnsafeBytes(_:)` or the `bytes` property (a `RawSpan`).
171173

172-
The preferred method to convert `Buffer` to `String` is to read output line by line using `.lines()`. You can optionally specify an encoding and buffering policy:
174+
The preferred way to convert `Buffer` to `String` is to read output line by line using `.lines()`. You can optionally specify an encoding and buffering policy:
173175

174176
```swift
175-
for try await line in outputSequence.lines(
177+
for try await line in execution.standardOutput.lines(
176178
encoding: UTF16.self,
177179
bufferingPolicy: .maxLineLength(1024)
178180
) {
@@ -213,7 +215,6 @@ let result = try await run(config, output: .string(limit: 4096))
213215
```
214216

215217

216-
Use it by setting `.string(_:)` or `.string(_:using:)` for `input`.
217218
### Input and Output Options
218219

219220
By default, `Subprocess`:
@@ -232,6 +233,7 @@ For the collected-result API, you must specify how to capture standard output.
232233
| `.string(_:)` or `.string(_:using:)` | Read from a string with optional encoding |
233234
| `.array(_:)` | Read from a `[UInt8]` array |
234235
| `Span<BitwiseCopyable>` | Read from a span (passed directly as the `input` parameter) |
236+
| `.inputWriter` | Write from the closure via `execution.standardInputWriter` (closure-based `run` only) |
235237
| `.data(_:)` | Read from `Data` (requires `SubprocessFoundation`) |
236238
| `.sequence(_:)` | Read from a `Sequence<Data>` or `AsyncSequence<Data>` (requires `SubprocessFoundation`) |
237239

@@ -245,6 +247,7 @@ For the collected-result API, you must specify how to capture standard output.
245247
| `.currentStandardOutput` or `.currentStandardError` | Write to the parent process's standard output or standard error |
246248
| `.string(limit:)` or `.string(limit:encoding:)` | Collect as `String?` |
247249
| `.bytes(limit:)` | Collect as `[UInt8]` |
250+
| `.sequence` | Stream to the closure via `execution.standardOutput` or `execution.standardError` (closure-based `run` only) |
248251
| `.data(limit:)` | Collect as `Data` (requires `SubprocessFoundation`) |
249252
| `.combinedWithOutput` | Merge standard error into the standard output stream (error parameter only) |
250253

@@ -273,7 +276,7 @@ let serverTask = Task {
273276
.gracefulShutDown(allowedDurationToNextStep: .seconds(5))
274277
]
275278

276-
let outcome = try await run(
279+
let result = try await run(
277280
.name("server"),
278281
platformOptions: platformOptions,
279282
output: .string(limit: 1024)

0 commit comments

Comments
 (0)