Swift package for writing command line tools.


SwiftPM compatible MIT License language Swift 5.1 platform macOS platform Linux Build Status

The CLIKit framework contains various convenient utilities for making it easier to write command line tools in Swift.


CLIKit is released under the MIT license. See LICENSE file for more detailed information.

Table of Contents

Getting Started

Add CLIKit to your Swift package by adding the following to your Package.swift file in the dependencies array:

.package(url: "", from: "<version>")

If you are using Xcode 11 or newer, you can add CLIKit by entering the URL to the repository via the File menu:

File > Swift Packages > Add Package Dependency...

Note: CLIKit requires Swift 5.1 or later.

Reference Documentation

There is generated reference documentation available.


The following sections contain some rudimentary information about the most prominent features in CLIKit, along with examples.

Command Line Parser

Example of a command definition:

class FibonacciCommand: Command {
    let description = "Calculate fibonacci numbers"

    @CommandFlag(short: "v", description: "Prints verbose output")
    var verbose: Bool
    @CommandOption(short: "i", default: 5, regex: #"^\d+$"#,
                   description: "Number of iterations to perform.")
    var iterations: Int
    func run() {
        let result = fibonacci(iterations, printSteps: verbose)
        print("Result: \(result)")

Example of running the parser on the executable arguments and then running the command handler after the command has been parsed:

let command = try CommandLineParser().parse(FibonacciCommand())

If the binary is called fibonacci, the command can be run like this in a shell:

$ fibonacci -i 4

Several commands can be grouped together as subcommands:

class MathCommand: Commands {
    let description = "Perform math operations"
    let fibonacci = FibonacciCommand()
    let factorize = FactorizeCommand()
    let sum = SumCommand()

Example of running the parser on the executable arguments and then runing the command handler after the command has been parsed:

let command = try CommandLineParser().parse(MathCommand())

If the binary is called math, the fibonacci subcommand can be run like this in a shell:

$ math fibonacci -i 4

There are a few different types of arguments:

@CommandFlag(short: "v", description: "Prints verbose output")
var verbose: Bool

@CommandOption(short: "i", default: 5, regex: #"^\d+$"#,
               description: "Number of iterations to perform.")
var iterations: Int

@CommandRequiredInput(description: "First number")
var numberA: Int

@CommandOptionalInput(description: "Number to factorize")
var number: Int?

@CommandVariadicInput(description: "More numbers")
var numbers: [Int]


Example of launching a subprocess and capturing its output:

import CLIKit

// Search for Swift using PATH environment variable.
guard let path = ExecutableFinder.find("swift") else {
    print("Didn't find swift, exiting.")

do {
    // Launch Swift as a subprocess and capture its output.
    let subprocess = Subprocess(executable: path,
                                arguments: ["-h"],
                                captureOutput: true)
    try subprocess.spawn()

    // Wait for the process to finish.
    let result = try subprocess.wait()

    // Print the captured output from the subprocess.
    print(try result.capturedOutputString())
} catch {

Terminal Output

Example of using the TerminalString struct to print a string with ANSI terminal codes:

Console.print("\(.green)This is green.\(.reset)\(.bold)This is bold.\(.reset)")

If the console is a "dumb" terminal or the Xcode console, the ANSI terminal codes will be filtered out.

The Console class has a few convenience methods for console input and output:

if Console.confirmYesOrNo(question: "Clear the screen?", default: false) {
    // Clear the screen.
} else {
    // Do not clear the screen.


Command line programs usually end when there is no more code to run on the main thread. To do asynchronous work, such as network requests, or running code on a dispatch queue, a runloop needs to be started. The runUntilTerminated() method of the Execution class can be used to start a runloop that will run until the program is terminated, either programmatically using exit() or similar, or explicitly terminated by the system, e.g. if the user presses Ctrl-C.



There is an optional closure parameter to handle any necessary cleanup when the program is terminated. The closure is called if the process receives SIGINT (typically if the user presses Ctrl-C), SIGHUP (terminal disconnected) or SIGTERM (terminate).


Execution.runUntilTerminated { signal in 

    switch signal {
    case .terminate:
        // Do any necessary cleanup here.
        // Return true to allow the system to handle the SIGTERM signal.
        return true
    case .interrupt:
        // Do any necessary cleanup here.

        // Return false to suppress the SIGINT signal.
        // This will not allow Ctrl-C to terminate the program.
        return false
    case .terminalDisconnected:
        // Do any necessary cleanup here.

        // Return false to suppress the SIGHUP signal.
        // This will allow the process to run without the terminal.
        return false

Read Evaluate Print Loop

The ReadEvaluatePrintLoop class has a built in command line editor with support for various common keyboard shortcuts, customizable tab completion, a command line history, and multi-line support. If the terminal is "dumb" or a debugger is attached (such as if you want to run in the Xcode console) it falls back to just reading buffered lines from stdin.


let readEvaluatePrintLoop = try ReadEvaluatePrintLoop()

readEvaluatePrintLoop.textCompletion = SimpleWordCompletion(completions: [

try { input in
    guard !["quit", "exit"].contains(input) else {
        return .break
    Console.write(terminalString: "You entered: \(input)\n")
    return .continue

Path Management

CLIKit contains a Path struct that makes working with file system paths easier.


let absolutePath = Path("/usr/bin/zip")

let relativePath = Path("bin/whatever")

let concatenatedPath = Path("/usr") + Path("/bin")

let messyPath = Path("//usr/../usr/local/bin/./whatever")

let pathFromLiteralString: Path = "/this/is/a/path"
let pathFromEmptyString: Path = ""
let pathFromConcatenatedStrings: Path = "/usr" + "/bin"

let pathFromComponents = Path(components: ["/", "usr/", "bin", "/", "swift"])
let pathFromEmptyComponents = Path(components: [])

let appendedPath = Path("/usr/local").appendingComponent("bin")
let appendedPath3 = Path("/usr/local").appending(Path("bin"))
let appendedPath2 = Path("/usr/local") + Path("bin")

let imagePath = Path("photos/photo").appendingExtension("jpg")

let imagePathWithoutExtension = imagePath.deletingExtension
let imagePathWithoutLastComponent = imagePath.deletingLastComponent


// Return an array of Path objects representing files in the current directory.
let filesInDirectory = try Path.currentDirectory.contentsOfDirectory

// Change directory to the user's home directory

if let desktop = Path.desktopDirectory {
    Path("/path/myfile.txt").copy(to: desktop)


try (desktop + "My Folder").createDirectory()



  • Swift Tools 5.2.0
View More Packages from this Author


  • None
Last updated: Mon Jan 23 2023 21:00:38 GMT-0500 (GMT-05:00)