SkillAgentSearch skills...

SwiftCLI

A powerful framework for developing CLIs in Swift

Install / Use

/learn @jakeheis/SwiftCLI
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

SwiftCLI

Build Status

A powerful framework for developing CLIs, from the simplest to the most complex, in Swift.

import SwiftCLI

class GreetCommand: Command {
    let name = "greet"
    
    @Param var person: String

    func execute() throws {
        stdout <<< "Hello \(person)!"
    }
}

let greeter = CLI(name: "greeter")
greeter.commands = [GreetCommand()]
greeter.go()
~ > greeter greet world
Hello world!

With SwiftCLI, you automatically get:

  • Command routing
  • Option parsing
  • Help messages
  • Usage statements
  • Error messages when commands are used incorrectly
  • Zsh completions

Table of Contents

Installation

Ice Package Manager

> ice add jakeheis/SwiftCLI

Swift Package Manager

Add SwiftCLI as a dependency to your project:

dependencies: [
    .package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.0")
]

Carthage

github "jakeheis/SwiftCLI" ~> 5.2.2

CocoaPods

pod 'SwiftCLI', '~> 6.0.0'

Creating a CLI

When creating a CLI, a name is required, and a version and description are both optional.

let myCli = CLI(name: "greeter", version: "1.0.0", description: "Greeter - a friendly greeter")

You set commands through the .commands property:

myCli.commands = [myCommand, myOtherCommand]

Finally, to run the CLI, you call one of the go methods.

// Use go if you want program execution to continue afterwards
myCli.go() 

// Use goAndExit if you want your program to terminate after the CLI has finished
myCli.goAndExit()

// Use go(with:) if you want to control the arguments which the CLI runs with
myCli.go(with: ["arg1", "arg2"])

Commands

In order to create a command, you must implement the Command protocol. All that's required is to implement a name property and an execute function; the other properties of Command are optional (though a shortDescription is highly recommended). A simple hello world command could be created as such:

class GreetCommand: Command {

    let name = "greet"
    let shortDescription = "Says hello to the world"

    func execute() throws  {
        stdout <<< "Hello world!"
    }

}

Parameters

A command can specify what parameters it accepts through certain instance variables. Using reflection, SwiftCLI will identify property wrappers of type @Param and @CollectedParam. These properties should appear in the order that the command expects the user to pass the arguments. All required parameters must come first, followed by any optional parameters, followed by at most one collected parameter.

class GreetCommand: Command {
    let name = "greet"

    @Param var first: String
    @Param var second: String?
    @CollectedParam var remaining: [String]
}

In this example, if the user runs greeter greet Jack Jill up the hill, first will contain the value Jack, second will contain the value Jill, and remaining will contain the value ["up", "the", "hill"].

@Param

Individual parameters take the form of the property wrapper @Param. Properties wrapped by @Param can be required or optional. If the command is not passed enough arguments to satisfy all required parameters, the command will fail.

class GreetCommand: Command {
    let name = "greet"

    @Param var person: String
    @Param var followUp: String

    func execute() throws {
        stdout <<< "Hey there, \(person)!"
        stdout <<< followUp
    }
}
~ > greeter greet Jack

Usage: greeter greet <person> <followUp> [options]

Options:
  -h, --help      Show help information

Error: command requires exactly 2 arguments

~ > greeter greet Jack "What's up?"
Hey there, Jack!
What's up?

If the user does not pass enough arguments to satisfy all optional parameters, the value of these unsatisfied parameters will be nil.

class GreetCommand: Command {
    let name = "greet"

    @Param var person: String
    @Param var followUp: String? // Note: String? in this example, not String

    func execute() throws {
        stdout <<< "Hey there, \(person)!"
        if let followUpText = followUp {
            stdout <<< followUpText
        }
    }
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack "What's up?"
Hello, Jack!
What's up?

@CollectedParam

Commands may have a single collected parameter after all the other parameters called a @CollectedParam. This parameter allows the user to pass any number of arguments, and these arguments will be collected into the array wrapped by the collected parameter. The property wrapped by @CollectedParam must be an array. By default, @CollectedParam does not require the user to pass any arguments. The parameter can require a certain number of values by using the @CollectedParam(minCount:) initializer.

class GreetCommand: Command {
    let name = "greet"

