Skip to content

Commit 651152d

Browse files
committed
Add support for redirecting stderr to stdout.
1 parent cffd634 commit 651152d

File tree

3 files changed

+113
-24
lines changed

3 files changed

+113
-24
lines changed

Diff for: Sources/TSCBasic/Process.swift

+57-22
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,20 @@ public final class Process: ObjectIdentifierProtocol {
129129
public enum OutputRedirection {
130130
/// Do not redirect the output
131131
case none
132-
/// Collect stdout and stderr output and provide it back via ProcessResult object
133-
case collect
134-
/// Stream stdout and stderr via the corresponding closures
135-
case stream(stdout: OutputClosure, stderr: OutputClosure)
132+
/// Collect stdout and stderr output and provide it back via ProcessResult object. If redirectStderr is true,
133+
/// stderr be redirected to stdout.
134+
case collect(redirectStderr: Bool)
135+
/// Stream stdout and stderr via the corresponding closures. If redirectStderr is true, stderr be redirected to
136+
/// stdout.
137+
case stream(stdout: OutputClosure, stderr: OutputClosure, redirectStderr: Bool)
138+
139+
/// Default collect OutputRedirection that defaults to not redirect stderr. Provided for API compatibility.
140+
public static let collect: OutputRedirection = .collect(redirectStderr: false)
141+
142+
/// Default stream OutputRedirection that defaults to not redirect stderr. Provided for API compatibility.
143+
public static func stream(stdout: @escaping OutputClosure, stderr: @escaping OutputClosure) -> Self {
144+
return .stream(stdout: stdout, stderr: stderr, redirectStderr: false)
145+
}
136146

137147
public var redirectsOutput: Bool {
138148
switch self {
@@ -145,12 +155,23 @@ public final class Process: ObjectIdentifierProtocol {
145155

146156
public var outputClosures: (stdoutClosure: OutputClosure, stderrClosure: OutputClosure)? {
147157
switch self {
148-
case .stream(let stdoutClosure, let stderrClosure):
158+
case let .stream(stdoutClosure, stderrClosure, _):
149159
return (stdoutClosure: stdoutClosure, stderrClosure: stderrClosure)
150160
case .collect, .none:
151161
return nil
152162
}
153163
}
164+
165+
public var redirectStderr: Bool {
166+
switch self {
167+
case let .collect(redirectStderr):
168+
return redirectStderr
169+
case let .stream(_, _, redirectStderr):
170+
return redirectStderr
171+
default:
172+
return false
173+
}
174+
}
154175
}
155176

156177
/// Typealias for process id type.
@@ -433,19 +454,30 @@ public final class Process: ObjectIdentifierProtocol {
433454
// Open /dev/null as stdin.
434455
posix_spawn_file_actions_addopen(&fileActions, 0, devNull, O_RDONLY, 0)
435456

436-
var outputPipe: [Int32] = [0, 0]
437-
var stderrPipe: [Int32] = [0, 0]
457+
var outputPipe: [Int32] = [-1, -1]
458+
var stderrPipe: [Int32] = [-1, -1]
438459
if outputRedirection.redirectsOutput {
439-
// Open the pipes.
460+
// Open the pipe.
440461
try open(pipe: &outputPipe)
441-
try open(pipe: &stderrPipe)
442-
// Open the write end of the pipe as stdout and stderr, if desired.
462+
463+
// Open the write end of the pipe.
443464
posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 1)
444-
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
465+
445466
// Close the other ends of the pipe.
446-
for pipe in [outputPipe, stderrPipe] {
447-
posix_spawn_file_actions_addclose(&fileActions, pipe[0])
448-
posix_spawn_file_actions_addclose(&fileActions, pipe[1])
467+
posix_spawn_file_actions_addclose(&fileActions, outputPipe[0])
468+
posix_spawn_file_actions_addclose(&fileActions, outputPipe[1])
469+
470+
if outputRedirection.redirectStderr {
471+
// If merged was requested, send stderr to stdout.
472+
posix_spawn_file_actions_adddup2(&fileActions, 1, 2)
473+
} else {
474+
// If no redirect was requested, open the pipe for stderr.
475+
try open(pipe: &stderrPipe)
476+
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], 2)
477+
478+
// Close the other ends of the pipe.
479+
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
480+
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
449481
}
450482
} else {
451483
posix_spawn_file_actions_adddup2(&fileActions, 1, 1)
@@ -475,17 +507,20 @@ public final class Process: ObjectIdentifierProtocol {
475507
thread.start()
476508
self.stdout.thread = thread
477509

478-
// Close the write end of the stderr pipe.
479-
try close(fd: &stderrPipe[1])
510+
// Only schedule a thread for stderr if no redirect was requested.
511+
if !outputRedirection.redirectStderr {
512+
// Close the write end of the stderr pipe.
513+
try close(fd: &stderrPipe[1])
480514

481-
// Create a thread and start reading the stderr output on it.
482-
thread = Thread { [weak self] in
483-
if let readResult = self?.readOutput(onFD: stderrPipe[0], outputClosure: outputClosures?.stderrClosure) {
484-
self?.stderr.result = readResult
515+
// Create a thread and start reading the stderr output on it.
516+
thread = Thread { [weak self] in
517+
if let readResult = self?.readOutput(onFD: stderrPipe[0], outputClosure: outputClosures?.stderrClosure) {
518+
self?.stderr.result = readResult
519+
}
485520
}
521+
thread.start()
522+
self.stderr.thread = thread
486523
}
487-
thread.start()
488-
self.stderr.thread = thread
489524
}
490525
#endif // POSIX implementation
491526
}

Diff for: Tests/TSCBasicTests/ProcessTests.swift

+55-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class ProcessTests: XCTestCase {
191191
do {
192192
let result = try Process.popen(args: script("simple-stdout-stderr"))
193193
XCTAssertEqual(try result.utf8Output(), "simple output\n")
194-
XCTAssertEqual(try result.utf8stderrOutput(), "simple error")
194+
XCTAssertEqual(try result.utf8stderrOutput(), "simple error\n")
195195
}
196196

197197
// A long stdout and stderr output.
@@ -211,6 +211,60 @@ class ProcessTests: XCTestCase {
211211
}
212212
}
213213

214+
func testStdoutStdErrRedirected() throws {
215+
// A simple script to check that stdout and stderr are captured in the same location.
216+
do {
217+
let process = Process(args: script("simple-stdout-stderr"), outputRedirection: .collect(redirectStderr: true))
218+
try process.launch()
219+
let result = try process.waitUntilExit()
220+
XCTAssertEqual(try result.utf8Output(), "simple error\nsimple output\n")
221+
XCTAssertEqual(try result.utf8stderrOutput(), "")
222+
}
223+
224+
// A long stdout and stderr output.
225+
do {
226+
let process = Process(args: script("long-stdout-stderr"), outputRedirection: .collect(redirectStderr: true))
227+
try process.launch()
228+
let result = try process.waitUntilExit()
229+
230+
let count = 16 * 1024
231+
XCTAssertEqual(try result.utf8Output(), String(repeating: "12", count: count))
232+
XCTAssertEqual(try result.utf8stderrOutput(), "")
233+
}
234+
}
235+
236+
func testStdoutStdErrStreaming() throws {
237+
var stdout = [UInt8]()
238+
var stderr = [UInt8]()
239+
let process = Process(args: script("long-stdout-stderr"), outputRedirection: .stream(stdout: { stdoutBytes in
240+
stdout += stdoutBytes
241+
}, stderr: { stderrBytes in
242+
stderr += stderrBytes
243+
}))
244+
try process.launch()
245+
try process.waitUntilExit()
246+
247+
let count = 16 * 1024
248+
XCTAssertEqual(String(bytes: stdout, encoding: .utf8), String(repeating: "1", count: count))
249+
XCTAssertEqual(String(bytes: stderr, encoding: .utf8), String(repeating: "2", count: count))
250+
}
251+
252+
func testStdoutStdErrStreamingRedirected() throws {
253+
var stdout = [UInt8]()
254+
var stderr = [UInt8]()
255+
let process = Process(args: script("long-stdout-stderr"), outputRedirection: .stream(stdout: { stdoutBytes in
256+
stdout += stdoutBytes
257+
}, stderr: { stderrBytes in
258+
stderr += stderrBytes
259+
}, redirectStderr: true))
260+
try process.launch()
261+
try process.waitUntilExit()
262+
263+
let count = 16 * 1024
264+
XCTAssertEqual(String(bytes: stdout, encoding: .utf8), String(repeating: "12", count: count))
265+
XCTAssertEqual(stderr, [])
266+
}
267+
214268
func testWorkingDirectory() throws {
215269
guard #available(macOS 10.15, *) else {
216270
// Skip this test since it's not supported in this OS.

Diff for: Tests/TSCBasicTests/processInputs/simple-stdout-stderr

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import sys
44

5-
sys.stderr.write("simple error")
5+
sys.stderr.write("simple error\n")
66
sys.stderr.flush()
77
sys.stdout.write("simple output\n")
88
sys.stdout.flush()

0 commit comments

Comments
 (0)