How To Write macOS Command Line Tools with Swift

I’ve spent a lot of time of late working on several macOS command line tools written in Swift. So I’ve gathered together the key points I’ve learned while creating and updating pdfmaker and imageprep: some best practices and ways to deliver many of the features common to programs the run at the command line.

imageprep running in Terminal

There are fair few guides of this kind on the internet, but most seem to focus on just getting a ‘Hello World’ appear in the Terminal. I’ve tried to pull together some more practical information.

Incidentally, the irony of doing this on a Mac, is not lost on me. We’re just months away from the 40th anniversary of the first commercially available graphic user interface, released with the Xerox Star, yet we have never stopped keying in commands. Truth be told, I resisted the command line for many years, focusing solely on the GUI as the modern way of computing. But there’s no doubt that for many tasks, especially those that benefit from scriptable automation, the command line is the way to go. It may not be as intuitive as a GUI, but it’s no less useful for that.

Start a Project

Xcode is already set up to allow you to create command line programs in Swift: just select the appropriate template when you create a new project.

Xcode makes command line tool creation straightforward

You will have your own preference for the way your code is organised, but I follow a basic structure of constants, globals, functions and then the code that is executed when the program is launched. Xcode provides a single file, main.swift, for all this, though you can break elements out into other files as you wish. There’s no main() function — your Swift code just runs from the top.

I’ll come back to code organisation when I talk about testing, but for now let’s just run through some key command-line tool components.

Trap keyboard interrupts

Update For more up-to-date info on trapping signals like ctrl-c/SIGINT, check out this post: Tackle async signal safety in Swift.

First, I set up a trap to catch attempts to interrupt the program. These are usually triggered when the user hits ctrlc to cancel an unwanted operation. Here’s the code:

signal(SIGINT) {
    theSignal in
        let bsp = String(UnicodeScalar(8))
        writeToStderr("(bsp)(bsp)\rapplication interrupted -- halting")
        exit(EXIT_FAILURE)
}

This Swift structure sets the code that will be be called when the application receives an interrupt signal — SIGINT. This code is a closure — code that can be safely called asynchronously — which recieves a single value into the variable theSignal. The closure just calls the application function writeToStderr(), which outputs the passed message to Standard Error, and then the Darwin function exit() to halt the program. SIGINT and EXIT_FAILURE are Darwin-set constants. You code’s import Foundation statement causes Darwin to be imported too.

The value of bsp is a backspace character, set this way because Swift doesn’t use the usual \b escape sequence. We issue two backspaces to rub out the ^C displayed in the terminal when the user hits ctrlc.

Read environment variables

With the interrupt handler in place, the next thing to do is read in any environment variables that your program needs to take note of. Swift has code to help you: the ProcessInfo class has a processInfo property which contains useful data about your running program: its environment property, a dictionary, allows you to query environment variables using their names as keys:

for (key, value) in ProcessInfo.processInfo.environment {
    print(key + " -> " + value)
}

Parsing arguments

The next thing to do is parse the program’s input. If no input is needed, that’s easy, but generally you want to pass values at the command line such as the location of a file you want the program to work with. Swift provides an enum, CommandLine, through which you can access these values through a property called arguments.

I check to see if no arguments have been passed: if that’s the case, I show the program’s help text and then exit. The first argument of any command line call is always the name of the command line program itself, so a check for one argument is the same as checking that the user entered nothing more than the name of the program.

The code that follows parses any arguments. It looks for specific short (eg. -s) and long (--source) versions of flags and switches. Flags are arguments that are followed by a value; switches are arguments whose presence is sufficient to set program state. For example, imageprep has the --source flag; this is followed by a path to a source file or directory. It also has --keep, which is not followed by a value and tells the program not to delete the source files at the end of the run.

Other arguments, like --help and --version, don’t set state but perform an action. It’s good practice to name long versions of a flag or switch to make clear their function. But it’s also important to include short versions to limit the amount of typing that knowledgeable users need to do.

You’ll check that flags are accompanied by a value, and that orphan values are handled properly too. You may want to treat the latter as legitimate input: a so-called ‘positional argument’ because the program relies on where it’s placed in the sequence of arguments to know how the value should be used. Positional arguments are very common — think of cp <source> <destination> — but unless your code has such straightforward inputs, it’s considered best practice to avoid positional arguments and use flagged ones instead.

Input values extracted and validated, your code can now go on and process them as per the logic of your specific application. However, there are some standard elements you should think about including.

Offer help

I’ve already mentioned help: it’s essential to include some text you can output that tells the user what the program does and what input it takes, ie. listing all its flags and switches (in short and long form). Make clear any default values that you apply. Provide some examples.

In addition to help, I have a function that outputs the program’s current version, to help users stay up to the date. This is called in response to --version, but I also call it when I display help information.

Route your output

