Darrell Root on Networking and Swift

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:

  1. The host to connect to (required)
  2. 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:

  1. Use the Xcode "Product -> Build for Running" menu option.
  2. Control-click on the Product binary (telnetswift in my case) in the Xcode navigator and "Show in Finder". Show in finder
  3. In the finder, copy the binary to get its full path. Copy product
  4. In a terminal window, type "cd ", then paste the binary full path, then backspace over the binary name to cd to the directory.
Change 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.

Tagged with: