Subprocess is a cross-platform Swift package for spawning child processes, built from the ground up with Swift concurrency.
To use Subprocess in a SwiftPM project, add it as a package dependency in your Package.swift:
dependencies: [
.package(
url: "https://github.com/swiftlang/swift-subprocess.git",
.upToNextMinor(from: "0.4.0")
)
]Then add the Subprocess module to your target dependencies:
.target(
name: "MyTarget",
dependencies: [
.product(name: "Subprocess", package: "swift-subprocess")
]
)Subprocess offers one package trait, SubprocessFoundation, which adds a dependency on Foundation and provides extensions on Foundation types like Data. This trait is enabled by default.
We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, new versions of this package require clients to upgrade to a more recent Swift toolchain release. Patch (i.e., bugfix) releases will not increase the required toolchain version, but any minor (i.e., new feature) release may do so.
The following table maps package releases to their minimum required Swift toolchain:
| Package version | Swift version | Xcode release |
|---|---|---|
| swift-subprocess 0.1.x | >= Swift 6.1 | >= Xcode 16.3 |
| swift-subprocess 0.2.x | >= Swift 6.1 | >= Xcode 16.3 |
| swift-subprocess 0.3.x | >= Swift 6.1 | >= Xcode 16.3 |
| swift-subprocess 0.4.x | >= Swift 6.1 | >= Xcode 16.3 |
| main | >= Swift 6.2 | >= Xcode 26 |
The simplest way to use Subprocess is to run a process and collect its output:
import Subprocess
let result = try await run(.name("ls"), output: .string(limit: 4096))
print(result.processIdentifier) // e.g. 1234
print(result.terminationStatus) // e.g. exited(0)
print(result.standardOutput) // e.g. Optional("LICENSE\nPackage.swift\n...")This returns an ExecutionResult containing the process identifier, termination status, and collected standard output and standard error.
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.
Caution
The Execution, AsyncBufferSequence, and StandardInputWriter values are valid only for the duration of the closure. Don't let them escape the closure.
You opt into each interactive stream by choosing the matching input or output type:
| To do this... | Pass this... | Then read from... |
|---|---|---|
| Write to standard input from the closure | input: .inputWriter |
execution.standardInputWriter |
| Stream standard output | output: .sequence |
execution.standardOutput |
| Stream standard error | error: .sequence |
execution.standardError |
Stream standard output line by line:
import Subprocess
let result = try await run(
.path("/usr/bin/tail"),
arguments: ["-f", "/path/to/nginx.log"],
input: .none,
output: .sequence,
error: .discarded
) { execution in
for try await line in execution.standardOutput.strings() {
if line.contains("500") {
// Oh no, 500 error
}
}
}Write to standard input and read from standard output:
let result = try await run(
.name("cat"),
input: .inputWriter,
output: .sequence,
error: .discarded
) { execution in
async let reading: Void = {
for try await line in execution.standardOutput.strings() {
print(line) // "Hello, Subprocess!"
}
}()
try await execution.standardInputWriter.write("Hello, Subprocess!\n")
try await execution.standardInputWriter.finish()
try await reading
}The closure-based run returns an ExecutionResult. Access the closure's return value with result.closureOutput, and the termination status with result.terminationStatus.
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:
let result = try await run(
.path("/my/app"),
input: .none,
output: .sequence,
error: .string(limit: 4096)
) { execution in
var lineCount = 0
for try await _ in execution.standardOutput.lines() {
lineCount += 1
}
return lineCount
}
print(result.closureOutput) // The line count returned from the closure.
print(result.standardError ?? "") // The captured standard error.Stream both standard output and standard error, writing to standard input from the same closure:
try await run(
.path("/my/app"),
input: .inputWriter,
output: .sequence,
error: .sequence
) { execution in
try await withThrowingTaskGroup { group in
group.addTask {
for try await line in execution.standardOutput.lines() { /* ... */ }
}
group.addTask {
for try await line in execution.standardError.lines() { /* ... */ }
}
group.addTask {
_ = try await execution.standardInputWriter.write("Hello Subprocess")
try await execution.standardInputWriter.finish()
}
try await group.waitForAll()
}
}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).
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:
for try await line in execution.standardOutput.lines(
encoding: UTF16.self,
bufferingPolicy: .maxLineLength(1024)
) {
// ...
}StandardInputWriter supports writing [UInt8], String, RawSpan, and (with the SubprocessFoundation trait) Data. Call finish() when done writing.
Configure arguments, environment variables, and the working directory:
import Subprocess
let result = try await run(
.path("/bin/ls"),
arguments: ["-a"],
// Inherit environment values from the parent process
// and add NewKey=NewValue
environment: .inherit.updating(["NewKey": "NewValue"]),
workingDirectory: "/Users/",
output: .string(limit: 4096)
)For reusable configurations, construct a Configuration value directly:
let config = Configuration(
.name("my-tool"),
arguments: ["--verbose"],
environment: .inherit
)
let result = try await run(config, output: .string(limit: 4096))By default, Subprocess:
- Provides no input to the child process
- Discards the child process's standard error
For the collected-result API, you must specify how to capture standard output.
Input options:
| Usage | Description |
|---|---|
.none |
No input (default) |
.fileDescriptor(_:closeAfterSpawningProcess:) |
Read from a file descriptor |
.standardInput |
Read from the parent process's standard input |
.string(_:) or .string(_:using:) |
Read from a string with optional encoding |
.array(_:) |
Read from a [UInt8] array |
Span<BitwiseCopyable> |
Read from a span (passed directly as the input parameter) |
.inputWriter |
Write from the closure via execution.standardInputWriter (closure-based run only) |
.data(_:) |
Read from Data (requires SubprocessFoundation) |
.sequence(_:) |
Read from a Sequence<Data> or AsyncSequence<Data> (requires SubprocessFoundation) |
Output options:
| Usage | Description |
|---|---|
.discarded |
Discard output |
.fileDescriptor(_:closeAfterSpawningProcess:) |
Write to a file descriptor |
.currentStandardOutput or .currentStandardError |
Write to the parent process's standard output or standard error |
.string(limit:) or .string(limit:encoding:) |
Collect as String? |
.bytes(limit:) |
Collect as [UInt8] |
.sequence |
Stream to the closure via execution.standardOutput or execution.standardError (closure-based run only) |
.data(limit:) |
Collect as Data (requires SubprocessFoundation) |
.combinedWithOutput |
Merge standard error into the standard output stream (error parameter only) |
The limit parameter specifies the maximum number of bytes to collect. Subprocess throws an error if the child process produces more output than the limit allows.
Use .combinedWithOutput for the error parameter to merge standard output and standard error into a single stream, equivalent to the shell redirection 2>&1:
let result = try await run(
.name("my-tool"),
output: .string(limit: 4096),
error: .combinedWithOutput
)
// result.standardOutput contains both standard output and standard errorWhen a parent task is cancelled, Subprocess can perform a configurable teardown sequence before forcefully terminating the child process. Set this up via PlatformOptions.teardownSequence:
let serverTask = Task {
var platformOptions = PlatformOptions()
platformOptions.teardownSequence = [
.gracefulShutDown(allowedDurationToNextStep: .seconds(5))
]
let result = try await run(
.name("server"),
platformOptions: platformOptions,
output: .string(limit: 1024)
)
}
// If serverTask is cancelled, Subprocess will:
// 1. Attempt a graceful shutdown (SIGTERM on Unix)
// 2. Wait up to 5 seconds for the process to exit
// 3. Send SIGKILL if the process hasn't exited
serverTask.cancel()On Unix, you can also send specific signals as teardown steps:
platformOptions.teardownSequence = [
.send(signal: .interrupt, allowedDurationToNextStep: .seconds(2)),
.gracefulShutDown(allowedDurationToNextStep: .seconds(5))
]The teardown sequence always concludes by sending a kill signal.
You can also trigger a teardown manually from within the closure via execution.teardown(using:), or send individual signals on Unix with execution.send(signal:).
PlatformOptions provides platform-specific settings for the child process:
- Unix:
userID,groupID,supplementaryGroups,processGroupID,createSession - macOS: All Unix options, plus
qualityOfServiceandpreSpawnProcessConfigurator - Windows: user credentials for starting the process as another user, console behavior, and window style
On macOS, preSpawnProcessConfigurator provides direct access to the underlying posix_spawn attributes and file actions:
import Darwin
import Subprocess
var platformOptions = PlatformOptions()
platformOptions.preSpawnProcessConfigurator = { spawnAttr, fileAttr in
var flags: Int16 = 0
posix_spawnattr_getflags(&spawnAttr, &flags)
posix_spawnattr_setflags(&spawnAttr, flags | Int16(POSIX_SPAWN_SETSID))
}
let result = try await run(.path(...), platformOptions: platformOptions)On Windows, preSpawnProcessConfigurator provides direct access to the underlying creation flags and startup info STARTUPINFOW used to call CreateProcessW:
import Darwin
import WinSDK
var platformOptions = PlatformOptions()
platformOptions.preSpawnProcessConfigurator = { creationFlags, startupInfo in
creationFlags |= DWORD(CREATE_NEW_CONSOLE)
}
let result = try await run(.path(...), platformOptions: platformOptions)See the PlatformOptions documentation for a complete list of configurable parameters on each platform.
Subprocess works on macOS, Linux, and Windows, with feature parity across all platforms as well as platform-specific options for each.
| Platform | Support Status |
|---|---|
| macOS | Supported |
| Ubuntu 20.04 | Supported |
| Ubuntu 22.04 | Supported |
| Ubuntu 24.04 | Supported |
| Red Hat Universal Base Image 9 | Supported |
| Debian 12 | Supported |
| Amazon Linux 2 | Supported |
| Windows 11 | Supported |
The latest API documentation can be viewed by running the following command:
swift package --disable-sandbox preview-documentation --target Subprocess
Subprocess is part of the Foundation project. Discussion and evolution take place on the Swift Foundation Forum.
If you find something that looks like a bug, please open a Bug Report! Fill out as many details as you can.
Like all Swift.org projects, we would like the Subprocess project to foster a diverse and friendly community. We expect contributors to adhere to the Swift.org Code of Conduct.
The Foundation Workgroup communicates with the broader Swift community using the forum for general discussions.
The workgroup can also be contacted privately by messaging @foundation-workgroup on the Swift Forums.