How to build better command line apps and tools using Swift?
These tips will help you to create amazing CLI tools, utility apps, server side projects or terminal scripts using the Swift language.
Running Swift files as scripts
It is possible to run a Swift file straight from the command line if you add a hashbang to the beginning of the file. This way you don’t have to manually compile the code using the swiftc
command. You can simply give the file the executable permission flag and the system will call the Swift REPL under the hood, so our app can be evaluated automatically. 🔨
#!/usr/bin/env swift
print("Hello, world!")
For example this main.swift
file above can be marked as an executable file, and we can simply call it via the ./main.swift
command later on (you just have to use chmod only one time).
chmod +x main.swift
./main.swift
# Hello, world!
The beauty of this method is that you can rapidly test your Swift command line snippets. You can even place the finished Swift scripts under the /usr/local/bin/
directory without the swift file extension to make them available “globally” for your operating system user. 💪
Using command line arguments in Swift
The CommandLine enum makes it very easy to fetch the arguments passed to our Swift application or script. You can access every argument using the arguments variable as an array of Strings, but it is also possible to get the raw data using the argc
and unsafeArgv
properties.
#!/usr/bin/env swift
/// the very first element is the current script
let script = CommandLine.arguments[0]
print("Script:", script)
/// you can get the input arguments by dropping the first element
let inputArgs = CommandLine.arguments.dropFirst()
print("Number of arguments:", inputArgs.count)
print("Arguments:")
for arg in inputArgs {
print("-", arg)
}
You should note that the first argument is always the path of the current script, so if you are only looking for the input arguments you can use the dropFirst()
method to return a subset of the input strings. Usually each argument is separated by a space character.
./main.swift hello world
# Script: main.swift
# Number of arguments: 2
# Arguments:
# - hello
# - world
In Xcode you can add custom arguments under the Edit Scheme… menu item when you click on the current scheme, look for the Arguments tab and use the Arguments Passed On Launch section.
Process info and environment in Swift
Just like we can access command line arguments, it is possible to examine the current process including some hardware information and environment variables.
#!/usr/bin/env swift
import Foundation
let info = ProcessInfo.processInfo
print("Process info")
print("Process identifier:", info.processIdentifier)
print("System uptime:", info.systemUptime)
print("Globally unique process id string:", info.globallyUniqueString)
print("Process name:", info.processName)
print("Software info")
print("Host name:", info.hostName)
print("OS major version:", info.operatingSystemVersion.majorVersion)
print("OS version string", info.operatingSystemVersionString)
print("Hardware info")
print("Active processor count:", info.activeProcessorCount)
print("Physical memory (bytes)", info.physicalMemory)
/// same as CommandLine.arguments
print("Arguments")
print(ProcessInfo.processInfo.arguments)
print("Environment")
/// print available environment variables
print(info.environment)
The environment variables property is a Dictionary where both the keys and the values are available as strings, so you might have to parse them if you are looking for different value types. You can set up environment custom variables in Xcode just like arguments, or you can pass them via the command line before you execute the Swift script using the export command.
Standard input and output in Swift
You can use the print function to write text to the standard output, but you should note that the print function has a variadic items definition, so you can pass around multiple arguments and a custom separator & terminator parameter to display more advanced outputs.
There is also a standard error stream, which is part of the standard streams of course, but what’s interesting about it is that you can also write to this channel through the FileHandle.standardError
property there is quite an elegant solution on a Stack Overflow thread originally created by Rob Napier, I’m going to include that one here as well. 🙏
Another great feature of the print function is the to parameter, which can accept a custom TextOutputStream
so you can wrap the stderr
stream in a custom object or you can also create custom output handlers and separate your print statements e.g. by context if you need.
#!/usr/bin/env swift
import Foundation
/// print using custom separator & terminator
print("This", "is", "fun", separator: "-", terminator: "!")
/// write to the standard output
"This goes to the standard error output"
.data(using: .utf8)
.map(FileHandle.standardError.write)
/// print to the standard output using a custom stream
final class StandardErrorOutputStream: TextOutputStream {
func write(_ string: String) {
FileHandle.standardError.write(Data(string.utf8))
}
}
var outputStream = StandardErrorOutputStream()
print("This is also an error", to: &outputStream)
/// clears the console (@NOTE: won't work in Xcode)
func clear() {
print("\u{1B}[2J")
print("\u{1B}[\(1);\(0)H", terminator: "")
}
print("foooooooooooooooooooooo")
clear()
print("Hello, world!")
/// print colorful text using ANSI escape codes
print("\u{1b}[31;1m\u{1b}[40;1m\("Hello, world!")\u{1b}[m")
print("\u{1b}[32;1m\("Hello, world!")\u{1b}[m")
/// reading lines from the standard input
print("Please enter your input:")
guard let input = readLine(strippingNewline: true) else {
fatalError("Missing input")
}
print(input)
The second half of the snippet is full of ANSI escape codes which I like quite a lot, because it can make our terminal output quite beautiful. The only problem is that they don’t work in Xcode at all (come-on Apple, please support this…). You can clear the console or change the background / foreground color of the output by using these codes.
There are quite a lot of libraries on GitHub that you can use to print colorful output, for example ColorizeSwift, ANSITerminal, ANSIEscapeCode and many more cool ones.
The very last thing that I’d like to show you is the readLine function, which you can use to read a line from the standard input. This comes handy if you need to get user input from the command line.
Use an argument parser library
If you are looking for a type-safe argument parser written in Swift, you should definitely take a look at the Swift Argument Parser library. It is created and maintained by Apple, so it’s kind of an official solution for this particular issue, but IMHO it lacks some advanced features.
This is the main reason why I prefer the Vapor command API built on top of the ConsoleKit library. Both libraries can parse arguments, options and flags, but ConsoleKit is also capable of displaying progress indicators, it features multiple command groups, secure input, auto-completion, multiple log levels and many more.
/// HelloCommand.swift
import Foundation
import ConsoleKit
final class HelloCommand: Command {
struct Signature: CommandSignature {
@Argument(name: "name", help: "The name to say hello")
var name: String
@Option(name: "greeting", short: "g", help: "Greeting used")
var greeting: String?
@Flag(name: "capitalize", short: "c", help: "Capitalizes the name")
var capitalize: Bool
}
static var name = "hello"
let help = "This command will say hello to a given name."
func run(using context: CommandContext, signature: Signature) throws {
let greeting = signature.greeting ?? "Hello"
var name = signature.name
if signature.capitalize {
name = name.capitalized
}
print("\(greeting) \(name)!")
/// progress bar
let bar = context.console.progressBar(title: "Hello")
bar.start()
/// perform some work...
// bar.fail()
bar.succeed()
/// input
let foo = context.console.ask("What?")
print(foo)
/// secure input
let baz = context.console.ask("Secure what?", isSecure: true)
print(baz)
/// choice
let c = context.console.choose("Make a choice", from: ["foo", "bar", "baz"])
print(c)
/// @Tip: look for more options under the context.console property.
}
}
/// main.swift
import Foundation
import ConsoleKit
let console: Console = Terminal()
var input = CommandInput(arguments: CommandLine.arguments)
var context = CommandContext(console: console, input: input)
var commands = Commands(enableAutocomplete: true)
commands.use(HelloCommand(), as: HelloCommand.name, isDefault: false)
do {
let group = commands.group(help: "Using ConsoleKit without Vapor.")
try console.run(group, input: input)
}
catch {
console.error("\(error)")
exit(1)
}
You can use both solution through the Swift Package Manager, the setup process is quite easy, you’ll find more tutorials about the Swift Argument Parser and I think that it is harder to find proper docs for ConsoleKit, so yeah… anyway, they’re great libraries you won’t regret using them. 😉
Take advantage of the Swift Package Manager
The Swift Package Manager is one of the best thing about the Swift programming language. I really love it and I use it almost every day. The fact that the package manifest file is defined using Swift itself makes it easy to use & understand.
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "myProject",
platforms: [
.macOS(.v10_15)
],
dependencies: [
.package(url: "https://github.com/vapor/console-kit", from: "4.1.0"),
],
targets: [
.executableTarget(name: "myProject",dependencies: [
.product(name: "ConsoleKit", package: "console-kit"),
]),
.testTarget(name: "myProjectTests", dependencies: ["myProject"]),
]
)
The package manager evolved quite a lot during the past few months, if you take a look at the Swift Evolution dashboard you can track these changes, the most recent update was the introduction of custom, user-defined Package Collections, but if you are looking for packages you can always take a look at the Swift Package Index website. 👍
Related posts
All about the Bool type in Swift
Learn everything about logical types and the Boolean algebra using the Swift programming language and some basic math.
Async HTTP API clients in Swift
Learn how to communicate with API endpoints using the brand new SwiftHttp library, including async / await support.
Beginners guide to functional Swift
The one and only tutorial that you'll ever need to learn higher order functions like: map, flatMap, compactMap, reduce, filter and more.
Beginner's guide to modern generic programming in Swift
Learn the very basics about protocols, existentials, opaque types and how they are related to generic programming in Swift.