Update 2023-08-28: There is a newer post with more, updated information on this topic.
Apple recently published the Swift Package ArgumentParser. As the name implies, this provides a library of commands to parse arguments in a command line tool.
For MacAdmins, Swift can be a useful choice to build tools, especially now that built-in Python is going away in some ‘future macOS.’ While you can (and should) ‘bring your own Python’ for MacAdmin tools, using Swift is interesting alternative.
I have talked about this in detail in my 2018 MacSysAdmin presentation “Swift for Apple Admins” and towards the end of my 2019 MacSysAdmin presentation “Moving to zsh.”
A powerful and ‘official’ library to parse arguments has been a glaring hole when writing Swift command line tools. Now, ArgumentParser can fill this gap. But, to use the ArgumentParser package, you need to use Swift Package Manager (SPM). Previously you could only use SPM from the command line. With Xcode 11 there is some support for Swift Package Manager in the UI. Either way, it is still an extra hurdle for a MacAdmin before they can build their tool.
In this post I will show how to build a simple command line tool using Swift Package Manager and the ArgumentParser package. And we will do it entirely from the command line, but also be able to use Xcode.
Preparation
You will need Xcode 11 or higher, preferably the latest version that works on your version of macOS. You can get Xcode from the Mac App Store or from the Apple Developer Portal.
Once you have downloaded Xcode, launch it to accept the license agreement and finish the additional installations, then we are ready to go.
What will we build?
I will build a simple command line to read user preferences. In macOS the value of a setting can come from a number of different ‘domains.’ A preference can be set by the user, for the computer, or from an MDM server with a configuration profile.
The built-in defaults
command will read preferences, but only from the user level, or from a specified file path. This ignores one of the main features of macOS’ preferences system. The defaults
tool will not realize or show if a value is being managed by a higher level, such as a configuration profile.
This is the simplified introduction into macOS Preferences and domains. You can learn all about preferences and profiles in my book “Property Lists, Preferences and Profiles for Apple Administrators.”
We will build a simple command line tool which can show the consolidated value of a setting, no matter where the value comes from.
I have built such a tool using python and eventually I will want to re-build that with Swift. But for our demo, we will just have a very simple subset of the functionality, so we will call our simple Swift tool just prf
.
Create a Project with Swift Package Manager
Open Terminal. Change Directory for the place where you store your code projects. (For me that is ~/Projects
.) Then create a new empty directory for the project and change into it.
> mkdir swift-prf
> cd swift-prf
Then we run the swift
command to set us up with an template SPM project.
> swift package init --type executable --name prf
The set the type as executable
because we want to build a command line tool. We also set the name to just prf
. When you don’t give a name, it will use the name of the enclosing directory.
This will create a bunch of files. Our main code is in Sources/prf/main.swift
. Right now, that is a single line print("Hello, world!')
.
You can build and run the project from Terminal with swift run
:
> swift run
[3/3] Linking prf
Hello, world!
Adding the ArgumentParser Package
Before we can write our own code, we need to add the ArgumentParser package to the project. The configuration for this is in the Package.swift
file in the root of the project. Open that file with your favored text editor.
In the dependencies
array that starts in line 8, add a line for the swift-argument-parser
package like this:
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.5"),
],
Version 0.0.5
is the latest version as I write this post.
Then further down in the code there is .target
named "prf"
which contains another dependencies array. Add the ArgumentParser
package there as well:
.target(
name: "prf",
dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]),
Leave the rest of the Package.swift
file as it is.
You can now run the tool again. You will see that swift will download and compile the package:
> swift run
Fetching https://github.com/apple/swift-argument-parser
Cloning https://github.com/apple/swift-argument-parser
Resolving https://github.com/apple/swift-argument-parser at 0.0.5
[32/32] Linking prf
Hello, world!
We will still get the same output, because we haven’t changed the code of our tool yet. But now we can use the ArgumentParser package.
The Code for the Tool
Open Sources/prf/main.swift
in you favorite text editor, delete the single print
line and add the following:
// prf
// a simple tool to read preferences
import Foundation
import ArgumentParser
struct ReadPreference: ParsableCommand {
@Argument()
var domain: String
@Argument()
var key: String
func run() throws {
let plist = CFPreferencesCopyAppValue(key as CFString, domain as CFString)
print(plist?.description ?? "<no value>")
}
}
ReadPreference.main()
Then try to run it again:
> swift run
[3/3] Linking prf
Error: Missing expected argument '<domain>'
Usage: read-preference <domain> <key>
At first glance this looks like an error, but when you look at it closely, you see that the tool is complaining that expected arguments are missing. Right now, the prf
tool expects two positional arguments, the preference domain and the key. You can pass arguments into swift run
but when you do, you need to provide the tool name as well;
> swift run prf com.apple.loginwindow lastUserName
armin
Now, you might want to confirm this and read the same preference with defaults read com.apple.loginwindow
and you will not find the lastUserName
key and value there. This is because this value is not stored on the user level, but on the computer level, when you run defaults read /Library/Preferences/com.apple.loginwindow
you see the value. So we see, that our simple prf
tool properly finds and displays the consolidated value!
What the Code does
When we look at the code we see that the main work happens in the run()
function of a ReadPreference
struct that inherits most of its behavior from ParsableCommand
struct from the ArgumentParser library.
In that function we use the CFPreferencesCopyAppValue
function which gets a consolidated preference value for a key and identifier pair.
The two properties of this struct are labelled with Swift Property Wrappers: @Argument()
this tells the ArgumentParser code that these are expected positional arguments that will be filled into these properties.
Everything else happens automatically.
Because we have two properties with the @Argument()
wrapper, the command line tool will expect two arguments, otherwise it will return an error. This is what we saw earlier.
Get Xcode in the Mix
All we’ve needed so far is the swift
command in Terminal and a text editor. Conceivably, this may be enough for building a decently complex tool. But, if you prefer to edit in Xcode, you can still do that. You can create an Xcode project file with:
> swift package generate-xcodeproj
generated: ./prf.xcodeproj
And then you can open that and edit, build, and run your tool in Xcode.
In Xcode you can provide arguments to the tool in the Scheme Editor. However, it is quite cumbersome to keep changing those for iterative testing. That’s when falling back to to swift run prf
in the command line is very useful.
When you change the configuration in the Packages.swift
files to remove or add more dependencies, you should re-generate the Xcode project file with swift package generate-xcodeproj
.
Where to go from here
As you can tell from the version number, ArgumentParser is still very ‘young’ and it doesn’t have quite as deep a feature set as Python’s argparse
. But there is, even now, much, much more to the ArgumentParser library than we are using in this simple example. There are verbs, options with long and short flags, and help messages and many other things. You can find the details in the package’s documentation.
There is also much more to Swift Package Manager and how to use it. You can learn more about SPM in its documentation.
Nevertheless, this should give you enough to get you started.
I am looking forward to the tools you build!