BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

Apple's SPMUtility library will power up command-line apps

Try out the new code from the Swift Package Manager team

Paul Hudson       @twostraws

Apple has unveiled a new collection of open-source utility code for Swift developers, grown out of its Swift Package Manager project. The collection contains some interesting new data types (OrderedSet – hurray!), some tools to make command line programs easier to write, and some helpers for common tasks like temporary files and SHA hashing.

Apple describes the code with a big warning that bears repeating before we continue: “This product consists of unsupported, unstable API. These APIs are implementation details of the package manager. Depend on it at your own risk” Still, who could resist having a quick play? Let’s see what we have…

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Using the library

Start by creating a new Swift project:

  1. Launch Terminal.
  2. Run cd Desktop to change to your Desktop directory, if you aren’t there already.
  3. Run mkdir UtilityTest and cd UtilityTest to make a directory and change into it.
  4. Run swift package init --type=executable to generate a new command-line project.

Now modify open Package.swift in the UtilityTest directory and modify it to this:

// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

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

That brings in the Swift Package Manager project, which is where Apple’s new utility code lives for the time being.

Finally, run swift package generate-xcodeproj to download our lone dependency and generate an Xcode project. When that finishes, open it in Xcode, then open main.swift inside the Sources > UtilityTest folder, and give it these three imports:

import Foundation
import Basic
import SPMUtility

That’s the setup complete, so let’s try it out!

Ordered sets

First up, we have a new OrderedSet type that gives all the performance of sets without their annoying habit of forgetting the order you add things. The code for this might surprise you with its simplicity: it’s a data type that combines an array and a set so that one can be used for storing order while the other is used for look ups.

Here’s how a regular set looks:

let set = Set(["Anna", "Olaf", "Elsa", "Kristoff", "Anna"])

When printed out, that contains ["Elsa", "Kristoff", "Anna", "Olaf"] – all the order has been lost. Moving over to an ordered set is now trivial:

let set = OrderedSet(["Anna", "Olaf", "Elsa", "Kristoff", "Anna"])

Sorted arrays

Moving on, there’s a new SortedArray type that ensures new data is always insert into the correct alphabetical position. For example:

var array = SortedArray<String>()
array.insert("Hans")
array.insert("Kristoff")
array.insert("Anna")
array.insert("Sven")

When that code finishes, the array will contain Anna, Hans, Kristoff, Sven. (Some trivia for you: the names Hans, Kristoff, Anna, and Sven were chosen to honor the Danish author Hans Christian Andersen, who wrote the book Frozen was based on – the Snow Queen.)

Obviously SortedArray might cause a serious performance slowdown compared to using a regular array and sorting only once, so use it wisely.

Launching processes

Writing command line apps has its own set of complexities, particularly if you want them to be cross-platform across macOS and Linux. Fortunately, Apple’s new utility code takes away a lot of the pain of dealing with POSIX compliance, wrapping it all up in some great little helpers.

For example, a common task is to run another process, wait for its result, then take some action based on that result. Thanks to the new Process type, this is surprisingly easy – here’s some code to list your Applications directory:

do {
    let process = Process(arguments: ["ls", "-l", "/Applications"])
    try process.launch()
    let result = try process.waitUntilExit()
    let output = try result.utf8Output()
    print(output)
} catch {
    print("Error while reading the Applications directory")
}

You can also read errors (stderr), exit status, and more.

Working with temporary files

Another brilliantly simple helper is TemporaryFile: it’s a class that instantiates a new file you can write to freely, but as soon as your file object goes out of scope – e.g. your method returns or your loop ends – it gets deleted. (Note: it’s down to the operating system when the file actually gets deleted.)

In its most simple form here’s how TemporaryFile works:

let file = try TemporaryFile()
let data = Data("Hello, world!".utf8)
file.fileHandle.write(data)

You can also add your own prefix or suffix to the filename if you want, like this:

let file = try TemporaryFile(prefix: "hws")

And if you want to know where the file is being stored, you can read its path like this:

print(file.path.asString)

Managing versions

Another great little helper is Version, which is perfect for handling semver checks in your code. You can create a new Version instance from major, minor, and patch numbers, then compare instances, like this:

let currentVersion = Version(1, 5, 0)
let newestVersion = Version(2, 0, 1)

if currentVersion < newestVersion {
    print("Upgrade is available!")
}

You can also create versions from strings in the format “X.Y.Z-prereleasedata”, but this returns an optional:

