Episode #368

Scripting in Swift with Marathon - Part 1

35 minutes
Published on December 19, 2018

This video is only available to subscribers. Get access to this video and 584 others.

Usually I lean on Ruby or Bash for writing command line scripts, but it is becoming increasingly more viable to use Swift for this as well. In the Vapor series, I wanted to write a little script (in Swift) that would generate migration files for me so I wouldn’t have to maintain this myself. For this, I used the Marathon tool, which helps alleviate some of the machinery necessary to use Swift in this way. And what better way to explore this tool than with this author guiding me along. John Sundell joins me in this episode to use Marathon and Swift to write a useful script for Vapor applications. This is a longer episode, so it is split into two parts. Enjoy!

Thanks to John Sundell for joining me on this episode!

Continued in Part 2

Episode Links

Installing Marathon

I followed the recommended instructions, which involves installing Mint first (😂):

 $ brew install mint

Now that Mint is installed, use it to install Marathon:

 $ mint run JohnSundell/Marathon

Writing Your first script

$ marathon create AddMigration

This creates an Xcode project in Marathon's support folder, creates the Swift package structure and links in the swift file for us to edit.

Any time we want to edit this file, we have to do so using Marathon:

$ marathon edit AddMigration

Reading Command Line Arguments

One of the first things we have to do is read command line arguments. Luckily, Foundation already has us covered there:

import Foundation

print(CommandLine.arguments)

This will print out a single argument, which is the full path to the script being run.

If you want to provide some arguments when debugging in Xcode, you can do so by editing the Xcode scheme.

Running Marathon Scripts

Running the scripts also is done via Marathon:

$ marathon run AddMigration <your> <arguments>

Working with Files

We want to work with the filesystem in this example, so we'll pull in John's library for this, helpfully called "Files". We'll need to tell Marathon keep track of the dependencies that we might want to use. We can do this in multiple ways.

The first way is to add the package as a known dependency with Marathon:

$ marathon add https://github.com/JohnSundell/Files.git

(Later when we want to update Marathon's copy of this, we can run marathon update ...)

This adds a cached copy of the dependency in Marathon's support folder, so we can now just import it in our script:

import Foundation
import Files

let folder = Folder.current
print("Your running from \(folder.name)")

If you run this with Xcode, the output will look something like this:

You're running from Debug
Program ended with exit code: 0

The folder name is just the single name, if you want the full path, you can use folder.path:

You're running from /Users/ben/Library/Developer/Xcode/DerivedData/example-ccknwhxumzpmtsgmrdeepmzbsfnf/Build/Products/Debug/
Program ended with exit code: 0

To create or retrieve a folder, you can use methods on the Folder type:

let folder = Folder.current
let subfolder = try folder.createSubfolderIfNeeded(withName: "MySubfolder")

This would create the folder if it didn't exist already

Exiting from scripts with errors

See how Xcode noted that the program ended with exit code 0? This is a Unix convention. "0" means successful, anything non-zero is an error. You can use this to communicate error codes, but it is also useful for chaining together commands:

first_run_this && then_run_that

If the first command returns a non-zero exit code, then the 2nd script will not run.

We want to honor the Unix convention of returning a non-zero value if we encounter an error.

if CommandLine.arguments.count < 2 {
    print("Usage: marathon run myScript <arg>")
    exit(1)
}

If we try to run this script and don't provide an argument, then the CommandLine.arguments array will only contain 1 value: the path to the script. We then print out a helpful message and exit with the value "1", indicating an error.

This episode uses Swift 4.2, Marathon 3.1.0, Vapor 3.0.8, Xcode 10.1.