Swift for Command-Line Tools

Marin Usalji, creator of Alcatraz, xcpretty and ObjectiveRecord gave a great talk about using Swift for CLI tools at the SLUG meetup. He covered the current state of the language, CLI basics, and what needs to be done.

As usual, video & slides are synchronized. The video is also subtitled. You can find a blog version of the talk (with code samples) below.


This blogpost is adapted from Marin’s own notes.

Since its release, Swift has gained a decent traction as a language for building mobile / desktop apps. This talk will show the other side of Swift, it’s scripting capabilities and how you can use it for building CLI tools.

We will cover CLI basics, status codes, documentation, pipeline, and some real life usages. Looking in the future, can Swift dominate Ruby and bash as a tooling language?

Introduction

Evolution

If you were watching Swift from it’s early beginnings, you might have noticed that executing Swift code got significantly simpler.

From

/Applications/Xcode6-Beta1.app/Contents/Developer/usr/bin/xcrun swift -i script.swift

to

xcrun swift -i script.swift

to

swift -i script.swift

to

swift script.swift

It’s noticeable that the core team gives attention to Xcode-less development, and is trying to make Swift a first class CLI language.

‘Scripting’ vs Compiling

When we talk about scripting, we’re mostly referring to interpreted languages. What makes working with these a pleasure is the ability to quickly just open a file and execute it inline, like so: ruby script.rb. Languages like Objective-C usually need a bunch of compiler flags and a compilation step before being executed. This makes working with Ruby, Python, Javascript and similar languages a pleasure.

Fortunately, some languages like Go have set a good example how to achieve the scripting-style workflow, while staying compiled languages. To give you an example, you can execute a Go file with go run script.go. Go compiler will compile the file implicitly, and execute it in place.

Swift went in the Go direction - you can execute a Swift program inline, without knowing it’s being compiled and running a binary.

# Execute inline 
- $ swift todos.swift

The best part is that you can still compile a Swift script into a binary and ship it, just like Go programs.

# Compile and run 
- $ swiftc todos.swift -o todos 
- chmod +x todos 
- todos

CLI 101

Shebang

UNIX looks for the magic comment on the beginning of your script, called Shebang, Hashbang or even pound-bang. It contains a hash, exclamation mark and the interpreter command such as /usr/bin/ruby, or /usr/bin/env ruby.

When a script with a Shebang is run as a program, the program loader parses the rest of the script’s initial line as an interpreter directive.

chmod

To make the script executable, we need to give it the right permissions. Command chmod allows us to change file modes or Access Control Lists. chmod +x script.swift

PATH

If you want to use your Swift program from anywhere, you need to tell your OS where to find it. UNIX-like systems usually look at the PATH variable. Assuming we’ll put the compiled products in ~/bin, we need to prepend it to the standard PATH: export PATH=~/bin:$PATH.

Status codes

When a program terminates, it exits with a code. Zero usually means a successful exit, and everything else is considered as an error.

If you want to force exit with a code, you can use the built-in exit() function, e.g. exit(1).

One of the useful functions is checking the exit status of the last process terminated. It’s usually stored in the $? variable, but there’s no support for it in Swift yet.

Option parsing

Commands usually take parameters and flags in the same way functions take arguments. Passing them through your shell is pretty easy, but then comes the parsing part inside your script.

It’s generally a bad idea to do this all by hand; Some languages like Ruby have OptionParser built in the standard lib.

Frameworks:

POSIX flags

By the POSIX standard, flags should have a long and short version. Long flags should start with a double dash, and short ones with only one. For example, command generate should be able to take --view and -v and achieve the same thing. The cool thing about abbreviations is that you can chain them without adding dashes, like so: generate --view --controller should be the same as generate -vc. (the latter only works in Silicon Valley)

Documentation

Every CLI tool should have at least the --help documentation. There’s also man documentation if you want to document your tool extensively, but most of the smaller tools don’t have it. The cool thing is, if you use a library like OptionParser, it should generate the help documentation for you.

IO

IO is often the most important part of a CLI tool. File and IO modules give you file-system traversal, various file operations including permissions, and very importantly - accessing stdin, stdout and stderr input/outputs.

You’re mostly either:

  • spawning another proccess (popen), letting it do some work and listening to it’s outputs
  • being spawned and transforming the stdin of your own program

Pipeline

UNIX pipeline is a very flexible way of chaining multiple programs to work together without knowing about each other. A pipe connects stdout output of the first program with stdin of another. In the given example A | B, program A sends to stdout, and B receives into stdin. Output from stderr is always displayed straight into terminal, unless you explicitly redirect it to stdout, /dev/null, or something else.

String manipulation

When talking about transforming - Regular expressions (verbose and yucky in Foundation) - Simple and elegant in Ruby - split, reverse, join, char index, length,…

External libraries

For a CLI tool to be insanely cool and fun to build, you often connect some crazy parts together. Imagine building an alarm clock that also talks to your coffee machine through it’s API and requests a Lyft after you drink the coffee.

You should be able to use these libraries without effort, like with Ruby gems. Currently there’s CocoaPods, but this setup requires Xcode and you probably don’t want to deal with Xcode.

Remaining Pain Points in Swift

  • Option parser
  • Regex
  • String operations
  • BDD from command line
  • Simpler framework loading / linking
  • Distributing frameworks

Sources



Marin Usalj

Marin Usalj