Develop a command-line tool using Swift Concurrency
Swift Argument Parser and async/await

I am a Software Engineer working on open source and enterprise mobile SDKs for iOS and MacOS developers written in Swift. From 🇩🇪 and happily living in 🇺🇸
Search for a command to run...
Swift Argument Parser and async/await

I am a Software Engineer working on open source and enterprise mobile SDKs for iOS and MacOS developers written in Swift. From 🇩🇪 and happily living in 🇺🇸
No comments yet. Be the first to comment.
WWDC26 kicked off on June 8, 2026. The information in this article reflects the information published by Apple on that date. There are 14 new frameworks. Name Description AccessoryAccess Manage

WWDC25 kicked off on June 9, 2025. The information in this article reflects the information published by Apple on that date. New Frameworks NameDescription AlarmKitSchedule prominent alarms and countdowns to help people manage their time. AVR...

WWDC25 is almost here, and I couldn’t be more excited! Whether you're attending the official events, community meetups, or just soaking in the energy around Cupertino, there’s no better time to connect, share ideas, and celebrate everything we love a...

In this blog post, I’ll share an observation and advice regarding the caching behavior of network responses by Apple’s APIs. If you are unfamiliar with caching of network responses then I recommend Apple’s article Accessing cached data that introduce...
WWDC24 kicked off on June 10, 2024. The information in this article reflects the information published by Apple on that date. New Frameworks NameDescription AccessorySetupKitUse AccessorySetupKit to discover accessories with Bluetooth or Wi-Fi...

Updated for ArgumentParser 1.1.0 and its native support of async/await
Apple's ArgumentParser library makes developing a command-line tool in Swift significant easier. The library parses the command-line arguments, instantiates your command type, and then either executes your run() method or exits with a useful message.
Straightforward, type-safe argument parsing for Swift
import ArgumentParser
@main
struct Repeat: ParsableCommand {
@Flag(help: "Include a counter with each repetition.")
var includeCounter = false
@Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
var count: Int?
@Argument(help: "The phrase to repeat.")
var phrase: String
mutating func run() throws {
let repeatCount = count ?? .max
for i in 1...repeatCount {
if includeCounter {
print("\(i): \(phrase)")
} else {
print(phrase)
}
}
}
}
You might want to use modern Swift Concurrency and await an asynchronous function in your command-line program. Trying so will result in a compilation error.

