The Swift Package Manager provides an out-of-the-box, strongly-typed argument parser that can be used without adding other third party dependencies.

Using ArgumentParser

Configuration

First, we need to add the Utility framework included in Swift Package Manager as a dependency to our project:

// swift-tools-version:4.0
import PackageDescription

let package = Package(
    name: "mycommandlinetool",
    dependencies: [
        .package(url: "https://github.com/apple/swift-package-manager.git", from: "0.1.0"),
    ],
    targets: [
        .target(name: "mycommandlinetool", dependencies: ["Utility"]),
    ]
)

Then update our project:

$ swift package update
Fetching https://github.com/apple/swift-package-manager.git
Cloning https://github.com/apple/swift-package-manager.git
Resolving https://github.com/apple/swift-package-manager.git at 0.1.0

$ swift package generate-xcodeproj
generated: ./mycommandlinetool.xcodeproj

Initial Parsing and --help

Now, we can import the Utility framework and proceed to parse the arguments:

import Foundation
import Utility

// The first argument is always the executable, drop it
let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())

let parser = ArgumentParser(usage: "<options>", overview: "This is what this tool is for")
let parsedArguments = try parser.parse(arguments)

Then we can run the --help command to see the usage and overview:

$ swift run mycommandlinetool --help
OVERVIEW: This is what this tool is for

USAGE: mycommandlinetool <options>

Strongly-Typed Arguments

ArgumentParser supports strongly-typed arguments, as follows:

let number: OptionArgument<Int> = parser.add(option: "--number", shortName: "-n", kind: Int.self, usage: "A number to compute")

The help command would now print:

$ swift run mycommandlinetool --help
OVERVIEW: This is what this tool is for

USAGE: mycommandlinetool <options>

OPTIONS:
  --number, -n
             A number to compute
  --help     Display available options

To get the argument value, we can simply check with if let:

if let integer = parsedArguments.get(number) {
    print("Your number is \(integer)")
}

And run it like:

$ swift run mycommandlinetool --number 6
Your number is 6
$ swift run mycommandlinetool -n 6
Your number is 6

For command line flags, we can use boolean options:

let uppercased: OptionArgument<Bool> = parser.add(option: "--uppercased", kind: Bool.self)

And run it like:

$ swift run mycommandlinetool -n 6 --uppercased
YOUR NUMBER IS 6

Handling parse errors

ArgumentParser will raise a exception when arguments cannot be parsed as expected. Typical thrown errors are:

  • Unexpected positional parameters or options are found
  • Argument type does not match the option

When an exception is thrown at the top level, the Swift command line tool will be terminated with a non-very-friendly error message like this:

$ swift run mycommandlinetool -n foo
Fatal error: Error raised at top level: 'foo' is not convertible to Int for argument --number; use --help to print usage: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.74.1/src/swift/stdlib/public/core/ErrorType.swift, line 187
Illegal instruction: 4

ArgumentParser exceptions can be easily cached as follows:

do {
    let parsedArguments = try parser.parse(arguments)
    processArguments(arguments: parsedArguments)
}
catch let error as ArgumentParserError {
    print(error.description)
}
catch let error {
    print(error.localizedDescription)
}

Which would print a more user-friendly error message.

$ swift run mycommandlinetool -n foo
'foo' is not convertible to Int for argument --number; use --help to print usage

Full source code

import Foundation
import Utility

// The first argument is always the executable, drop it
let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())

let parser = ArgumentParser(usage: "<options>", overview: "This is what this tool is for")
let number: OptionArgument<Int> = parser.add(option: "--number", shortName: "-n", kind: Int.self, usage: "A number to compute")
let uppercased: OptionArgument<Bool> = parser.add(option: "--uppercased", kind: Bool.self)

func processArguments(arguments: ArgumentParser.Result) {
    if let integer = arguments.get(number) {
        let message = "Your number is \(integer)"
        if arguments.get(uppercased) == true {
            print(message.uppercased())
        } else {
            print(message)
        }
    }
}

do {
    let parsedArguments = try parser.parse(arguments)
    processArguments(arguments: parsedArguments)
}
catch let error as ArgumentParserError {
    print(error.description)
}
catch let error {
    print(error.localizedDescription)
}

Command Line Tools Series