Command Line in Swift: the Different Alternatives

Published: July 2, 2024
Written by:
Natan Rolnik
Natan Rolnik

When building a command line tool using Swift, there are a few different approaches you can choose. Although you can migrate from one to another with some refactoring, the goal of this post is to compare the pros and cons of each solution, to help you decide what’s the best fit for your use case.

The most common paths you can choose are:

  • a script as a single Swift, without dependencies
  • a script, but with dependencies using swift sh
  • an executable target via SPM (with or without the argument parser)

The following sections will bring arguments (pun intended 🥁) with the advantages and disadvantages of each one, as well as how to run or build.

script.swift

The easiest way to get started with a command line tool in Swift is by writing a single file:

import Foundation

let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
print(dateFormatter.string(from: Date()))

This code will print the current date and time, with your machine’s current locale.

You can try this yourself: create a new file named now.swift with the code above. To make it faster, click the copy button, open Terminal and write: pbpaste > now.swift. This will paste the copied content and save it to a file in the current working directory. To execute this code, run swift now.swift. You should see the current date printed in the console.

To support reading arguments in a script, use the CommandLine and its static property arguments:

if CommandLine.arguments.contains("--verbose") {
    print("Current locale is (Locale.current.identifier)")
}

Run the script with the --verbose flag: swift now.swift --verbose, and you will see that it prints the current locale.

The advantage of this approach is that it’s very simple and fast to get started. Having Swift installed in the machine running it is enough.

However, with this comes also its limitations and disadvantages: it doesn’t support adding package dependencies. Your script might get too long, or you’ll find yourself adding similar functions across different scripts. To add dependencies, your script will have to become a compiled executable - or at least a target that the Swift Package Manager (SPM) can build and run.

swift sh

Between the simple script approach and using SPM directly, there is also a tool called swift sh, by Max Howell. It uses SPM behind the scenes to link packages and execute your code, while keeping the simplicity of a script. To install it with homebrew, run brew install swift-sh.

Now, importing a dependency doesn’t even require writing a Package.swift file:

import Foundation
import XMLCoder // CoreOffice/XMLCoder

struct Feed: Codable {
    let title: String
    let entry: [Entry]

    struct Entry: Codable {
        let title: String
    }
}

let url = URL(string: "https://swifttoolkit.dev/feed.xml")!
do {
    let (data, _) = try await URLSession.shared.data(from: url)
    let feed = try XMLDecoder().decode(Feed.self, from: data)
    print(feed.title)
    print(feed.entry.suffix(3).map(.title).joined(separator: "\n"))
} catch {
    print(error)
}

This script will fetch the XML feed of SwiftToolkit.dev, parse it, and print the title of the last 3 posts.

In the second line of this script, notice the import of XMLCoder, a package for encoding and decoding XML data. The content of the comment right after it tells swift sh where to find the package at. Check the README to see all different options.

If you copy and execute this script (for the exercise, save it as posts.swift), you can run it with the following command:

swift sh posts.swift

In the first run, it will take a few seconds for swift sh to do its magic, to fetch the package dependencies, build and run the script. After the first run it should be faster.

Due to the lack of completion and the swift sh edit command not working in recent Xcode versions, it might be hard to work on scripts with swift sh. Although it is as a fast and simple approach to run scripts with dependencies, it might be hard to work on scripts that require constant changes and maintenance.

An SPM Executable Target

The next step on the scale of Swift tooling is creating an executable target with the Swift Package Manager:

swift package init --type executable

If you want a target name different than the default one, pass the --name argument:

swift package init --type executable --name MyTool

The init command will create a package in the current directory: a Package.swift file, where you can add your dependencies (or other targets), and also a main.swift file, which is the script itself.

Instead of a main.swift file, you can rename it and have an entry point using the @main attribute:

@main
struct MyTool {
    static func main() {
        print("Your tool starts here!")
    }
}

In a similar manner as explained in the script in the first section, accessing CommandLine.arguments allows your program to read the arguments passed when running the tool. But doing it manually might be error prone, and there is a better approach!

A Tool Using the Argument Parser

The state of the art in Swift programs is the Argument Parser, created and maintained by Apple itself. It provides property wrappers and many other handy types to create tools.

Being the “official” package, it is automatically added as a dependency when creating an executable with the tool type:

swift package init --type tool

Inside any ParsableCommand, you can use the property wrappers for Arguments, Flags, Options, and more goodies.

The advantages of building an executable are many:

  • your program is not constrained to a single file anymore
  • you can depend on packages (local, private or public)
  • you can distribute your tool without the source code

In the other hand, you might need to either distribute the source code or the compiled binary, potentially for different OSs (macOS, Linux or even Windows) and architectures (x86 and ARM).

Distributing binaries is a subject for another post!

Running with SPM

To build and run it, use the following command:

swift run

When you have only one target - which is the case initially - there is no need to specify the target name. When multiple targets exist in the same package, you append its name to the run command. Also, if you want to pass arguments, then the target name is required:

swift run MyTool --verbose

Compiling as Executable

To compile your target into an executable, use the build command:

swift build

To use the release configuration instead of DEBUG (the default one), pass the -c argument:

swift build -c release

Finally, you might want to know where your binary will be located at. To find out:

swift build -c release --show-bin-path

You can omit the -c release argument to see the debug path, or open the path in Finder to better understand the output file structure.


Explore Further

If you have any questions, comments, or suggestions, ping SwiftToolkit.dev at Mastodon or X.

When building programs with the Swift Argument Parser, check out the cheatsheet. It is a quick reference you can use when declaring targets and adding package and target dependencies.

We also have a 3 part series guide about the Swift Argument Parser.

See you at the next post. Have a good one!
Swift, Xcode, the Swift Package Manager, iOS, macOS, watchOS and Mac are trademarks of Apple Inc., registered in the U.S. and other countries.

© Swift Toolkit, 2024-2025