Introduction to the Swift Subprocess Package
The Swift language group’s Subprocess package lets you spawn processes in Swift apps. The package makes it easier to run Terminal commands and launch command-line programs from Swift apps.
Launching a program
Call the run function to launch a process. In most cases a process will be a Terminal command, a shell script, or a command-line program.
The run function is asynchronous so you must add await when calling run. The function can throw errors so you should wrap the call in a do-catch block to handle errors.
If the program to run is installed on the user’s computer, use the .name parameter and supply the name of the program to run. You must also supply an output argument to specify how to store the output.
The following code runs the ls command and collects its output in a string:
do {
let result = try await run(.name("ls"),
output: .string(limit: 2048))
} catch {
print(error)
}
Use the standardOutput property from the result to access the program’s output.
Launching a program from a Mac app bundle
If you bundle a command-line program with a Mac Swift app, you can’t use .name to run the program because the program isn’t installed on the user’s Mac. You must use the .path parameter and supply a path to the program’s location. If you have a URL for the program, use its path property as the path.
The following code runs a program in the app bundle named MyProgram:
var executableLocation: FilePath
let bundle = Bundle.main
let resourceFolder = bundle.resourceURL
let executableURL = resourceFolder?.appendingPathComponent("MyProgram")
executableLocation = FilePath(executableURL?.path(percentEncoded: false) ?? "/")
do {
let result = try await run(.path(executableLocation),
output: .string(limit: 2048))
} catch {
print(error)
}
Passing arguments
Many Terminal commands and command-line programs take arguments. To pass these arguments to the run function, add an arguments parameter to the run function and supply the arguments.
arguments: ["arg1", "arg2", "arg3"]
The Subprocess package has an Arguments data structure for the arguments. If you create a variable to hold a program’s arguments, make sure its data type is Arguments and not an array of strings.
Setting the working directory
If you need to set the working directory for a program, add a workingDirectory argument to the run function and supply the working directory.
The workingDirectory argument takes a FilePath instance, not a URL. If you have a URL, use the URL struct’s path function to get a file path.
var currentFolder: URL
var workingDirectory: FilePath
workingDirectory = FilePath(currentFolder.path(percentEncoded: false))
Output options
The examples so far have returned the output as a string. You must specify a length limit for the string. Make sure you set a high enough limit. If the output’s length exceeds the limit, the run function returns no output.
Other output options include the following:
- A
Dataobject - A file descriptor
- An array of bytes
Returning error messages
If you need to return an error message if the program runs unsuccessfully, add an error argument to the run function. The data type for the error message can be one of the output option types: string, Data object, file descriptor, or array of bytes.
error: .string(limit: 2048)
Use the standardError property from the result to access the error message.
Supplying input
If you need to supply input to a program, add an input argument to the run function. You can supply the following kinds of input:
- A string
- A
Dataobject - A file descriptor
- An array of bytes
- A data sequence
- An async data sequence
// content contains the input string
input: .string(content)
Check if a program ran successfully
To see if the program ran successfully, check the terminationStatus property of the result. The isSuccess property returns true if the program ran successfully.
if result.terminationStatus.isSuccess {
// Success
} else {
// Failure
}
A more complex example
The following function runs the Jujutsu version control system command jj bookmark list:
func runListBookmarksCommand(repoFolder: URL) async -> Result<String, JujutsuError> {
do {
let result = try await run(
.name("jj"),
arguments: ["bookmark", "list"],
workingDirectory: FilePath(repoFolder.path(percentEncoded: false)),
output: .string(limit: 8192),
error: .string(limit: 8192)
)
if result.terminationStatus.isSuccess {
return .success(result.standardOutput ?? "")
} else {
return .failure(JujutsuError(message: result.standardError ?? "Error"))
}
} catch {
return .failure(JujutsuError(message: error.localizedDescription))
}
}
struct JujutsuError: Error, Equatable {
var message: String = ""
}