When building tools in Swift, I usually start with a command line tool. This allows me to ignore the complexity of creating a user interface while figuring out the underlying APIs and data models.
Technically, command line tools have a user interface, as well. They print output to pipes, standard out or standard error for data, progress, status or other information. They should provide information using the exit status. They take input from standard in, pipes, environment variables, or from command line arguments.
While there are many subtleties to consider with all of these, these “interfaces” are still less complex to handle than a full user interface built with AppKit, UIKit or SwiftUI.
Swift provides the functionality to deal with files, outputs, pipes with APIs. This post will not cover those. This post will focus on getting arguments from the command line.
Note: this tutorial was written using Swift 5.10, Xcode 15.4, swift-argument-parser 1.5 on macOS 14.6.1. The details and user interfaces may change with different versions, but the fundamentals should remain.
CommandLine.arguments
The built-in way to get the arguments for your process is with CommandLine.arguments
which returns an array of String
objects. CommandLine.arguments
is quite basic, but can be suitable for simple use cases.
Create a project directory named CLArgs
and use swift package init
to create a swift package to build an executable:
> mkdir CLArgs
> cd CLArgs
> swift package init --type executable
This will create a skeleton project, you will find a basic “Hello, world” code in Sources/main.swift
. Replace the print
statement there with:
import Foundation
let arguments = CommandLine.arguments.dropFirst()
guard let name = arguments.first
else {
print("command requires an argument!")
exit(1)
}
print("Hello, \(name)")
Note: You can use your favorite text editor or IDE to edit SPM projects. You can also use Xcode. When you run xed .
in the Swift package directory, Xcode will open the Swift package in a project view. You can edit, build and run the package in Xcode or use Xcode for editing and build and run it from the command line.
In terminal, build and run the project with
> swift run CLArgs Armin
This tells the swift
to build the CLArgs
target defined in Package.swift
and run it with the argument Armin
. You should see this output:
> swift run CLArgs Armin
Building for debugging...
[1/1] Write swift-version-39B54973F684ADAB.txt
Build of product 'CLArgs' complete! (0.11s)
Hello, Armin
Let’s look at the code in detail.
let arguments = CommandLine.arguments.dropFirst()
CommandLine.arguments
returns an array of strings. By convention, the first argument (arguments[0]
) contains the path to the executable. In most situations, you will not be interested in this first argument. One straightforward way to deal with this is to ‘drop’ the first element of the array right away.
guard let name = arguments.first
else {
print("command requires an argument!")
exit(1)
}
We get the first element of the arguments
array. Additional arguments are simply ignored. When no arguments are provided, this will return nil and guard
statement will trigger, where we print an error message and exit the code with a non-zero value, signaling a failure.
print("Hello, \(name)")
The actual point of this sample code: print a greeting with the name.
In this simplest of examples, we spend a majority of the code on preparing the arguments and verifying that they meet our requirements.
CommandLine.arguments
will serve you well for simple needs and quick command line tools. However, you will quickly notice that a robust command line tool needs to verify the existence of certain arguments, whether the value matches certain criteria, and print error messages and usage directions when the arguments don’t match the expectations. Many command line tools also have flags and options with short and long forms that need to be processed.
This turns into a lot of code very quickly.
Swift Argument Parser
Enter Swift Argument Parser. A package that provides “straightforward, type-safe argument parsing for Swift.”
You could modify the Package.swift
file in our CLArgs project to import Swift Argument Parser but there is an even easier way to start. Back out of the CLArgs
project directory and create a new one:
> cd ..
> mkdir SwiftArg
> cd SwiftArg
> swift package init --type tool
When you inspect the Package.swift
file in this new project, you will see that it is already linked to the Swift Argument Parser package. Sources/SwiftArgs.swift
contains another “Hello, world” template code, but using Swift Argument Parser.
import ArgumentParser
@main
struct SwiftArgs: ParsableCommand {
mutating func run() throws {
print("Hello, world!")
}
}
The struct here implements the ParsableCommand
protocol which allows us to use all the nice functionality from the ArgumentParser
library. It is also marked with the @main
tag, which tells the compiler to run the main()
function in this when the binary is launched. The main()
function is implemented by ParsableCommand
which, well, parses the arguments and then launches the run()
function.
Swift Package Manager vs Xcode projects
You can open and edit Swift Package Manager projects in Xcode with the xed .
command. Recent Xcode versions know how to work with SPM projects without needing to create an Xcode project file. Xcode will use the configurations in the Package.swift file. This is useful when you like to work in Xcode, but want the project to remain transferable to other editors or IDEs.
There is a weird quirk. When you build and/or run the project from within Xcode it will use the default Xcode build directory (default is ~/Library/Developer/Xcode/DerivedData/
). This is different from the location that the swift build
or swift run
commands in Terminal use (.build
in the package directory). This can lead to longer build times and confusion.
You can also use swift-argument-parser and other packages within Xcode projects. This can be necessary if you are building the command line as a target within a larger Xcode project. Maybe you want to use some of Xcode’s advanced features for managing projects, like build phases and Archives. Or maybe you just prefer working in Xcode.
To create a command line with ArgumentParser in Xcode, create a new Project and select the ‘Command Line Tool’ template for macOS. Once the new project is created, select ‘Add Package Dependencies…’ from the File menu. Locate ‘swift-argument-parser’ in the ‘Apple Swift Packages’ collection or just enter the URL in the search field and click ‘Add Package…’ (twice)
Then, you have to delete the main.swift
file from the template and create a new SwiftArgs.swift
with this code:
import Foundation
import ArgumentParser
@main
struct SwiftArgs: ParsableCommand {
mutating func run() throws {
print("Hello, world!")
}
}
This is the same as the template code created with the swift package init --type tool
from above.
When testing and running the command line tool in Xcode will will want to pass arguments into the binary. You can do so by editing the scheme. Choose Product > Scheme > Edit Scheme… from the menu or click on the target icon (the one with the command line icon in the center of the menu bar) and select Edit Scheme… Make sure you are on the ‘Run’ section in that dialog and select the ‘Arguments’ tab. Here you can add, remove, enable or disable the arguments that Xcode passes into your tool when you run it from Xcode.
Continually changing the arguments in the scheme editor can be tedious. You can also use ‘Show Build Folder in Finder’ from the ‘Product’ menu, open the Products/Debug
folder in Terminal by dragging that folder on to the Terminal icon in the Dock and run the built command from there with ./SwiftArgs
Whichever way you prefer to create and work with your project, the rest of this tutorial will work the same way.
Using Swift Argument Parser
Right now, we are just running print("Hello, world!")
, which is quite underwhelming. Let’s step this up just a little bit:
@main
struct SwiftArgs: ParsableCommand
@Argument var name: String
func run() {
print("Hello, \(name)")
}
}
First we create a property called name
of type String
with the @Argument
property wrapper. This tells the ArgumentParser
library, that we want this variable filled with an argument from the command line. When the run()
function is called, we can “just use” the name property, like any other Swift property.
When you run this, something interesting happens: we get an error!
> swift run SwiftArgs
Error: Missing expected argument '<name>'
USAGE: swift-args <name>
ARGUMENTS:
<name>
OPTIONS:
-h, --help Show help information.
When you check the exit code of the command with echo $?
you see it return an error code of 64. This means it was missing arguments or got malformed arguments. As should be good practice for command line tools, our tool did print a help message, describing what it was expecting. Here we see that our SwiftArgs command expects a single argument giving a name.
Run the command again, but with an argument:
> swift run SwiftArgs Armin
Hello, Armin
Now everything works as expected. When our tool launches, ArgumentParser grabs the argument, places it in our name
property and executes the run()
function in our struct that implements ParsableCommand
. Since ArgumentParser errors out with the help message, when an argument is missing or too many arguments are present, we can be certain that the name
variable is populated when our code runs.
Command Configuration
There is a small detail that is bugging me, though. The help message generated by ArgumentParser deduced that the name of binary should be swift-args
instead of SwiftArgs
, but the binary name is SwiftArgs
, which is the name of the directory we initialized the project in. This is because of different naming standards for Swift types and command line tools. You can change the name of the executable created in the Package.swift
file in line 15 under .executableTarget
.
We could change the name to something completely different here, say apdemo
for ‘Argument Parser Demo. When you apply that change in
Package.swift` it changes the name of the binary, but the auto-generated help message does not pick that up. It still use the auto-generated name.
> swift run apdemo --help
USAGE: swift-args <name>
ARGUMENTS:
<name>
OPTIONS:
-h, --help Show help information.
(Isn’t it neat that ArgumentParser automatically implements --help
and -h
flags?)
We could change the name of our struct, which will work in simple situations. But you will have a situation where the struct name will not match what you want for the executable name. There is a way to tell ArgumentParser exactly what we want, though.
Insert this code below the struct SwiftArgs
line and above the @Argument
:
static let configuration = CommandConfiguration(
commandName: "apdemo"
)
When you now look at the generated help again, the command name matches:
OVERVIEW: apdemo - swift-argument-parser tutorial tool
USAGE: apdemo <name>
ARGUMENTS:
<name>
OPTIONS:
-h, --help Show help information.
There is more information we can provide in the command configuration. Extend the CommandConfiguration
initializer like this:
static let configuration = CommandConfiguration(
commandName: "apdemo",
abstract: "apdemo - swift-argument-parser tutorial tool",
version: "0.1"
)
and run the command to get the help message again.
OVERVIEW: apdemo - swift-argument-parser tutorial tool
USAGE: apdemo <name>
ARGUMENTS:
<name>
OPTIONS:
--version Show the version.
-h, --help Show help information.
The abstract
appears as an ‘overview’ above the help message and we now see a new option --version
. When you run the tool with that option, you will not be surprised to see the 0.1
provided in the configuration, but it is useful nonetheless.
There are more fields you can provide in the CommandConfiguration
: discussion
allows you to provide a long form description of the command. usage
allows you to override the auto-generated usage text. There are some more that we will explore later.
More Arguments
You can add more @Arguments
and they will be filled in order from the arguments provided at the command line. Add another property with the @Argument
wrapper:
@Argument var name: String
@Argument var age: Int
func run() {
print("Hello, \(name)!")
print("You are \(age) years old.")
}
When you run the tool without any arguments, you can inspect the updated help message. The usage and arguments area now shows both expected arguments. When you run the tool with a single argument, you get an abbreviated help, showing only the missing argument. When you provide a name and a number as command line arguments, everything works as expected.
But what if you provide two strings?
> swift run apdemo Armin lalala
Error: The value 'lalala' is invalid for '<age>'
Help: <age>
Usage: apdemo <name> <age>
See 'apdemo --help' for more information.
We declared the age
property as an Int
, so ArgumentParser expects an integer number for the second argument. When the second argument cannot be parsed into an integer, it shows the error.
Change the type of the age
property to a double and run it again with a decimal for the age.
Some Help, please?
name
and age
might be enough to tell a user of your command line tool what to enter. But I think we should provide a bit more explanation. You can attach a help
message to the argument:
@Argument(help: "a name")
var name: String
@Argument(help: "age (integer number)")
var age: Int
I have broken the property declarations into two lines each for clarity and changed the age
back to an Int
for simplicity. The help messages will appear next to the argument names in the long and short help messages.
Off to a good start
We have just started to scratch the surface of what swift-argument-parser can do for us. In the next part, we will cover options and flags.