Swift Argument Parser Example
Command-line Swift tools need to convert command-line arguments into constants that can be accessed at runtime. The "swift-argument-parser" package at https://github.com/apple/swift-argument-parser makes this easy. It also automatically generates a --help option listing the full recognized syntax.
To experiment, in Xcode create a new project: "File -> New Project -> macOS -> Application -> Command-Line-Tool"
Then use "File -> Swift Packages -> Add Package Dependency". Paste in https://github.com/apple/swift-argument-parser into the package repository URL and click "next".
For my "Swift Telnet" project, I have two arguments:
- The host to connect to (required)
- The port to connect to (with a default of 23 if not specified).
I created a new "TelnetOptions.swift" file, defining a struct which conforms to the ParsableArguments protocol defined in the "ArgumentParser" import:
import Foundation
import ArgumentParser
struct TelnetOptions: ParsableArguments {
@Argument var host: String
@Argument var port: Int = 23
}
The host argument must be entered by the user or the program will print a help message and exit.
The port argument does not need to be specified by the user. In that case, ArgumentParser will set the port to 23.
Near the beginning of my main.swift file, I call the static parseOrExit() function (defined in the ParsableArguments protocol):
let options = TelnetOptions.parseOrExit()
Finding the binary to test from the command-line can be inconvenient. Here is my procedure:
- Use the Xcode "Product -> Build for Running" menu option.
- Control-click on the Product binary (telnetswift in my case) in the Xcode navigator and "Show in Finder".
- In the finder, copy the binary to get its full path.
- In a terminal window, type "cd ", then paste the binary full path, then backspace over the binary name to cd to the directory.
Then, on the command-line, you can run your binary with the -h option to see the syntax the argument parser expects. You will probably need to prepend "./" to your command because . is not usually in your search path.
droot@MacBookPro2019 Debug % ./telnetswift -h
USAGE: telnet <host> [<port>]
ARGUMENTS:
<host>
<port> (default: 23)
OPTIONS:
-h, --help Show help information.
Once the argument parser help confirms your desired syntax, you can access your arguments in the remainder of your main.swift file:
reader = try TcpReader(hostname: options.host, port: options.port)
More advanced argument parsing syntax is documented at https://github.com/apple/swift-argument-parser/tree/main/Documentation
My Swift telnet implementation is still under development. It is open-source under the MIT license at https://github.com/darrellroot/telnetswift
A more complicated swift-argument-parser example
My Netrek-Server-Swift project https://github.com/darrellroot/netrek-server-swift/tree/master/Sources/netrek-server-swift demonstrates more advanced swift-argument-parser functionality.
This is a MacOS command-line project which uses swift-argument-parser as a package dependency.
The main.swift file triggers the parsing in the normal way:
let netrekOptions = NetrekOptions.parseOrExit()
Netrek-server-swift supports flags. We set the flag "debug" to false by default, but allow the user to set it to true. We also set a custom --help message:
@Flag(help: "Enables debug logging. Warning: fills up filesystems in a few hours.")
var debug = false
Here is the corresponding --help output for that flag:
--debug Enables debug logging. Warning: fills up filesystems in a few hours.
We can give a String a default value, but allow the user to optionally override it:
@Option(help: "The directory to store netrek log files and user database.")
var directory: String = "/tmp/netrek"
Resulting in the corresponding --help output:
--directory <directory> The directory to store netrek log files and user database. (default:
/tmp/netrek)
We can use an optional String, which is only non-nil if the user sets that option:
@Option(help: "The fully qualified domainname of this netrek server for the metaserver to display")
var domainName: String?
Which results in this syntax:
--domain-name <domain-name>
The fully qualified domainname of this netrek server for the metaserver to
display
We can have mutually exclusive option flags, using an enumeration conforming to the EnumerableFlag protocol. In this case, we set the default to .empire.
enum GameStyle: String, EnumerableFlag {
case bronco
case empire
}
@Flag(help: "Specifies the Netrek Game Style.")
var gameStyle: GameStyle = .empire
--bronco/--empire Specifies the Netrek Game Style. (default: empire)
Here is the entire NetrekOptions.swift file:
import Foundation
import ArgumentParser
enum GameStyle: String, EnumerableFlag {
case bronco
case empire
}
struct NetrekOptions: ParsableCommand {
@Option(help: "The fully qualified domainname of this netrek server for the metaserver to display")
var domainName: String?
@Option(help: "The directory to store netrek log files and user database.")
var directory: String = "/tmp/netrek"
@Flag(help: "Enables debug logging. Warning: fills up filesystems in a few hours.")
var debug = false
@Flag(help: "Specifies the Netrek Game Style.")
var gameStyle: GameStyle = .empire
}
Which results in the following --help syntax output:
% ./netrek-server-swift --help
USAGE: netrek-options [--domain-name <domain-name>] [--directory <directory>] [--debug] [--bronco] [--empire]
OPTIONS:
--domain-name <domain-name>
The fully qualified domainname of this netrek server for the metaserver to
display
--directory <directory> The directory to store netrek log files and user database. (default:
/tmp/netrek)
--debug Enables debug logging. Warning: fills up filesystems in a few hours.
--bronco/--empire Specifies the Netrek Game Style. (default: empire)
-h, --help Show help information.
Here are examples of accessing our arugments later in our program. netrekOptions.directory and netrekOptions.debug are not optional, so we can rely on them being set. netrekOptions.domainName is optional, so we should use optional unwrapping to access the variable.
var url = URL(fileURLWithPath: netrekOptions.directory)
if netrekOptions.debug {
guard let domainName = netrekOptions.domainName else {
swift-argument-parser makes parsing arguments easy. It also guarantees that the --help output is consistent with the programmatic logic. I highly recommend this package for Swift CLI programs.