The Problem
SwiftArgumentParser package provides a great infrastructure to write command line interface (CLI) tools.
Normally, CLI tools process tasks in synchronous ways, one command after the others. But sometimes, when interacting with long running task, such as communicating with remote servers, we also want to give users of our CLI tools feedback about the progress, and any errors that could happen during processing.
Currently, there are different way create asynchronous CLI commands using Swift Argument Parsers such as:
- Using
Runloop.main.run()
- dispatchMain
- DispatchSemaphore
These APIs might work great, but it is quite complicated to integrate into command line interfaces.
Following code is an example of a simple CLI command to fetch info about an user from GitHub API using octokit.swift library.
@main
struct FetchUser: ParsableCommand {
func run() throws {
let semaphore = DispatchSemaphore(value: 0)
Octokit().user(name: "octocat") { response in
switch response {
case .success(let user):
dump(user)
case .failure(let error):
dump(error)
}
semaphore.signal()
}
semaphore.wait()
}
}
The Solution
A new Concurrency feature has been introduced into Swift langauge recently. This feature simplifies writing asynchronous code by using async/await
keywords
We can easily transform a completion-based API to async/await-based API using withCheckedContinuation and withCheckedThrowingContinuation
Convert completion-based APIs to async/await
In this example, we are using Octokit to fetch information of a GitHub’s user.
We can easily convert the above completion-based API into async/await-based API using withCheckedThrowingContinuation
func fetchUser() async throws -> User {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<User, Error>) in
Octokit().user(name: "octocat") { response in
switch response {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Integration with AsyncParsableCommand
SwiftArgumentParser package currently supports async/await integration in a separated branch
This branch introduces some additional type to expose async/await API, such as AsyncMain, AsyncParseableCommand.
import ArgumentParser
import OctoKit
@main enum Main: AsyncMain {
typealias Command = FetchUser
}
struct FetchUser: AsyncParsableCommand {
func run() async throws {
let result = try await fetchUser()
dump(result)
}
func fetchUser() async throws -> User {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<User, Error>) in
Octokit().user(name: "octocat") { response in
switch response {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
Now, when running swift run fetch-user
, you should see the following result in your terminal.
➜ swift run fetch-user
[3/3] Build complete!
▿ OctoKit.User #0
- id: 583231
▿ login: Optional("octocat")
- some: "octocat"
▿ avatarURL: Optional("https://avatars.githubusercontent.com/u/583231?v=4")
- some: "https://avatars.githubusercontent.com/u/583231?v=4"
▿ gravatarID: Optional("")
- some: ""
▿ type: Optional("User")
- some: "User"
▿ name: Optional("The Octocat")
- some: "The Octocat"
▿ company: Optional("@github")
- some: "@github"
▿ blog: Optional("https://github.blog")
- some: "https://github.blog"
▿ location: Optional("San Francisco")
- some: "San Francisco"
- email: nil
▿ numberOfPublicRepos: Optional(8)
- some: 8
▿ numberOfPublicGists: Optional(8)
- some: 8
- numberOfPrivateRepos: nil
▿ nodeID: Optional("MDQ6VXNlcjU4MzIzMQ==")
- some: "MDQ6VXNlcjU4MzIzMQ=="
▿ url: Optional("https://api.github.com/users/octocat")
- some: "https://api.github.com/users/octocat"
▿ htmlURL: Optional("https://github.com/octocat")
- some: "https://github.com/octocat"
▿ followersURL: Optional("https://api.github.com/users/octocat/followers")
- some: "https://api.github.com/users/octocat/followers"
▿ followingURL: Optional("https://api.github.com/users/octocat/following{/other_user}")
- some: "https://api.github.com/users/octocat/following{/other_user}"
▿ gistsURL: Optional("https://api.github.com/users/octocat/gists{/gist_id}")
- some: "https://api.github.com/users/octocat/gists{/gist_id}"
▿ starredURL: Optional("https://api.github.com/users/octocat/starred{/owner}{/repo}")
- some: "https://api.github.com/users/octocat/starred{/owner}{/repo}"
▿ subscriptionsURL: Optional("https://api.github.com/users/octocat/subscriptions")
- some: "https://api.github.com/users/octocat/subscriptions"
▿ reposURL: Optional("https://api.github.com/users/octocat/repos")
- some: "https://api.github.com/users/octocat/repos"
▿ eventsURL: Optional("https://api.github.com/users/octocat/events{/privacy}")
- some: "https://api.github.com/users/octocat/events{/privacy}"
▿ receivedEventsURL: Optional("https://api.github.com/users/octocat/received_events")
- some: "https://api.github.com/users/octocat/received_events"
▿ siteAdmin: Optional(false)
- some: false
- hireable: nil
- bio: nil
- twitterUsername: nil
▿ numberOfFollowers: Optional(4690)
- some: 4690
▿ numberOfFollowing: Optional(9)
- some: 9
▿ createdAt: Optional(2011-01-25 18:44:36 +0000)
▿ some: 2011-01-25 18:44:36 +0000
- timeIntervalSinceReferenceDate: 317673876.0
▿ updatedAt: Optional(2022-01-24 15:08:43 +0000)
▿ some: 2022-01-24 15:08:43 +0000
- timeIntervalSinceReferenceDate: 664729723.0
- numberOfPrivateGists: nil
- numberOfOwnPrivateRepos: nil
- amountDiskUsage: nil
- numberOfCollaborators: nil
- twoFactorAuthenticationEnabled: nil
- subscriptionPlan: nil
The asynchronous process to communicate with GitHub API server to fetch user info can be done with a simple call let result = try await fetchUser()
. Isn’t it simple?
Conclusion
In this article, I have introduced you to a new in-coming feature of SwiftArgumentParser which simplifies writing asynchronous CLI command a lot. By using async/await
keywords, and converting our main entry command into AsyncMain
, we can call our asynchronous commands in sequences with simple APIs.
Hopefully, the async
branch will be merged into the main branch of swift-argument-parser soon. But for now, we can already use this feature by pointing our Package.swift
directly to the async
branch.
Swift is also a great language for scripting. Swift Argument Parser is a must-have package when building a CLI tool using Swift. I have recently explored the new feature from Swift Argument Parser that helps us bringing async/await into our CLI tools.https://t.co/MoHIgzfmsx
— An Tran (@antranapp) February 1, 2022