DEV Community

Maarek
Maarek

Posted on

Create your own command line tools in Swift

Swift is certainly a great programming language for developing apps made for the Apple ecosystem. But as you know, Swift, as a language, is not Apple specific. You can use Swift in many other areas such as backend (with Vapor). Swift runs on Linux and Windows too.
Today I would like to present a quick "Getting started" post about how to make your own command line tools with Swift.

As a developer there is many tasks that I'm sure you have already made your own tools for, to automate them.
Cropping images, scrapping stuff from the internet, generating all kind of files or config stuff... You name it.
Today I'll try to demonstrate how to create your own tool using Swift, the Swift package manager, and the argument parser.

Building a command line tool using the Swift Package Manager

I find very useful to have an image comparison tool on my computer. I have a lot of UITests on my apps and while the tests are running, they automatically take screenshots of the screen. There are many use-cases such as upload them to iTunes Connect, but I also use it to compare screens over time and over versions of my apps. If I see any unexpected difference between my "Base" screenshots and the one taken while testing, I know something is wrong with my UI.

So today we'll be building an image comparison command line tool

Let's start by creating the project :

$ mkdir img_cmp
$ cd img_cmp
$ swift package init --type executable
Enter fullscreen mode Exit fullscreen mode

Nothing crazy here; I create an img_cmp directory for my project and initilize the project.
A folder structure hase been created for me :

Creating executable package: img_cmp
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/img_cmp/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/img_cmpTests/
Creating Tests/img_cmpTests/img_cmpTests.swift
Creating Tests/img_cmpTests/XCTestManifests.swift
Enter fullscreen mode Exit fullscreen mode

Awesome, now you can build and run the project with command line :

swift build
swift run
Enter fullscreen mode Exit fullscreen mode

If you're on a macOS machine and have XCode installed, you can double click on Packaged.swift, it will open the project.
Otherwise you can simply open Sources/img_cmp/main.swift in you favorite text editor.

Now we are going to import the Argument Parser. This is a Swift package made by Apple to get simple user input via arguments while running the program.

In your Package.swift file, you will add the dependancy as follow :

let package = Package(
    name: "img_cmp",
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"),
    ],
    targets: [
        .target(
            name: "img_cmp",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ]),
        .testTarget(
            name: "img_cmpTests",
            dependencies: ["img_cmp"]),
    ]
)
Enter fullscreen mode Exit fullscreen mode

Now you have added the Argument Parser framework. If you are on XCode, it will automatically update the dependancy package and resolve the dependancy versions.

In the main.swift, we can now import the framework :

import ArgumentParser
Enter fullscreen mode Exit fullscreen mode

Creating the program

We need to start by creating a parsable command. Nothing simpler: make a struct that conforms to ParsableCommand, a protocol defined in the Argument Parser framework.
Here I'll call it ImgCmp.
After that, we'll call the static main function, that will take care of running the program for me.

struct ImgCmp: ParsableCommand {

}
ImgCmp.main()
Enter fullscreen mode Exit fullscreen mode

Great, now to make our program do something, I will implement the function run in our struct (also defined in the protocol ParsableCommand). This will be the method used to execute the command.

struct ImgCmp: ParsableCommand {
    func run() throws { }
}
ImgCmp.main()
Enter fullscreen mode Exit fullscreen mode

Notice this command can throw error, so the OS can catch the exit status of your program.

Get all the arguments !

Alright now we want the user to input two images to compare one to another. So we'll need (at least) 2 arguments.
The ArgumentParser let use input 3 type of arguments :

  • Arguments
    That's a required input needed for the program to run.

  • Options
    An option is an extra value that the user can input for a specific behaviour.

  • Flag
    A Flag is a simple option that the user can add or not.

Example :

nano -L /path/to/file --tabsize=1
Enter fullscreen mode Exit fullscreen mode

Here we use the command nano which is used to edit a file.
-L is a flag (Don't add newlines to the ends of files).
/path/to/file is an argument (The file to edit).
--tabsize=1 is an option (Set the size (width) of a tab to cols columns).

To parse argument, the framework makes everything for us.
It's based on property wrappers - that you might have used with SwiftUI and it's pretty simple :
Every argument is an attribute of the struct (here ImgCmp) and the propery wrapper takes parameters to define and specify the argument.
Let's start with our two main arguments :

@Argument(help: "The reference image.")
var base: String

@Argument(help: "The image to compare.")
var image: String
Enter fullscreen mode Exit fullscreen mode

Now if you run the command without two arguments it should say:

$ img_cmp swift run img_cmp
Error: Missing expected argument '<base>'

USAGE: img-cmp <base> <image>

ARGUMENTS:
  <base>                  The reference image. 
  <image>                 The image to compare. 

OPTIONS:
  -h, --help              Show help information.
Enter fullscreen mode Exit fullscreen mode

But add some strings and you're good :

$ img_cmp swift run img_cmp /Users/me/Desktop/a.png /Users/me/Desktop/b.png
Enter fullscreen mode Exit fullscreen mode

Now for me, this is not enough. I want the user to be able to specify a tolerance for comparing. Say if A is 99% B, it's okay for me.
For that we'll add an option

@Option(name: [.customLong("tolerance"), .customShort("t")], help: "The tolerance to consider an image identical as a value from 0 to 1.\n 0 is strictly identical.")
    var tolerence: Float?
Enter fullscreen mode Exit fullscreen mode

With the Option property wrapper, you can specify a long and a short name (here "tolerance" and "t").
Now we can use execute :

$ img_cmp swift run img_cmp /Users/me/Desktop/a.png /Users/me/Desktop/b.png -t 0.01
Enter fullscreen mode Exit fullscreen mode

Let's also add a verbose flag. If the user would like to display more information and non blocking messages. A basic "-v" will work for me.

@Flag(name: [.customLong("verbose"), .customShort("v")], help: "Show logs, information and non blocking messages.")
var verbose = false
Enter fullscreen mode Exit fullscreen mode

Back the to run function : to exit a program, just throw an ExitCode (.success, .failure...). Just add an exit statement to the function : throw ExitCode.success

There you go!

Now I wont (unless you ask for it in the comment) write about the program's body because it's kind of out of subject (and in real life, you would probably use imagemagick ^^), but here how your code should look like so far:

import ArgumentParser

struct ImgCmp: ParsableCommand {

    @Argument(help: "The reference image.")
    var base: String

    @Argument(help: "The image to compare.")
    var image: String

    @Flag(name: [.customLong("verbose"), .customShort("v")], help: "Show logs, information and non blocking messages.")
    var verbose = false

    @Option(name: [.customLong("tolerance"), .customShort("t")], help: "The tolerance to consider an image identical, as a value from 0 to 1.\n 0 is stricly identical.")
    var tolerance: Float?

    func run() throws {
        // here you would compare the images, log stuff and return the right status code.
        throw ExitCode.success
    }
}

ImgCmp.main()
Enter fullscreen mode Exit fullscreen mode

I hope you enjoyed this simple post. Don't hestitate to ask any question in the comment section, I'll be glad to help :).

Happy coding!

Top comments (0)