SwiftArgumentParser: A Swift mod
During the WWDC 2021, Apple promoted for the first time its new SwiftArgumentParser library. It aims to help creating command-line tools in Swift.
In just a few lines of code, we can build a fully documented command-line tool. Like in this example from its tiny (I’m pretty sure they made it tiny on purpose) README file:
We express our CLI as a graceful command tree while
SwiftArgumentParser takes care of all the implementation details: it parses the command-line arguments, instantiates the target command, fulfills its arguments based on their names and finally runs it. It can even generate a built-in help description if a help option is passed:
Apple brands its library as straightforward.
This achievement is big: even if you have never used the library, chances are you can already tell what the code does and how you could use it to create your own CLI command out of it.
Have you ever seen anything as straightfordward in Swift?
SwiftArgumentParser should remind you of
SwiftUI. To create a new CLI option, you declare an
@Option property without writing any parsing code. An approach that is similar to the declarative syntax of
SwiftUI. In each case, thanks to this high level of abstraction, we only need to state what our code should do rather than writing it explicitly.
However, because Swift’s heavy focus on types,
straightfordwardness is usually not the first quality that comes to mind. Contrary to dynamic languages, such as Ruby, Swift strongly relies on compilation-time validations, making it a safer, but significantly less flexible, language.
In fact, even after pondering on this for a bit, I have no idea how to implement such a behavior using Swift.
SwiftArgumentParser is not a regular Swift library.
To corroborate, in the previous example, if instead of the
main method, we used the autogenerated empty initializer of
This state-of-the-art, Swift library would just… crash! (In this case,
Argument was initialized with only a basic help description, how could
phrase return a string out of it?)
It seems Apple has to bypass some Swift compiler safety checks to offer such an API. It decided to do it even if it could obviously lead to serious codebase issues. Something we, thorough Swift developers, are not used to doing when targeting a production environment. Something we are not used to seeing from Apple.
Starting from this point, I knew the
SwiftArgumentParser repository would contain some cryptic but powerful Swift usages. I had to deep dive inside it and figure out how Apple managed to make the magic happen.
Recreating the smallest SwiftArgumentParser API
There are a lot of blogposts out there on how to use
SwiftArgumentParser. In this one, I will instead highlight the main implementation tricks Apple used to provide such beautiful API and what is the cost of it.
The library only reproduces the following key features:
FBParsableCommandprotocol with a
mainentry point able to parse the command line arguments and execute the program’s logic.
FBOptionString property wrapper to express the String options of the CLI. It supports default values.
- A builtin support for help.
Similarly to the
SwiftArgumentParser, an example of
FBArgumentParser in a
README file would look like this:
And its usage like that:
Let’s see how to implement it step by step while keeping it straightforward.
Defining the main
Of course, our implementation starts with the library’s main protocol:
It is a very basic version of the
ParsableCommand. For example it does not support nesting commands nor contains those fancy properties to customize its help description. But that’s enough for now. As the original one, it will be used by the users of the library to declare their program logic.
We also define the same static
main entry point in an extension (the users are not supposed to override it) as a static method:
The execution steps of the
main method are described in the
You kick off execution by calling your type’s static main() method. The SwiftArgumentParser library parses the command-line arguments (#1), instantiates your command type (#2), and then either executes your run() method or exits with a useful message (#3).
Let’s write those steps.
First, we create a
FBCommandParser object to encapsulate the command line arguments parsing code.
As we only support CLI option type arguments (“–option value” or “-o value”), we store those key-value arguments in a basic dictionary. We would need something more sophisticated if more argument types were supported, but this will do for now.
Then, we execute the
run method of the created instance and neatly exit the program if an error occured:
We purposedly left a huge
FBCommandParser. We basically still have to implement the core of the program: instantiating the target command and filling its properties based on the parsed CLI arguments dictionary.
Instantiating client commands
FBParsableCommand obviously needs an initiliazer constraint:
main is a static method and
run an instance one.
Adding a basic
init(arguments: [String]) would work from an implementation perspective:
But it would require a lot work for the library’s users to implement it. The user would have to manually fulfill its properties based on the given dictionary. Our library should do all the heavy lifting.
This is where
SwiftArgumentParser shined for the first time. It used a very clever technic:
Decodableto make the compiler generate all the argument parsing code on the client side in one fell swoop
In fact, if we consider the
[String: String] argument array as a basic formatted data, we can consider
FBParsableCommand as a custom data type. Thus if we can make the
FBParsableCommand inherit from
All the decoding code can be generated at compilation time right inside the command implementation!
Let’s see it in action:
We first define a basic custom
Decoder object able to return the value associated to the given argument property.
It is almost straightforward as the value associated to a CLI option is already mapped to its name by our
FBCommandParser.parseArguments method. We just need to use the decoding key as a search key in the parsed dictionary.
Then, we can use our custom
Decoder to instanciate the target command, thanks to its conformance to
Now, as long as the client command properties implement
The Swift compiler will automatically generate the appropriate
Decoder method calls to fulfill all the properties of the target command.
Notice though that
FBRepeat does not manage default values correctly yet. If we set a default value to a property:
It will be ignored by the generated implementation of the
It seems like a small problem but it would force users to provide a custom implementation of
Decodable in order to add default argument. We do not want that. We need to provide the users with an easy way to customize the decoding of its command.
You’ve guessed it, it is time to introduce property wrappers!
Using property wrappers as argument descriptions
SwiftArgumentParser the information that we need to collect from the command line is defined using property wrappers. Each one of them matches to a type of argument (
@Argument etc) and provides initiliazers to describe them. We can specify how to parse the argument, its potential default value, its help description etc.
Let’s add a similar declarative API in
FBSwiftArgumentParser and see how we can use it to tweak the parsing behavior.
We start with a basic
It has two public initializers so clients can declare two kinds of arguments:
FBOption.init(wrappedValue:)describes an optional option with a default value
FBOption.init()describes a required option
Its conformance to
Decodable is straightforward, thanks to singleValueContainer, so we keep the previous benefits provided by the Swift compiler:
As expected, our code is still as broken as before though:
When no value is specified,
FBCommand.main method still tries to decode the command using
FBOption.init(from:) without being able to provide a proper value for the
phrase argument. The declared default “Hello” value is ignored.
Let’s adapt our parsing code and take into account the potential default values provided by the property wrappers.
The two faces of the command property wrappers
This is probably the most confusing aspect of the implementation.
SwiftArgumentParserproperty wrappers have two usages depending on their initiliazation
Because of their declarative interfaces, when decoded, a property wrapper provides a value that can be used by the user during the execution of the command. Otherwise, it only provides a description of itself, used internally, that describes how to decode it.
Let’s highlight those two aspects in
init(wrappedValue:), used by the library’s clients, produces an
FBOptionthat describes how to parse its underlying String value
init(from:), used internally, produces a decoded
FBOptionwith a valid wrapped value
In code, we can represent the two cases using an enum:
In the first case,
FBOption only wraps a
In the other case,
FBOption stores its actual value:
As a result,
FBOption can only return a value if it is decoded:
This is the issue we faced in the introduction and obviously a tradeoff:
FBOption has two usages but only one non optional public API.
However, as long as the library clients rely on the
main method to execute their program, we can ensure only the autogenerated
Decodable.init(from:) initializer of the parsable command will be used to call the
run method. Thus each property wrappers will be decoded and return a value when the client tries to read a potential CLI option.
Furthermore, when a user declares a command, it now implicitly declares a list of
Let’s see how we can use them in our decoding code to ensure the optional values provided will be used when the decoding occurs.
Using property wrappers as reflective markers
Even if it seems at odds with its heavy focus on compile-time validation, Swift enables us to inspect, and work with, the members of a type — dynamically, at runtime. It is by using this uncommon capacity of Swift that Apple implemented the decoding of the property wrappers.
Let’s see how it works.
We first need a new constraint on the protocol:
This way, we force the user to provide a way to instantiate a command in a proper definition state, using the definition given by each property’s wrapper.
In fact, we could be confident that the users will only use descriptive wrapper initializers in this case:
Seeing the use of a decoding one, even if it compiles, would be pretty unusual:
Thanks to this assumption, as long as the user respects the contract, by instanciating a blank version of the target command, we can use
FBOption as a reflective marker and retrieve all its argument definitions:
Notice that we need to parse the property wrapper names to match them with their corresponding key, each property wrapper starts with an underscore prefix.
We can now easily retrieve all the potential default values of a command with their corresponding arguments:
By merging them with the initial CLI arguments dictionary, we can thus easily fill the potential gaps when initializing the target command, using this time its
Thus when an optional option is used:
@FBOption wrapper will be decoded with its default value:
The decoding is fully functional!
Let’s implement the final feature: generating a help description when requested.
Generating command help
Thanks to the mirror inspection already in place, it will be easy. We just have to add some final touches to our API.
We add a
FBCommandConfiguration structure so the clients can provide a description of their commands:
help parameter in each argument:
Based on the static definition of a command, we can now easily generate a detailed error anytime we catch a help argument:
And here we go!
FBSwiftArgumentParser is able to generate a description of our command when a help option is specified:
The drawback of the SwiftParserArgument implementation
Our basic version of
SwiftArgumentParser is small but it highlighted the main key tricks to create such a straightforward Swift library. Apple used a clever interlacing of operations executed at both compilation time and runtime.
At compilation time, all the command decoding code is generated on the client side thanks to the automatic conformance to
Decodable. At runtime,
SwiftArgumentParser parses the static definition of each command and adapts the decoding strategy consequently. This last step is permitted by an extended usage of property wrappers: it allows Apple to encapsulate and inject logic right inside the client objects declaration. I really liked it.
However, to use the API of
SwiftArgumentParser, clients have to follow an unusual list of agreements to ensure the program runs smoothly:
- All the properties of a command have to be property wrappers provided by the library
- We must not override the default decoding behavior of the commands
- We must not instantiate commands ourself
Such agreements can not be guaranteed by the Swift compiler. In fact,
SwiftArgumentParser counts more than twenty fatalError in its codebase. That is more than twenty manual checks that will lead to a crash if you are not respecting the usage contract.
That is why its implementation is hard to understand. In a regular codebase, targetting a production environment, we prefer to rely on the Swift compiler and therefore stay in the arguably safer environment it creates for us even if it means a more verbose code.
A Swift mod
SwiftArgumentParser, Apple obviously made a tradeoff and decided that straightforwardness takes precedence over safety when writing CLI tools.
The fact that
SwiftArgumentParser will always be at the top of any programming stack surely influenced the decision.
SwiftArgumentParser adds its own programming paradigm on top of Swift. By establishing rules that go way beyond traditional Swift library usage restrictions, like “commands should not use regular properties”, it extends the language itself, creating sort of a mod of Swift and raising our code to new heights.
Of course, as the Swift compiler only knows Swift principles, those libraries have to validate their own rules at runtime with technics using metaprogramming as reflection. It inevitably leads to unsafe misuses.
In 2021, Apple had to demystify SwiftUI. By revealing a bit of the SwiftUI implementation, Apple somehow agreed its usage of Swift can be confusing. We actually could not write such libraries using regular Swift - at least, using Swift as Apple wants us to use it, in a safe manner.
Still, chances are, those libraries are pioneers in the language features development rather than classic unsafe implementations. I will be not be suprised to see new Swift mods from Apple in the future.
As a side note, it is interesting to see how, after putting so much effort in building a safe language, Apple allows itself to bypass some of the compiler checks and write “flexible” Swift code, close to the good old dynamic typing of Objective-C.
SwiftArgumentParser when developing xcresource. You can take a look at it! It aims to facilitate downloading Xcode templates or snippets from git repositories.
Thanks for reading!