if let version = Version(string: "2.0.0-alpha4") {
    print("You're running v\(version.major)")
}

Calculating SHA hashes

Finally, there’s a new SHA256 class for computing the SHA-2 hash of strings. While I wish it were a simple String extension, that’s just because I’m lazy – here’s how to use it:

let sha = SHA256("Time doesn't exist; clocks exist.")
print(sha.digestString())

Parsing arguments

There are a few common things any command-line app wants to do, and Apple’s utilities have gone a long way to making them easier.

First, there’s a new ArgumentParser class that’s responsible for reading arguments from the command-line and making them available to your program. You start by creating an instance of the class, like this:

let parser = ArgumentParser(commandName: "swearcheck", usage: "filename [--input naughty_words.txt]", overview: "Swearcheck checks a file of code for swearing, because let's face it: you were angry coding last night.")

That provides ArgumentParser with some basic information to show if the user runs the program using -h or --help. You can then go ahead and add positional or optional arguments, like this:

let input = parser.add(option: "--input", shortName: "-i", kind: String.self, usage: "A filename containing naughty words in your language", completion: .filename)
let filename = parser.add(positional: "filename", kind: String.self)

That first one will look for both “--input filename.txt” and “-i filename.txt”, whereas the second one will look just for a loose value to be read.

Once you’ve configured your parser you can go ahead and use it. Most of the time you’ll want to use the input from the command line (dropping the first item, because that contains the program name), like this:

let args = Array(CommandLine.arguments.dropFirst())
let result = try parser.parse(args)

Finally, you can read individual arguments by calling get() with the argument you created, like this:

if let wordsFilename = result.get(input) {
    print("Using \(wordsFilename) for the list of naughty words to check for.")
} else {
    print("Using 'JavaScript' in lieu of a list of naughty words.")
}

ArgumentParser will throw errors if values were missing or incorrect, so you should really wrap the whole thing in a do/catch block, like this:

do {
    let parser = ArgumentParser(commandName: "swearcheck", usage: "filename [--input naughty_words.txt]", overview: "Swearcheck checks a file of code for swearing, because let's face it: you were angry coding last night.")
    let input = parser.add(option: "--input", shortName: "-i", kind: String.self, usage: "A filename containing naughty words in your language", completion: .filename)
    let filename = parser.add(positional: "filename", kind: String.self)

    let args = Array(CommandLine.arguments.dropFirst())
    let result = try parser.parse(args)

    guard let codeFile = result.get(filename) else {
        throw ArgumentParserError.expectedArguments(parser, ["filename"])
    }

    print("Scanning \(codeFile) for sweary code…")

    if let wordsFilename = result.get(input) {
        print("Using \(wordsFilename) for the list of naughty words to check for.")
    } else {
        print("Using 'JavaScript' in lieu of a list of naughty words.")
    }
} catch ArgumentParserError.expectedValue(let value) {
    print("Missing value for argument \(value).")
} catch ArgumentParserError.expectedArguments(let parser, let stringArray) {
    print("Missing arguments: \(stringArray.joined()).")
} catch {
    print(error.localizedDescription)
}

Enhancing your terminal output

Moving on, the TerminalController class gives you more control over the terminal window, as you might have guessed from its name. This includes things like printing text in color, clearing the current line, and measuring the terminal width in characters.

To demonstrate this, we could write a simple text string user super cool colors, and by “super cool” I really mean “the kind of thing you wrote when you were 14”:

let colors: [TerminalController.Color] = [.red, .green, .yellow, .cyan, .white]

if let stdout = stdoutStream as? LocalFileOutputByteStream {
    let tc = TerminalController(stream: stdout)

    for (index, letter) in "Hello, world!".enumerated() {
        tc?.write(String(letter), inColor: colors[index % colors.count], bold: true)
    }
}

Beautiful.

Will it end up being separate?

This has been a pretty long article, and we’ve covered probably only about 25% of what the new library offers. Hopefully you can see there’s a heck of a lot here, and this was just me covering the highlights. That being said, it feels very much like some of these things could easily go on to Swift Evolution if Apple were so inclined – OrderedSet might be a quick win, for example!

Before you starting adding this code to big projects, remember Apple’s warning: it's unstable and unsupported, at least for now. While it’s interesting to see their direction of travel, and always great to see new open source code, keep in mind that it can change dramatically at any point in the future!

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.