Now we come to an important consideration: where should this information be output? It’s easy, as I did at first, to use Swift’s print() function to show not only your program’s help data but all output, but there’s a problem. Your program may output information for the user, such as errors, warnings and general information, but it may also output results. These can all be output using print() and they’ll all be displayed at the command line, but what if you want to pipe the results into another program for further processing, or to a file? In these cases you don’t want the computer-readable output ‘polluted’ with any human-oriented messages. That can lead to further errors.

Fortunately, the designers of Unix, the OS upon which macOS is ultimately based, figured out a way of dealing with this. There are two output channels: Standard Output and Standard Error. You send all computing results — you program’s true output — to the former, and route all messages, whether they’re errors or not, to Standard Error. How do we use these with Swift?

Foundation’s FileHandle class, that’s how. It has properties that reference each of these outputs:

let STD_ERR = FileHandle.standardError
let STD_OUT = FileHandle.standardOutput

Remember I mentioned a function called writeToStderr() earlier? I uses STD_ERR, as defined in the lines above, as follows:

func writeToStderr(_ message: String) {
    let messageAsString = message + "\r\n"
    if let messageAsData: Data = messageAsString.data(using: .utf8) {
        STD_ERR.write(messageAsData)
    }
}

Writing to a file handle works with data rather than strings, so I need to convert from one to the other. I also need to make sure every line has the correct carriage return and new line escapes. You’d do exactly the same with output destined for STD_OUT; you’d just change the name of the object whose write() method you call.

To the user viewing the operation of the program in Terminal, they will see all the messages and output appear as lines of text, just as if you had used print() for everything. But if make sure all your other messages get output with writeToStderr() or your own equivalent, your data output will include data and nothing more.

Colour your messages

Here’s imageprep’s error message handler:

func reportErrorAndExit(_ message: String, _ code: Int32 = EXIT_FAILURE) {
    writeToStderr(RED + BOLD + "ERROR " + RESET + message + " -- exiting")
    exit(code)
}

RED, BOLD and RESET are constants containing formatting data that colours part of the message. Outputting human-oriented text this way is a great way of making key messages stand out. Swift uses unicode scalar formatting for this, which I set up using constants.

let RED    = "\u{001B}[31m"
let YELLOW = "\u{001B}[33m"
let RESET  = "\u{001B}[0m"
let BOLD   = "\u{001B}[1m"
let ITALIC = "\u{001B}[3m"

The colour codes are as follows:

ColourValue
Black30
Red31
Green32
Yellow33
Blue34
Magenta35
Cyan36
White37

The format code are:

FormatValue
Normal0
Bold1
Dim2
Italic3
Underline4
Flash5

Test your code

I’ve been testing command-line programs with a series of test cases written within a shell script I created for the purpose. For GUI programs, I use Xcode’s own XCTest framework. It works very well with object-oriented code, but less well with code that isn’t based on classes — like my command line tools. Your code may be based on classes and that means integration with XCTest is straightforward. Alternatively, you might recast your existing code as a class, which your command line tool instantiates and then calls methods to process the input and generate the results.

Notarise your program

I use Apple’s software notarisation system to provide users with a degree of trust that my apps are safe to use. Getting GUI applications notarised is easy with Xcode, but it’s a little trickier for command line tools. Notarisation relies on the Mac standard app identification system: the presence of an Info.plist file within the app bundle. But a command line tool isn’t a bundle, so where do you put its Info.plist file?

You put it inside the app binary itself. Go to the Xcode target’s Build Settings and scrolling to Packaging section (or entering info.plist in the search field). You’ll see a Create Info.plist Section in Binary option — make sure this is set to Yes. This will add an Info.plist file to your project, which you can edit in the usual Xcode way. With the info.plist data compiled into your binary, you can get it notarised.

Incidentally, you can access the Info.plist data via the Bundle class in the code you write to display the program’s version number:

let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String

To notarise the compiled program, you need to use Xcode’s own command line tools — and you can find full details about the process in my post here.

To do: pipes

Currently, neither of the command line tools I’ve written in Swift work with the shell’s pipe command, using the symbol |, which uses the output of one command as the input of another. I originally thought this could be done with the following Swift:

let STD_IN = FileHandle.standardInput
if let input = STD_IN.availableData {
    . . .
}

However, availableData blocks until it receives an end-of-line. This isn’t a problem if there is data to be piped in, but if not, it waits until the user hits Enter. So I’m now exploring ways to receive data only if it’s there. I’ll cover the solution in a follow-up to this post, but I’m currently exploring how to do this asynchronously using code like this:

STD_IN.readabilityHandler = {
    handle in
        let newData = handle.availableData
        let dataStr = String(bytes: newData, encoding: .utf8) ?? ""
        print(dataStr)
}

Incidentally, if you just need to read text in from the console — for example, if you ask the user to enter a value — you can use the readline() function:

writeToStderr("Please enter some text:")
var input = "default"
if let instr = readline() {
    input = instr
}

You can view the source code for imageprep and pdfmaker at my GitHub site: