Calculator

Parsing arguments in a command line application is not a trivial task. Over time, applications can evolve to support many features, which makes the number of arguments supported grow considerably.

For example, the impressive curl command line tool has 207 different argument options, including parameters and flags.

This large amount of options makes curl very flexible and powerful. However, it also makes it hard to use; hard to learn and master.

Newer command line tools, like git, gem, carthage or travis, do a better job organize their features by grouping them in subcommands.

Examples:

  • git clone, git fetch, git branch, git commit, git push
  • gem install, gem update, gem build
  • carthage update, carthage build, carthage archive
  • travis login, travis monitor, travis status

Let’s see how to create a command line tool that supports these types of subcommands, by using the out-of-the-box ArgumentParser class from Swift Package Manager.

Project Setup

We are going to write a very basic command line calculator to compute basic math operations. Here is the proposed interface:

  • Addition: calculator add <x> <y> ...
  • Subtraction: calculator subtract <x> <y> ...
  • Multiplication: calculator multiply <x> <y> ...
  • Division: calculator divide <x> <y> ...

First, we create our new tool:

$ mkdir calculator && cd calculator
$ swift package init --type executable
$ swift run calculator
Hello, world!

Add Swift Package Manager as a dependency on Package.swift:

// swift-tools-version:4.0
import PackageDescription

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

And generate the Xcode project:

$ swift package generate-xcodeproj
$ open calculator.xcodeproj

Command Protocol

Our commands will be based on the subparser feature provided by ArgumentParser.

public func add(subparser command: String, overview: String) -> ArgumentParser

This feature, allows to create nested “parsers” that can handle arguments at their corresponding level.

Subparsers are defined by their command name and overview. We can extrapolate this information into a protocol. In order to register our commands, we will initialize them with the main ArgumentParser instance. Finally, we will place the main command execution code inside a run method.

protocol Command {
    let command: String
    let overview: String

    init(parser: ArgumentParser)
    func run(with arguments: ArgumentParser.Result) throws
}

We can then create our first command:

struct AdditionCommand: Command {

    let command = "add"
    let overview = "Compute the sum of all the numbers."

    init(parser: ArgumentParser) {
        parser.add(subparser: command, overview: overview)
    }

    func run(with arguments: ArgumentParser.Result) throws {
        //
    }

}

Command Parameters

A calculator would be useless if we couldn’t pass in some numbers to operate with.

In this case, we are looking a positional argument, of type integer, that can contain one or more values (an array).

private let numbers: PositionalArgument<[Int]>

init(parser: ArgumentParser) {
    let subparser = parser.add(subparser: command, overview: overview)
    numbers = subparser.add(positional: "numbers", kind: [Int].self,
                            usage: "List of numbers to operate with.")
}

This will allow us to pass any number of integers to compute the sum:

$ calculator add 1 2 3 4
10

Now we can compute the sum with reduce within our run method:

func run(with arguments: ArgumentParser.Result) throws {
    guard let integers = arguments.get(numbers) else {
        return
    }
    let result = integers.reduce(0, +)
    print(result)
}

🛠 Full code of AdditionCommand.swift:

import Foundation
import Utility
import Basic

struct AdditionCommand: Command {

    let command = "add"
    let overview = "Compute the sum of all the numbers."

    private let numbers: PositionalArgument<[Int]>

    init(parser: ArgumentParser) {
        let subparser = parser.add(subparser: command, overview: overview)
        numbers = subparser.add(positional: "numbers", kind: [Int].self,
                                usage: "List of numbers to operate with.")
    }

    func run(with arguments: ArgumentParser.Result) throws {
        guard let integers = arguments.get(numbers) else {
            return
        }
        let result = integers.reduce(0, +)
        print(result)
    }

}

Command Registry

Now that we can create commands, we want to easily register them so ArgumentParser can help us determine which command to execute.

To do this, we are going to create a command registry, that will keep track of our commands. All we need to keep track of is an array of commands, and an instance of the main ArgumentParser class.

struct CommandRegistry {

    private var commands: [Command] = []

    mutating func register(command: Command.Type) {
        commands.append(command.init(parser: parser))
    }
}

To keep out main.swift clean and tidy, we are going to have our CommandRegistry handle the following tasks:

  • parse the arguments
  • determine which command to execute
  • execute the command by calling run()

First, we are going to create the main ArgumentParser:

private let parser: ArgumentParser

init(usage: String, overview: String) {
    parser = ArgumentParser(usage: usage, overview: overview)
}

This method will parse the command line arguments (only needs to be called once):

private func parse() throws -> ArgumentParser.Result {
    let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())
    return try parser.parse(arguments)
}

We can determine the command to run by calling the subparser() method in the parsed arguments results and getting the corresponding command from our registry:

private func process(arguments: ArgumentParser.Result) throws {
    guard let subparser = arguments.subparser(parser),
        let command = commands.first(where: { $0.command == subparser }) else {
        parser.printUsage(on: stdoutStream)
        return
    }
    try command.run(with: arguments)
}

ArgumentParser includes a handy printUsage method that we use here, for cases where the user entered an unknown command.

🛠 Full code of CommandRegistry.swift:

import Foundation
import Utility
import Basic

struct CommandRegistry {

    private let parser: ArgumentParser
    private var commands: [Command] = []

    init(usage: String, overview: String) {
        parser = ArgumentParser(usage: usage, overview: overview)
    }

    mutating func register(command: Command.Type) {
        commands.append(command.init(parser: parser))
    }

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

    private func parse() throws -> ArgumentParser.Result {
        let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst())
        return try parser.parse(arguments)
    }

    private func process(arguments: ArgumentParser.Result) throws {
        guard let subparser = arguments.subparser(parser),
            let command = commands.first(where: { $0.command == subparser }) else {
            parser.printUsage(on: stdoutStream)
            return
        }
        try command.run(with: arguments)
    }

}

Main.swift

We have managed to encapsulate the command execution code in our command classes, while the main argument parsing code is handled by the command registry.

This leaves us with a very slim main.swift file, really easy to maintain and expand.

var registry = CommandRegistry(usage: "<command> <options>", overview: "Basic Calculator")

registry.register(command: AdditionCommand.self)

registry.run()

Running our command generates the pretty usage output:

$ swift run calculator
OVERVIEW: Basic Calculator

USAGE: calculator <command> <options>

SUBCOMMANDS:
  add                     Compute the sum of all the numbers.
$ swift run calculator add --help
OVERVIEW: Compute the sum of all the numbers.

POSITIONAL ARGUMENTS:
  numbers   List of numbers to operate with.

We can test it with passing some ints:

$ swift run calculator add 400 600
1000
$ .build/debug/calculator add 1 2 3 4 5 6 7 8 9 10
55
$ .build/debug/calculator add 40000000000000000000000 1
'40000000000000000000000' is not convertible to Int for argument numbers; use --help to print usage

Subtraction, Multiplication and Division

🛠 Full code of SubtractionCommand.swift:

import Foundation
import Utility
import Basic

struct SubtractionCommand: Command {

    let command = "subtract"
    let overview = "Compute the difference of all the numbers."

    private let numbers: PositionalArgument<[Int]>

    init(parser: ArgumentParser) {
        let subparser = parser.add(subparser: command, overview: overview)
        numbers = subparser.add(positional: "numbers", kind: [Int].self,
                                usage: "List of numbers to operate with.")
    }

    func run(with arguments: ArgumentParser.Result) throws {
        guard var integers = arguments.get(numbers) else {
            return
        }
        let first = integers.removeFirst()
        let result = integers.reduce(first, -)
        print(result)
    }

}

🛠 Full code of MultiplicationCommand.swift:

import Foundation
import Utility
import Basic

struct MultiplicationCommand: Command {

    let command = "multiply"
    let overview = "Compute the product of all the numbers."

    private let numbers: PositionalArgument<[Int]>

    init(parser: ArgumentParser) {
        let subparser = parser.add(subparser: command, overview: overview)
        numbers = subparser.add(positional: "numbers", kind: [Int].self,
                                usage: "List of numbers to operate with.")
    }

    func run(with arguments: ArgumentParser.Result) throws {
        guard let integers = arguments.get(numbers) else {
            return
        }
        let result = integers.reduce(1, *)
        print(result)
    }

}

🛠 Full code of DivisionCommand.swift:

import Foundation
import Utility
import Basic

struct DivisionCommand: Command {

    let command = "divide"
    let overview = "Compute the division of all the numbers."

    private let numbers: PositionalArgument<[Int]>

    init(parser: ArgumentParser) {
        let subparser = parser.add(subparser: command, overview: overview)
        numbers = subparser.add(positional: "numbers", kind: [Int].self,
                                usage: "List of numbers to operate with.")
    }

    func run(with arguments: ArgumentParser.Result) throws {
        guard var integers = arguments.get(numbers) else {
            return
        }
        let first = integers.removeFirst()
        let result = integers.reduce(first, /)
        print(result)
    }

}

Our updated main.swift:

var registry = CommandRegistry(usage: "<command> <options>", overview: "Basic Calculator")

registry.register(command: AdditionCommand.self)
registry.register(command: SubtractionCommand.self)
registry.register(command: MultiplicationCommand.self)
registry.register(command: DivisionCommand.self)

registry.run()

Running our tool with no parameters outputs the updated usage overview:

$ swift run calculator
OVERVIEW: Basic Calculator

USAGE: calculator <command> <options>

SUBCOMMANDS:
  add                     Compute the sum of all the numbers.
  divide                  Compute the division of all the numbers.
  multiply                Compute the product of all the numbers.
  subtract                Compute the difference of all the numbers.

Some tests 💯:

$ swift run calculator subtract 10 7 7
-4
$ swift run calculator multiply 1 2 3 4 5
120
$ swift run calculator divide 20 5 2
2

And of course:

$ swift run calculator divide 20 0
Fatal error: Remainder of or division by zero
Illegal instruction: 4

😅

Summary

As you can see, Swift Package Manager includes out-of-the-box a great set of powerful tools. ArgumentParser is an excellent parser for command line arguments.

Command Line Tools Series