Swift Programming from the Command Line

Swift is the main programming language for Apple platforms. This article explores some ways of using this language from the command line.

The information here is intended for those who are familiar with command line development, but who may not know anything about Swift. We will use a trivial two-line Swift program as an example.

This material comes from the author’s experience developing the qp application. These procedures work, or at least seem to work, but I cannot explain why they work, since I do not know the inner workings of this environment.

Instead, this article is from the perspective of someone who was familiar with command line development, but who knew nothing about Swift. It summarizes the things I learned along the way, so that others following the same path can have a faster start.

The examples below assume that you are working inside the Terminal app on macOS. You can follow along by entering any fixed-width text shown in bold.

Interactive Swift

The most direct way to use Swift from the command line is to run the interpreter interactively. The name of the Swift interpreter is simply swift.

You may be asked to grant permission to a debugger process when you run the Swift interpreter this way. You can manually control the permissions for developer tools using DevToolsSecurity(8).

swift
Welcome to Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8).
Type :help for assistance.

When you run the interpreter, you get a “Read Eval Print Loop,” or REPL. You type code directly to the interpreter, and it is executed immediately. For example:

  1> print("Hello")
Hello
  2> print("Goodbye")
Goodbye
  3> ^D

To quit the interpreter, type Control-D, shown as ^D above.

This two-line program will be all the Swift code used for this article.

Swift as a Scripting Language

The Swift interpreter can also be used to run scripts. You write a file whose first line names swift as the interpreter to use for running the rest of the file.

Save the following text into a file called greeting.swift:

#!/usr/bin/swift

print("Hello")
print("Goodbye")

The first line of a Swift script begins with #!, followed by the name of the interpreter. This is the same as for any other Unix script.

After creating the above file, you can make it executable and then run it directly:

chmod +x greeting.swift
./greeting.swift
Hello
Goodbye

In my opinion, this is the second best way to experiment with Swift while learning the language. It provides fairly quick response and immediate feedback. (The best way is described later in this article.)

Compiled Swift

After writing a Swift script, you can compile it to a binary executable using the Swift compiler, which is named swiftc.

The swiftc compiler allows the -o option to specify an output file name.

swiftc -o greeting greeting.swift
./greeting
Hello
Goodbye

Note that you do not need to remove the first line in the script file that begins with #!. The Swift compiler ignores it.

Multiple Source Files

For any non-trivial program, you will need to divide your code into a number of source files.

This is where Swift gets a little tricky. The complication has to do with:

  • A Swift program’s entry point, and
  • Top-level (global) versus non-top-level code

Entry point

If you write C code, you know that a program’s entry point is a function named main.

For Swift, the entry point is a file named main.swift.

Every program you wish to generate from more than one Swift source file must have a single source file named main.swift. Its contents are equivalent to the body of the main function in a C program.

To illustrate this using our example, we will now split our two-line program into three source files. (In industry jargon, this process is called “Growth-Oriented Refactoring,” or GORE.)

First, create a file hello.swift to have the following content, which is a single function definition.

func hello() {
  print("Hello")
}

Next, create a file goodbye.swift to have the following content, which is again a single function definition.

func goodbye() {
  print("Goodbye")
}

Finally, create a file named main.swift to have the following content. It contains two function calls.

hello()
goodbye()

To produce a binary from these three Swift files, pass all the files to swiftc in a single invocation:

swiftc -o greeting hello.swift goodbye.swift main.swift
./greeting
Hello
Goodbye

Top-level versus non-top-level code

The file named main.swift is the only file that can contain top-level code, meaning code not inside a containing scope.

In our example, the files hello.swift and goodbye.swift each contain a function definition. The actual code they contain is within that function’s scope, and therefore is not top-level code.

On the other hand, the file main.swift does contain top-level code. Its two function calls are not enclosed in a containing scope.

Top-level code is only allowed in a file named main.swift.

Building with make

Because you can run the Swift compiler on the command line, you can automate your builds using make.

For our example, we can write a simple Makefile as follows:

greeting: hello.swift goodbye.swift main.swift
	swiftc -o $@ $^

(Note that the second line must be indented with a single TAB character, not spaces.)

This causes the compiler to re-run whenever any of the three Swift files changes, generating a new version of the program.

Running with make

If you use make to build your program, you might as well also use it to run your program. Add another rule to your Makefile after the previous one, as follows:

run: greeting
	./greeting

You can then run your program by typing:

make run

For me, this is the best way to learn and experiment with Swift (or anything else that can be reduced to these steps):

  1. Edit
  2. make
  3. make run

I dare say there is no other development environment that can even hope to approach the speed at which one can experiment with ideas as these three simple steps. (If you can think of one, I can retort that even step 2 above is redundant.)

Debugging

Debugging code from the command line can be quite cumbersome.

The swiftc compiler takes the same -g option as most other command line compilers to generate a debuggable executable:

swiftc -g -o greeting hello.swift goodbye.swift main.swift

You can then debug the resulting file using the lldb debugger:

lldb greeting
(lldb) target create "greeting"
Current executable set to 'greeting' (x86_64).
(lldb) 

In order to debug this way, you obviously need to know how to use lldb through its command line interface.

I personally have no problem with debugging by typing commands to lldb. What I do have a problem with is the way command line debuggers show the current line after hitting a breakpoint. They do not show the source code when debugging as when editing. Instead, they print the current line and its neighboring lines on every stoppage:

(lldb) br set -n hello
Breakpoint 1: where = greeting`greeting.hello() -> () + 8 at hello.swift:2:9, address = 0x0000000100000988
(lldb) run
Process 12345 launched: ...
Process 12345 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000988 greeting`hello() at hello.swift:2:9
   1   	func hello() {
-> 2   	  print("Hello")
   3   	}
Target 0: (greeting) stopped.
(lldb) 

For this reason, I would not use such an interface if a better alternative is available. However, if you really had no other choice, it will get the job done. That is, it will allow you to debug your program.

I can suggest two alternatives, which I personally switch between, depending on whether the program uses Apple-specific APIs.

Debugging alternative 1: Xcode

If you prefer command line development, learning a complicated graphical IDE is an unappealing idea, but you may have no choice if you want to debug your program.

What you can do is write your Swift program using the methods described in this article, and then import your files into Xcode just to do the debugging.

There are many resources on learning Xcode. You can start with the official Apple documentation.

Debugging alternative 2: Emacs and realgud

This is an alternative only for developers who use the Emacs editor.

The Emacs editor, combined with its GUD library, allows seeing the source code when debugging in the same way as when editing. However, with Swift, this setup does not work, because GUD does not support lldb.

An alternative that does work is realgud, along with its interface to lldb, realgud-lldb. If you are an Emacs user, you can look into these packages. I can report that this combination will work with Swift.

External Links

For more regarding main.swift, see the following article. Pay attention to the section titled “Global Variables,” which is not covered above.

https://developer.apple.com/swift/blog/?id=7

For more on the Swift REPL, see the following article.

https://developer.apple.com/swift/blog/?id=18

If you use make run, you may want to pass command line arguments to the underlying program. The following link describes how to write your Makefile to allow this.

https://stackoverflow.com/questions/2214575/passing-arguments-to-make-run