Skip to content

Commit 7029994

Browse files
committed
Introudce CombinedErrorOutput
CombinedErrorOutput is a concrete error output type for subprocesses that combines the standard error output with the standard output stream. When `CombinedErrorOutput` is used as the error output for a subprocess, both standard output and standard error from the child process are merged into a single output stream. This is equivalent to using shell redirection like `2>&1`.
1 parent 44be5d5 commit 7029994

File tree

5 files changed

+303
-31
lines changed

5 files changed

+303
-31
lines changed

Sources/Subprocess/API.swift

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
public func run<
3333
Input: InputProtocol,
3434
Output: OutputProtocol,
35-
Error: OutputProtocol
35+
Error: ErrorOutputProtocol
3636
>(
3737
_ executable: Executable,
3838
arguments: Arguments = [],
@@ -74,7 +74,7 @@ public func run<
7474
public func run<
7575
InputElement: BitwiseCopyable,
7676
Output: OutputProtocol,
77-
Error: OutputProtocol
77+
Error: ErrorOutputProtocol
7878
>(
7979
_ executable: Executable,
8080
arguments: Arguments = [],
@@ -117,7 +117,7 @@ public func run<
117117
/// - isolation: the isolation context to run the body closure.
118118
/// - body: The custom execution body to manually control the running process
119119
/// - Returns: an `ExecutableResult` type containing the return value of the closure.
120-
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: OutputProtocol>(
120+
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: ErrorOutputProtocol>(
121121
_ executable: Executable,
122122
arguments: Arguments = [],
123123
environment: Environment = .inherit,
@@ -164,7 +164,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: Out
164164
/// - isolation: the isolation context to run the body closure.
165165
/// - body: The custom execution body to manually control the running process.
166166
/// - Returns: an `ExecutableResult` type containing the return value of the closure.
167-
public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
167+
public func run<Result, Input: InputProtocol, Error: ErrorOutputProtocol>(
168168
_ executable: Executable,
169169
arguments: Arguments = [],
170170
environment: Environment = .inherit,
@@ -257,7 +257,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
257257
/// - isolation: the isolation context to run the body closure.
258258
/// - body: The custom execution body to manually control the running process
259259
/// - Returns: An `ExecutableResult` type containing the return value of the closure.
260-
public func run<Result, Error: OutputProtocol>(
260+
public func run<Result, Error: ErrorOutputProtocol>(
261261
_ executable: Executable,
262262
arguments: Arguments = [],
263263
environment: Environment = .inherit,
@@ -391,7 +391,7 @@ public func run<Result>(
391391
public func run<
392392
InputElement: BitwiseCopyable,
393393
Output: OutputProtocol,
394-
Error: OutputProtocol
394+
Error: ErrorOutputProtocol
395395
>(
396396
_ configuration: Configuration,
397397
input: borrowing Span<InputElement>,
@@ -451,7 +451,7 @@ public func run<
451451
public func run<
452452
Input: InputProtocol,
453453
Output: OutputProtocol,
454-
Error: OutputProtocol
454+
Error: ErrorOutputProtocol
455455
>(
456456
_ configuration: Configuration,
457457
input: Input = .none,
@@ -463,10 +463,13 @@ public func run<
463463
standardOutput: Output.OutputType,
464464
standardError: Error.OutputType
465465
)
466+
let inputPipe = try input.createPipe()
467+
let outputPipe = try output.createPipe()
468+
let errorPipe = try error.createPipe(from: outputPipe)
466469
let result = try await configuration.run(
467-
input: try input.createPipe(),
468-
output: try output.createPipe(),
469-
error: try error.createPipe()
470+
input: inputPipe,
471+
output: outputPipe,
472+
error: errorPipe
470473
) { (execution, inputIO, outputIO, errorIO) -> RunResult in
471474
// Write input, capture output and error in parallel
472475
var inputIOBox: IOChannel? = consume inputIO
@@ -540,18 +543,21 @@ public func run<
540543
/// - body: The custom execution body to manually control the running process
541544
/// - Returns an executableResult type containing the return value
542545
/// of the closure.
543-
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: OutputProtocol>(
546+
public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: ErrorOutputProtocol>(
544547
_ configuration: Configuration,
545548
input: Input = .none,
546549
output: Output = .discarded,
547550
error: Error = .discarded,
548551
isolation: isolated (any Actor)? = #isolation,
549552
body: ((Execution) async throws -> Result)
550553
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
554+
let inputPipe = try input.createPipe()
555+
let outputPipe = try output.createPipe()
556+
let errorPipe = try error.createPipe(from: outputPipe)
551557
return try await configuration.run(
552-
input: try input.createPipe(),
553-
output: try output.createPipe(),
554-
error: try error.createPipe()
558+
input: inputPipe,
559+
output: outputPipe,
560+
error: errorPipe
555561
) { execution, inputIO, outputIO, errorIO in
556562
var inputIOBox: IOChannel? = consume inputIO
557563
return try await withThrowingTaskGroup(
@@ -590,7 +596,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: Out
590596
/// - body: The custom execution body to manually control the running process
591597
/// - Returns an executableResult type containing the return value
592598
/// of the closure.
593-
public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
599+
public func run<Result, Input: InputProtocol, Error: ErrorOutputProtocol>(
594600
_ configuration: Configuration,
595601
input: Input = .none,
596602
error: Error = .discarded,
@@ -599,10 +605,13 @@ public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
599605
body: ((Execution, AsyncBufferSequence) async throws -> Result)
600606
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
601607
let output = SequenceOutput()
608+
let inputPipe = try input.createPipe()
609+
let outputPipe = try output.createPipe()
610+
let errorPipe = try error.createPipe(from: outputPipe)
602611
return try await configuration.run(
603-
input: try input.createPipe(),
604-
output: try output.createPipe(),
605-
error: try error.createPipe()
612+
input: inputPipe,
613+
output: outputPipe,
614+
error: errorPipe
606615
) { execution, inputIO, outputIO, errorIO in
607616
var inputIOBox: IOChannel? = consume inputIO
608617
var outputIOBox: IOChannel? = consume outputIO
@@ -702,7 +711,7 @@ public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
702711
/// - body: The custom execution body to manually control the running process
703712
/// - Returns an executableResult type containing the return value
704713
/// of the closure.
705-
public func run<Result, Error: OutputProtocol>(
714+
public func run<Result, Error: ErrorOutputProtocol>(
706715
_ configuration: Configuration,
707716
error: Error = .discarded,
708717
preferredBufferSize: Int? = nil,
@@ -711,10 +720,13 @@ public func run<Result, Error: OutputProtocol>(
711720
) async throws -> ExecutionResult<Result> where Error.OutputType == Void {
712721
let input = CustomWriteInput()
713722
let output = SequenceOutput()
723+
let inputPipe = try input.createPipe()
724+
let outputPipe = try output.createPipe()
725+
let errorPipe = try error.createPipe(from: outputPipe)
714726
return try await configuration.run(
715-
input: try input.createPipe(),
716-
output: try output.createPipe(),
717-
error: try error.createPipe()
727+
input: inputPipe,
728+
output: outputPipe,
729+
error: errorPipe
718730
) { execution, inputIO, outputIO, errorIO in
719731
let writer = StandardInputWriter(diskIO: inputIO!)
720732
let outputSequence = AsyncBufferSequence(

Sources/Subprocess/Configuration.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,19 @@ internal struct IODescriptor: ~Copyable {
811811
self.closeWhenDone = closeWhenDone
812812
}
813813

814+
internal init?(duplicating ioDescriptor: borrowing IODescriptor?) throws {
815+
let descriptor = try ioDescriptor?.duplicate()
816+
if let descriptor {
817+
self = descriptor
818+
} else {
819+
return nil
820+
}
821+
}
822+
823+
func duplicate() throws -> IODescriptor {
824+
return try IODescriptor(self.descriptor.duplicate(), closeWhenDone: self.closeWhenDone)
825+
}
826+
814827
consuming func createIOChannel() -> IOChannel {
815828
let shouldClose = self.closeWhenDone
816829
self.closeWhenDone = false
@@ -962,6 +975,13 @@ internal struct CreatedPipe: ~Copyable {
962975
return self._writeFileDescriptor.take()
963976
}
964977

978+
internal init(duplicating createdPipe: borrowing CreatedPipe) throws {
979+
self.init(
980+
readFileDescriptor: try IODescriptor(duplicating: createdPipe._readFileDescriptor),
981+
writeFileDescriptor: try IODescriptor(duplicating: createdPipe._writeFileDescriptor)
982+
)
983+
}
984+
965985
internal init(closeWhenDone: Bool, purpose: Purpose) throws {
966986
#if canImport(WinSDK)
967987
/// On Windows, we need to create a named pipe.
@@ -1213,3 +1233,32 @@ extension Set {
12131233
return self.remove(element) != nil
12141234
}
12151235
}
1236+
1237+
#if canImport(WinSDK)
1238+
extension HANDLE {
1239+
func duplicate() throws -> HANDLE {
1240+
var handle: HANDLE? = nil
1241+
guard
1242+
DuplicateHandle(
1243+
GetCurrentProcess(),
1244+
self,
1245+
GetCurrentProcess(),
1246+
&handle,
1247+
0, true, DWORD(DUPLICATE_SAME_ACCESS)
1248+
)
1249+
else {
1250+
throw SubprocessError(
1251+
code: .init(.failedToCreatePipe),
1252+
underlyingError: .init(rawValue: GetLastError())
1253+
)
1254+
}
1255+
guard let handle else {
1256+
throw SubprocessError(
1257+
code: .init(.failedToCreatePipe),
1258+
underlyingError: .init(rawValue: GetLastError())
1259+
)
1260+
}
1261+
return handle
1262+
}
1263+
}
1264+
#endif

Sources/Subprocess/IO/Output.swift

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extension OutputProtocol {
5656
/// On Unix-like systems, `DiscardedOutput` redirects the
5757
/// standard output of the subprocess to `/dev/null`, while on Windows,
5858
/// redirects the output to `NUL`.
59-
public struct DiscardedOutput: OutputProtocol {
59+
public struct DiscardedOutput: OutputProtocol, ErrorOutputProtocol {
6060
/// The type for the output.
6161
public typealias OutputType = Void
6262

@@ -82,7 +82,7 @@ public struct DiscardedOutput: OutputProtocol {
8282
///
8383
/// Developers have the option to instruct the `Subprocess` to automatically
8484
/// close the related `FileDescriptor` after the subprocess is spawned.
85-
public struct FileDescriptorOutput: OutputProtocol {
85+
public struct FileDescriptorOutput: OutputProtocol, ErrorOutputProtocol {
8686
/// The type for this output.
8787
public typealias OutputType = Void
8888

@@ -115,7 +115,7 @@ public struct FileDescriptorOutput: OutputProtocol {
115115

116116
/// A concrete `Output` type for subprocesses that collects output
117117
/// from the subprocess as `String` with the given encoding.
118-
public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol {
118+
public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol, ErrorOutputProtocol {
119119
/// The type for this output.
120120
public typealias OutputType = String?
121121
/// The max number of bytes to collect.
@@ -147,7 +147,7 @@ public struct StringOutput<Encoding: Unicode.Encoding>: OutputProtocol {
147147

148148
/// A concrete `Output` type for subprocesses that collects output from
149149
/// the subprocess as `[UInt8]`.
150-
public struct BytesOutput: OutputProtocol {
150+
public struct BytesOutput: OutputProtocol, ErrorOutputProtocol {
151151
/// The output type for this output option
152152
public typealias OutputType = [UInt8]
153153
/// The max number of bytes to collect
@@ -286,6 +286,57 @@ extension OutputProtocol where Self == BytesOutput {
286286
}
287287
}
288288

289+
// MARK: - ErrorOutputProtocol
290+
291+
/// Error output protocol specifies the set of methods that a type must implement to
292+
/// serve as the error output target for a subprocess.
293+
///
294+
/// Instead of developing custom implementations of `ErrorOutputProtocol`, use the
295+
/// default implementations provided by the `Subprocess` library to specify the
296+
/// output handling requirements.
297+
public protocol ErrorOutputProtocol: OutputProtocol {}
298+
299+
/// A concrete error output type for subprocesses that combines the standard error
300+
/// output with the standard output stream.
301+
///
302+
/// When `CombinedErrorOutput` is used as the error output for a subprocess, both
303+
/// standard output and standard error from the child process are merged into a
304+
/// single output stream. This is equivalent to using shell redirection like `2>&1`.
305+
///
306+
/// This output type is useful when you want to capture or redirect both output
307+
/// streams together, making it easier to process all subprocess output as a unified
308+
/// stream rather than handling standard output and standard error separately.
309+
public struct CombinedErrorOutput: ErrorOutputProtocol {
310+
public typealias OutputType = Void
311+
}
312+
313+
extension ErrorOutputProtocol {
314+
internal func createPipe(from outputPipe: borrowing CreatedPipe) throws -> CreatedPipe {
315+
if self is CombinedErrorOutput {
316+
return try CreatedPipe(duplicating: outputPipe)
317+
}
318+
return try createPipe()
319+
}
320+
}
321+
322+
extension ErrorOutputProtocol where Self == CombinedErrorOutput {
323+
/// Creates an error output that combines standard error with standard output.
324+
///
325+
/// When using `combineWithOutput`, both standard output and standard error from
326+
/// the child process are merged into a single output stream. This is equivalent
327+
/// to using shell redirection like `2>&1`.
328+
///
329+
/// This is useful when you want to capture or redirect both output streams
330+
/// together, making it easier to process all subprocess output as a unified
331+
/// stream rather than handling standard output and standard error separately
332+
///
333+
/// - Returns: A `CombinedErrorOutput` instance that merges standard error
334+
/// with standard output.
335+
public static var combineWithOutput: Self {
336+
return CombinedErrorOutput()
337+
}
338+
}
339+
289340
// MARK: - Span Default Implementations
290341
#if SubprocessSpan
291342
extension OutputProtocol {

Sources/Subprocess/SubprocessFoundation/Output+Foundation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import FoundationEssentials
2121

2222
/// A concrete `Output` type for subprocesses that collects output
2323
/// from the subprocess as data.
24-
public struct DataOutput: OutputProtocol {
24+
public struct DataOutput: OutputProtocol, ErrorOutputProtocol {
2525
/// The output type for this output option
2626
public typealias OutputType = Data
2727
/// The maximum number of bytes to collect.

0 commit comments

Comments
 (0)