Develop a command-line tool using Swift Concurrency
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
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?
No, at the time I wrote this blog post (beginning of January 2022) there is 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.
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.
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.
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
.
Now I can finally adopt the example
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. Check it out!
Bonus Tip
Do you struggle with the error dyld: Library not loaded: @rpath/libswift_Concurrency.dylib
when running your command-line tool from within Xcode?
In my implementation shared on GitHub, check Package.swift
file and 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.
Originally published at https://blog.eidinger.info.