You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: README.md
+63-60Lines changed: 63 additions & 60 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -63,27 +63,37 @@ print(result.terminationStatus) // e.g. exited(0)
63
63
print(result.standardOutput) // e.g. Optional("LICENSE\nPackage.swift\n...")
64
64
```
65
65
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.
67
67
68
68
69
69
### Run with a Custom Closure
70
70
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.
72
72
73
73
> [!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.
75
75
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`|
76
83
77
84
Stream standard output line by line:
78
85
79
86
```swift
80
87
importSubprocess
81
88
82
-
letoutcome=tryawaitrun(
89
+
letresult=tryawaitrun(
83
90
.path("/usr/bin/tail"),
84
-
arguments: ["-f", "/path/to/nginx.log"]
85
-
) { execution, outputSequence in
86
-
fortryawait line in outputSequence.lines() {
91
+
arguments: ["-f", "/path/to/nginx.log"],
92
+
input: .none,
93
+
output: .sequence,
94
+
error: .discarded
95
+
) { execution in
96
+
fortryawait line in execution.standardOutput.strings() {
87
97
if line.contains("500") {
88
98
// Oh no, 500 error
89
99
}
@@ -94,73 +104,65 @@ let outcome = try await run(
94
104
Write to standard input and read from standard output:
95
105
96
106
```swift
97
-
let outcome =tryawaitrun(.name("cat")) { execution, inputWriter, outputSequence in
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`.
107
126
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`:
109
128
110
-
* Manage the runnning process without streaming
111
129
```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
-
fortryawait item in outputStream { ... }
130
+
let result =tryawaitrun(
131
+
.path("/my/app"),
132
+
input: .none,
133
+
output: .sequence,
134
+
error: .string(limit: 4096)
135
+
) { execution in
136
+
var lineCount =0
137
+
fortryawait_in execution.standardOutput.lines() {
138
+
lineCount +=1
139
+
}
140
+
return lineCount
121
141
}
122
142
123
-
run(.path("/my/app"), output: .discarded) { execution, errorStream in
124
-
fortryawait item in errorStream { ... }
125
-
}
143
+
print(result.closureOutput) // The line count returned from the closure.
144
+
print(result.standardError??"") // The captured standard error.
126
145
```
127
146
147
+
Stream both standard output and standard error, writing to standard input from the same closure:
128
148
129
-
* Write to standard input and stream standard output or standard error
130
149
```swift
131
-
run(.path("/my/app"), output: .discarded) { execution, inputWriter, outputStream in
150
+
tryawaitrun(
151
+
.path("/my/app"),
152
+
input: .inputWriter,
153
+
output: .sequence,
154
+
error: .sequence
155
+
) { execution in
132
156
tryawaitwithThrowingTaskGroup { group in
133
-
group.addTask { fortryawait item in outputStream { ... } }
134
157
group.addTask {
135
-
_=tryawait inputWriter.write("Hello Subprocess")
136
-
tryawait inputWriter.finish()
158
+
fortryawait line in execution.standardOutput.lines() { /* ... */ }
137
159
}
138
-
tryawait group.waitForAll()
139
-
}
140
-
}
141
-
142
-
143
-
run(.path("/my/app"), error: .discarded) { execution, inputWriter, errorStream in
144
-
tryawaitwithThrowingTaskGroup { group in
145
-
group.addTask { fortryawait item in errorStream { ... } }
146
160
group.addTask {
147
-
_=tryawait inputWriter.write("Hello Subprocess")
148
-
tryawait inputWriter.finish()
161
+
fortryawait line in execution.standardError.lines() { /* ... */ }
149
162
}
150
-
tryawait 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
-
tryawaitwithThrowingTaskGroup { group in
159
-
group.addTask { fortryawait item in outputStream { ... } }
160
-
group.addTask { fortryawait item in errorStream { ... } }
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`).
171
173
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:
173
175
174
176
```swift
175
-
fortryawait line inoutputSequence.lines(
177
+
fortryawait line inexecution.standardOutput.lines(
176
178
encoding: UTF16.self,
177
179
bufferingPolicy: .maxLineLength(1024)
178
180
) {
@@ -213,7 +215,6 @@ let result = try await run(config, output: .string(limit: 4096))
213
215
```
214
216
215
217
216
-
Use it by setting `.string(_:)` or `.string(_:using:)` for `input`.
217
218
### Input and Output Options
218
219
219
220
By default, `Subprocess`:
@@ -232,6 +233,7 @@ For the collected-result API, you must specify how to capture standard output.
232
233
|`.string(_:)` or `.string(_:using:)`| Read from a string with optional encoding |
233
234
|`.array(_:)`| Read from a `[UInt8]` array |
234
235
|`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) |
235
237
|`.data(_:)`| Read from `Data` (requires `SubprocessFoundation`) |
236
238
|`.sequence(_:)`| Read from a `Sequence<Data>` or `AsyncSequence<Data>` (requires `SubprocessFoundation`) |
237
239
@@ -245,6 +247,7 @@ For the collected-result API, you must specify how to capture standard output.
245
247
|`.currentStandardOutput` or `.currentStandardError`| Write to the parent process's standard output or standard error |
246
248
|`.string(limit:)` or `.string(limit:encoding:)`| Collect as `String?`|
247
249
|`.bytes(limit:)`| Collect as `[UInt8]`|
250
+
|`.sequence`| Stream to the closure via `execution.standardOutput` or `execution.standardError` (closure-based `run` only) |
248
251
|`.data(limit:)`| Collect as `Data` (requires `SubprocessFoundation`) |
249
252
|`.combinedWithOutput`| Merge standard error into the standard output stream (error parameter only) |
0 commit comments