Maybe changing the definition of the run function to add the async keyword?
Nope, because this particular function is needed "as-is" in the struct according to its ParsableCommand protocol requirement.
Does the ArgumentParser framework support other functions for async/await?
Swift ArgumentParser 1.1.0 brings out-of-the-box support for async/await.
Example
import ArgumentParser
@main
struct Repeat: ParsableCommand, AsyncParsableCommand {
@Flag(help: "Include a counter with each repetition.")
var includeCounter = false
@Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
var count: Int?
@Argument(help: "The phrase to repeat.")
var phrase: String
// A command's run() method now supports async/await when the command conforms to AsyncParsableCommand. (#404)
mutating func run() async throws {
let repeatCount = count ?? .max
await asyncRepeat(phrase: phrase, repeatCount: repeatCount)
}
func asyncRepeat(phrase: String, repeatCount: Int) async {
try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // dummy use of an aysnc operation .. wait 5 seconds :)
for i in 1...repeatCount {
if includeCounter {
print("\(i): \(phrase)")
} else {
print(phrase)
}
}
}
}
The secret is to conform your command to AsyncParsableCommand protocol and then use the keyword async in the run function declaration.
At the time I wrote this blog post originally (beginning of January 2022) there was no out-of-the-box support by the ArgumentParser framework.
Lucky for us the issue contains a code snippet from Sergio Campamá how to add support for async/await by ourself!
I am using this pattern and adopt the official Repeat example from Apple, share my lessons learned and which pitfalls to avoid.
First, in my program I define a new protocol to indicate that commands need to perform asynchronous work.
protocol AsyncParsableCommand: ParsableCommand {
mutating func runAsync() async throws
}
Every command conforming to AsyncParsableCommand needs to implement the asynchronous function named runAsync.
Similar to what the ArgumentParser does to invoke the run function for an ParsableCommand I create an extension on ParsableCommand and define a static async main() function.
extension ParsableCommand {
static func main() async {
do {
var command = try parseAsRoot(nil) /// `parseAsRoot` uses the program's command-line arguments when passing `nil`
if var asyncCommand = command as? AsyncParsableCommand {
try await asyncCommand.runAsync()
} else {
try command.run()
}
} catch {
exit(withError: error)
}
}
}
In the implementation I reuse the public function parseAsRoot from the ArgumentParser framework. It will return the main command (here: the Repeat struct from the example).
If the command implements the AsyncParsableCommand protocol then let's await the runAsync function. Otherwise simply call the command's run function.
By the way: you do not see any conditional statements despite that Swift Concurrency is only available starting from specific platform versions. The reason for this is that I restricted the platform to macOS 10.15 and above in my Swift Package manifest.
import PackageDescription
let package = Package(
name: "RepeatCommandLineTool",
platforms: [.macOS(.v10_15)],
products: [
.executable(name: "repeat", targets: ["RepeatCommandLineTool"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
],
targets: [
.executableTarget(
name: "RepeatCommandLineTool",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
.testTarget(
name: "RepeatCommandLineToolTests",
dependencies: ["RepeatCommandLineTool"]),
]
)
You have to rename your main.swift file if you haven't done that already. Otherwise you will run in the error
'main' attribute cannot be used in a module that contains top-level code
This has nothing to do with Swift Concurrency but because the @main attribute is used. Speaking of it ... I move the @main attribute, to indicate the top-level entry point for program, to a new enum which has an async static function which will await the newly async main function of the ParsableCommand.
@main
enum CLI {
static func main() async {
await Repeat.main()
}
}
Now I can finally adopt the example
struct Repeat: ParsableCommand, AsyncParsableCommand {
@Flag(help: "Include a counter with each repetition.")
var includeCounter = false
@Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
var count: Int?
@Argument(help: "The phrase to repeat.")
var phrase: String
mutating func runAsync() async throws {
let repeatCount = count ?? .max
await asyncRepeat(phrase: phrase, repeatCount: repeatCount)
}
func asyncRepeat(phrase: String, repeatCount: Int) async {
try? await Task.sleep(nanoseconds: 5 * 1_000_000_000) // dummy use of an aysnc operation .. wait 5 seconds :)
for i in 1...repeatCount {
if includeCounter {
print("\(i): \(phrase)")
} else {
print(phrase)
}
}
}
}
Finally I can await the asyncRepeat function which waits for 5 seconds before repeating the phrase passed as argument to the program :)
All the code shared is stored in the following GitHub repository.
In addition to this blog post I created a YouTube Video
Do you struggle with the error dyld: Library not loaded: @rpath/libswift_Concurrency.dylib when running your command-line tool from within Xcode?
Here is my advice on how to avoid the error. I was able to successfully test this on my local machine running macOS 11 and Xcode 13.2.
Add to the Package.swift file a new import statement on the top.
import Foundation
At the end of the file add the following code:
// IMPORTANT: enable the following function call if you encounter the error
// `dyld: Library not loaded: @rpath/libswift_Concurrency.dylib`
//hookInternalSwiftConcurrency()
func hookInternalSwiftConcurrency() {
let isFromTerminal = ProcessInfo.processInfo.environment.values.contains("/usr/bin/swift")
if !isFromTerminal {
package.targets.first?.addLinkerSettingUnsafeFlagRunpathSearchPath()
}
}
extension PackageDescription.Target {
func addLinkerSettingUnsafeFlagRunpathSearchPath() {
linkerSettings = [linkerSetting]
}
private var linkerSetting: LinkerSetting {
guard let xcodeFolder = Executable("/usr/bin/xcode-select")("-p") else {
fatalError("Could not run `xcode-select -p`")
}
let toolchainFolder = "\(xcodeFolder.trimmed)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx"
return .unsafeFlags(["-rpath", toolchainFolder])
}
}
extension String {
var trimmed: String { trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
}
private struct Executable {
private let url: URL
init(_ filePath: String) {
url = URL(fileURLWithPath: filePath)
}
func callAsFunction(_ arguments: String...) -> String? {
let process = Process()
process.executableURL = url
process.arguments = arguments
let stdout = Pipe()
process.standardOutput = stdout
process.launch()
process.waitUntilExit()
return stdout.readStringToEndOfFile()
}
}
extension Pipe {
func readStringToEndOfFile() -> String? {
let data: Data
if #available(OSX 10.15.4, *) {
data = (try? fileHandleForReading.readToEnd()) ?? Data()
} else {
data = fileHandleForReading.readDataToEndOfFile()
}
return String(data: data, encoding: .utf8)
}
}
Uncomment line // hookInternalSwiftConcurrency() which should solve the problem. In case the command-line tool is executed not from the terminal (= Xcode) then additional linker settings get added to the target. In particular the Runpath Search Path is set so that the dynamic linker is able to find libswift_Concurrency.dylib in your installed toolchain.