Exploring Swift - Creating a simple CLI tool
Swift is a powerful, modern and relatively easy to pick up language. It was publicly unveiled in 2014, at WWDC and it is based on C (for this reason, you can easily run C, Objective-C and, more recently, C++ code directly in your Swift Projects). The language supports Object Oriented constructs (such as classes and, with them, inheritance), but it was designed around the paradigm known as Protocol Oriented Programming.
Although it’s easy to pick up and become productive, mastering Swift is a process that takes time and effort. For this reason, it's usually a good idea to become familiar with the official documentation. You can take a Swift Language Tour or further consult the Language's Reference and, eventually, read the API Design Guidelines. These are all bundled under Swift’s Documentation section.
You can conceivably be very successful in your work without consulting these resources periodically, but it's useful to know they exist. The Swift project team also manages a Blog, where you can stay up to date with newer developments, including some that didn't yet make it to WWDC talks.
If you would like to understand how Swift evolves as a language - and to understand how the Language team decides which features to add and which not to add, you can check the Swift Evolution GitHub repository. You can also see what’s in store for upcoming versions of Swift by checking the Swift Evolution Page directly, as well. Both sources contain highly technical information, but you have the opportunity to see why Swift works the way it does. There are many gems hidden in the answers to proposal, not just the proposals themselves, so they are all great resources if you want to understand Swift’s language design philosophy straight from the language team.
Out of the box, Swift supports C/Objective-C interoperability (as Apple had/has a lot of Objective-C legacy code) and, since Swift 5.9, opt-in C++ interoperability.
Similar to many other programming languages, you can use Swift to create anything from simple cli tools (like the one in this section), to locally executed desktop applications, all the way to complex web servers.
Throughout this post, we are going to create a very simple cli tool which takes a list of numbers separated by the “,” (comma) symbol, and then returns those numbers in ascending order. The tools itself is not particularly useful, but it’s going to help us explore Swift a bit more closely, from the ground up.
Problem Decomposition
In order to capture the arguments of the cli application, we can use Apple’s swift-argument-parser module.
The main steps required to obtain the output are going to be:
In a dedicated struct entity, we are going to save the argument provided to the cli on call time, as a String variable named input. For brevity, we are not going to perform any input validation (which is a very bad practice in real world applications. You should always validate user input, even for offline applications. Validating user input is required not just for security reasons - but also to ensure that your application does not end up in an undefined state);
Using the split function associated to the String types, we are going to divide the input string into individual elements, based on the separator “,”;
Using the configMap function associated to Arrays , we are going to convert every element obtained from the split function into an Int
Finally, using the sorted function associated to Arrays, we can store a copy of the sorted array, in a temporary variable, which can then be printed to the console.
Before you start solving a problem, it’s usually a good idea to break it down to smaller, more manageable pieces. This helps you (and, if you work in a team, your colleagues) set up a common understanding. It’s not always possible to design an application (especially a complex one) unless you already built it once. Similarly, it’s not always possible to tackle complex problems, unless you break them down into smaller pieces first. As you solve more problems, it becomes a lot easier to find the smaller chunks - and this steps becomes less important, if you work alone.
Creating a new Package
In Swift, code is organized in Modules. Unlike other languages (eg Java, C# or Go), you do not explicitly define modulenames at the beginning of a Swift file, nor do you explicitly define the namespace.
One or more modules, together with a single manifest file, make up a Swift Package. Within the Package’s manifest file, you can define the Package Dependencies (functionality that is defined in other, external packages) and one or more Targetsfor your application to be compiled for. Finally, each target builds a Product, which is either a library (to be used in other projects) or an executable (and actual file).
To standardize the structure of Swift Project, but also to support the creation of various tools (such as build and CI/CD tools), all Swift projects are maintained as a Swift Bundle.
To start a new project, you can use a Swift utility to bootstrap a package for you. The utility would initialize your package, by creating the main directories and files for you. For example, in a dedicated directory called cli-example, you can run the command below (in the terminal):
$> swift package init --name cli-example --type executable
This command would return the following output, in the terminal’s standard output stream:
Creating executable package: cli-example
Creating Package.swift
Creating Sources/
Creating Sources/main.swift
The tool created a file for package management, named Package.swift, as well as a directory for source files, named Sources. Within the Sources directory, you can find a swift file named main.swift. By convention, the main.swift file is the only file that can contain top-level instruction.
Alternatively, you could designate a class as the top-level entry point for the program by using the @main attribute, in a file that is not called main.swift. The two options are mutually exclusive: you cannot declare a @main class in a file named main.swift.
A tree representation of the directory structure would be:
.
└── Package.swift
└── Sources
└── main.swift
Adding Dependencies
Using the Package.swift file, you can also add external dependencies to your project. For example, Apple already provides a package you can use to capture and use command-line arguments, named swift-argument-parser. We can add this package as a dependency to cli-example, by modifying the Swift Package Manifest:
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "cli-example",
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.2"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "cli-example",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
)
]
)
Throughout this example, we used Swift’s Package Manager, but you don’t necessarily have to. You can also use external package managers and their repositories, such as CocoaPods - which is industry standard for Apple development.
For reference, this is what the CocoaPods manifest (known as specification or spec) would look like:
// Example
platform :ios, '8.0'
use_frameworks!
target 'MyApp' do
pod 'AFNetworking', '~> 2.6'
pod 'ORStackView', '~> 3.0'
pod 'SwiftyJSON', '~> 2.3'
end
Alternatively, you can simply use Xcode to manage dependencies, as shown in Apple’s Xcode documentation
Implementing the CLI Tool
Before proceeding with the actual implementation, it would be useful to be aware of a few rules and conventions (enforced by Swift’s tooling, but not always well documented in literature):
In a Swift Package, source code files must end with the .swift extension.
Swift source code needs to be placed in a directory named Sources or Src (or variants of these directory names, such as Source or src), or in one of its subdirectories (eg Sources/MainApplication or Sources/Services).
Within the Swift Package Description Manifest, you can specify custom source directories for individual targets, using the path key in the executableTarget dictionary.
In Swift, directories act as helpers for you, the developer. They don’t really hold much meaning (unlike other languages where directories are effectively namespaces or packages), unless you attribute meaning in the Swift Package Description manifest, as mentioned previously. On compilation, all Swift files are loaded in a flat structure. For this reason, you cannot have two files with the same name in a single Swift project (unless they are part of different modules).
The entry point of an application is, by convention, the main.swift file. There are two main exceptions to this rule:
The package contains only one source file, in which case the name doesn’t matter
The package does not contain a main.swift file and, instead, it has a class or structure marked with the @main attribute. In this case, the entry point is the main function belonging to the marked type(functions associated to the type and not an instance of the type are identified by the static keyword). This is the more common pattern, if you work on developing regular applications in Xcode. The snippet below is an example of such an implementation.
@main
struct TestStruct {
var sample = ""
//This is the Instance Function
func run() {
print("The sample is: \(sample)")
}
//This is the entry point. It is a static function with no input and no returns (() -> Void, but Void can be omitted)
static func main() {
let s = TestStruct(sample: "A String provided within the static main function")
s.run()
}
}
With these conventions and rules in mind, we can now work on our simple Command Line Tool implementation. Although the exact structure of your project will likely vary, it’s a good idea to separate the main sections of your application into their areas of responsibility. For example, you would use an App directory to keep the application’s entry point, while the rest of the business logic would be maintained in separate directories (such as Services, Networking, Shaders etc.). Since the directory structure is more important to you than it is to the Swift tools, these conventions are mainly meant to help you organize your code.
To start, add two new directories inside the Sources directory - and name them App and Services, respectively (though you could use any other names, instead). Within the Services directory, add a new swift file, with the name CLIService.swift. Then, move the original main.swift file, from Sources to Sources/App. Your project structure should now resemble the tree below:
.
└── Package.swift
└── Sources
└── App
└── main.swift
└── Services
└── CLIService.swift
In the App/main.swift file, we would typically define instructions that are relevant to the application’s life cycle. For example, this is where you would execute the function that captures input from the user. For now, a set of print instructions would suffice. Replace the content of the main.swift file with the following print instructions:
print("Welcome to our simple CLI!")
print("We have finished the execution, I will close now")
Within the Services/CLIService.swift file, use the import keyword to add the ArgumentParser module to this file. This allows the compiler to recognize and use the functions, protocols, and types provided by that module within the file where it's imported. Then, define a struct to act as a wrapper for our CLI functionality. It should be called CLIService and it should conform to the ParsableCommand protocol (we are going to look closely into protocols later in the book. For now, know that the protocol is part of the ArgumentParser module).
import ArgumentParser
struct CLIService: ParsableCommand {
}
First, we would need to store the user input. Structs can contain properties (the equivalent of fields in other languages) and functions. Therefore, you can declare a public variable input, of type String, as the first property of the CLIService struct. To mark the variable as the storage of the user’s input, we can use the @Option property wrapper (more on this later)
import ArgumentParser
struct CLIService: ParsableCommand {
@Option(help: "Specify the input")
public var input: String
}
The ParsableCommand protocol includes a main function, which can be executed by your application’s entry point. It captures the arguments passed in the command line and, using them, it executes the instance function named run. We can, therefore, write our sorting logic within the run function - or in a separate function, which is executed (called) by the runfunction.
There are benefits and drawbacks to both approaches. If you keep the sorting functionality inside the run function, the code is easier to read, but slightly harder to extend. Conversely, if you use a dedicated function for sorting, you could add more functionality (such as removing an element), at the potential cost of resource consumption (function calls require resources) and a slight decrease in readability (how easy a human can understand the code and its purpose), especially if the functionality extends over a larger number of files.
Here is an example where the sorting logic is performed directly in the run function:
import ArgumentParser
struct CLIService: ParsableCommand {
@Option(help: "Specify the input")
public var input: String
//This function is called by CLIExample.main()
public func run() throws {
let result = input
.split(separator:",")
.compactMap{
Int($0)
}
.sorted()
print("The array of sorted numbers is: \(result)")
}
}
Alternatively, here is the same logic, but with the sorting performed by another, dedicated function, named sortInput:
import ArgumentParser
struct CLIService: ParsableCommand {
@Option(help: "Specify the input")
public var input: String
//This function is called by run()
private func sortInput() -> [Int]{
input
.split(separator:",")
.compactMap{
Int($0)
}
.sorted()
}
//This function is called by CLIExample.main()
public func run() throws {
print("The array of sorted numbers is: \(sortInput())")
}
}
You may keep either of the two examples. Since we separated the project in two files, each with their own purpose, you can easily replace or modify each of the two pieces, independently - and revert your decisions easily. You can also add more functionality in either of the two, without affecting the other. Just as importantly, you can test the two components individually.
Going forward, you can keep either of the two examples used for CLIService. They will both work just as well, because the struct is named CLIService in both examples, and the function is named run in both examples.
Update the main.swift file and, between the two print instructions, call the main() function of the CLIService struct.
print("Welcome to a simple CLI tool!")
CLIService.main()
print("Finished the execution, closing the process...")
When the cli-example project will be executed, the main.swift file will act as the entry point - and execute the staticfunction main() associated to the CLIService struct.
You can test out the example using the swift run command, as shown below.
$> swift run cli-example --input 3,1,5,7,6,4,90,78,45,32,15
Running that command would result in the following output (together with some other messages related to building and debugging, recorded by the run tool itself):
Fetching https://github.com/apple/swift-argument-parser from cache
Fetched https://github.com/apple/swift-argument-parser from cache (1.29s)
Computing version for https://github.com/apple/swift-argument-parser
Computed https://github.com/apple/swift-argument-parser at 1.5.0 (1.82s)
Computed https://github.com/apple/swift-argument-parser at 1.5.0 (0.00s)
Creating working copy for https://github.com/apple/swift-argument-parser
Working copy of https://github.com/apple/swift-argument-parser resolved at 1.5.0
Building for debugging...
56/56 Applying cli-example
Build of product 'cli-example' complete! (6.24s)
Welcome to a simple CLI tool!
The array of sorted numbers is: [1, 3, 4, 5, 6, 7, 15, 32, 45, 78, 90]
Finished the execution, closing the process...
If you would try running the same command again, you would still get the build output - indicating that the project builds again. However, it would take a lot less.
$> swift run cli-example --input 3,1,5,7,6,4,90,78,45,32,15
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'cli-example' complete! (0.20s)
Welcome to our simple CLI!
The array of sorted numbers is: [1, 3, 4, 5, 6, 7, 15, 32, 45, 78, 90]
We have finished the execution, I will close now
This is because, when we executed the initial swift run command, Swift’s toolset also built the project. When this occurs, the toolset creates a hidden directory (with a “.” in front of its name). In order to see hidden directories in the terminal, you would need to use ls -a
. For example:
$> ls -la
A truncated representation of the directory tree would look something like this:
.
└── .build
└── artifacts
└── checkouts
└── swift-argument-parser
└── ....
└── repositories
└── swift-argument-parser-54a11a8d
└── ...
└── arm64-apple-macosx
└── debug
└── ...
└── debug # // This is where the build clie binary resides.
└── Package.resolved
└── Package.swift
└── Sources
└── app
└── main.swift
└── services
└── CLIService.swift
From the project’s directory, you can also run the built binary, as well. This time, it runs directly, as it’s the final, built executable.
$> ./.build/debug/cli-example --input 3,1,5,7,6,4,90,78,45,32,15
Welcome to our simple CLI!
The array of sorted numbers is: [1, 3, 4, 5, 6, 7, 15, 32, 45, 78, 90]
We have finished the execution, I will close now
You can also see the file type, to confirm that it is, indeed, a Mach-O executable:
$> file ./.build/debug/cli-example
./.build/debug/cli-example: Mach-O 64-bit executable arm64
Notice that the build is, at this point, a debug build. As shown in the Swift Build System Guide, though, you can also build a release version, directly:
$> swift build -c release
[1/1] Planning build
Building for production...
[10/10] Linking cli-example
Build complete! (11.58s)
Once you run this command, your released binary can be found in .build/release
. You can also run the release binary.
$> .build/release/cli-example --input 3,1,5,7,6,4,90,9,8,2,3,3,5,6
Welcome to our simple CLI!
The array of sorted numbers is: [1, 2, 3, 3, 3, 4, 5, 5, 6, 6, 7, 8, 9, 90]
We have finished the execution, I will close now
If you check the contents of the release directory, though, you would find many other items. All of these files were generated when the Swift toolset (including the compiler) analyzed the source code files - and built the entire project.
.
└── ArgumentParser-tool.build
└── ...
└── ArgumentParserToolInfo-tool.build
└── ...
└── cli-example.dSYM --> debug symbols
└── ...
└── cli-example.product
└── Objects.LinkFileList --> text reference to other binary object files
└── cli_example.build
└── CLIService.swift.o --> binary object files
└── cli_example.d --> text reference to make dependency files
└── main.swift.o
└── sources
└── output-file-map.json
└── master.swiftdeps
└── swift-version--58304C5D6DBC2206.txt
└── ...
└── ...
└── ArgumentParserToolInfo.build
└── ToolInfo.swift.o
└── ArgumentParserToolInfo.d
└── ArgumentParserToolInfo-Swift.h
└── sources
└── output-file-map.json
└── module.modulemap
└── master.swiftdeps
└── ArgumentParser.build
└── ...
└── ...
└── Modules
...
└── cli_example.abi.json
└── ArgumentParserToolInfo.abi.json