    @CollectedParam(minCount: 1) var people: [String]

    func execute() throws {
        for person in people {
            stdout <<< "Hey there, \(person)!"
        }        
    }
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack Jill Water
Hey there, Jack!
Hey there, Jill!
Hey there, Water!

Value type of parameter

With all of these parameter property wrappers, any type can be used so long as it conforms to ConvertibleFromString. Most primitive types (e.g. Int) conform to ConvertibleFromString already, as do enums with raw values that are primitive types.

class GreetCommand: Command {
    let name = "greet"

    @Param var number: Int

    func execute() throws {
        stdout <<< "Hey there, number \(number)!"     
    }
}
~ > greeter greet Jack

Usage: greeter greet <number> [options]

Options:
  -h, --help      Show help information

Error: invalid value passed to 'number'; expected Int

~ > greeter greet 4
Hey there, number 4!

Parameters with enum types which conform to CaseIterable have additional specialized behavior. In an error message, the allowed values for that parameter will be spelled out.

class GreetCommand: Command {
    
    let name = "greet"
    
    enum Volume: String, ConvertibleFromString, CaseIterable {
        case loud
        case quiet
    }
    
    @Param var volume: Volume
    
    func execute() throws {
        let greeting = "Hello world!"
        
        switch volume {
        case .loud: stdout <<< greeting.uppercased()
        case .quiet: stdout <<< greeting.lowercased()
        }
        
    }
}
~ > greeter greet Jack

Usage: greeter greet <volume> [options]

Options:
  -h, --help      Show help information

Error: invalid value passed to 'volume'; expected one of: loud, quiet

~ > greet greet loud
HELLO WORLD!

To conform a custom type to ConvertibleFromString, simply implement one function:

extension MyType: ConvertibleFromString {
    init?(input: String) {
        // Construct an instance of MyType from the String, or return nil if not possible
        ...
    }
}

Options

Commands have support for two types of options: flag options and keyed options. Both types of options can be denoted by either a dash followed by a single letter (e.g. git commit -a) or two dashes followed by the option name (e.g. git commit --all). Single letter options can be cascaded into a single dash followed by all the desired options: git commit -am "message" == git commit -a -m "message".

Options are specified with property wrappers on the command class, just like parameters:

class ExampleCommand: Command {
    ...
    @Flag("-a", "--all")
    var flag: Bool

    @Key("-t", "--times")
    var key: Int?
    ...
}

Flags

Flags are simple options that act as boolean switches. For example, if you were to implement git commit, -a would be a flag option. They take the form of booleans wrapped by @Flag.

The GreetCommand could take a "loudly" flag:

class GreetCommand: Command {

    ...

    @Flag("-l", "--loudly", description: "Say the greeting loudly")
    var loudly: Bool

    func execute() throws {
        if loudly {
             ...
        } else {
            ...
        }
    }

}

A related option type is @CounterFlag, which counts the nubmer of times the user passes the same flag. @CounterFlag can only wrap properties of type Int. For example, with a flag declaration like:

class GreetCommand: Command {
    ...
    @CounterFlag("-s", "--softly", description: "Say the greeting softly")
    var softly: Int
    ...
}

the user can write greeter greet -s -s, and softly.value will be 2.

Keys

Keys are options that have an associated value. Using "git commit" as an example, "-m" would be a keyed option, as it has an associated value - the commit message. They take the form of variables wrapped by '@Key`.

The GreetCommand coul

Related Skills

View on GitHub
GitHub Stars875
CategoryDevelopment
Updated5d ago
Forks73

Languages

Swift

Security Score

100/100

Audited on Apr 3, 2026

No findings