SwiftCLI
A powerful framework for developing CLIs in Swift
Install / Use
/learn @jakeheis/SwiftCLIREADME
SwiftCLI
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
- Creating a CLI
- Commands
- Command groups
- Shell completions
- Built-in commands
- Input
- External tasks
- Single command CLIs
- Customization
- Running your CLI
- Example
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
node-connect
351.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
110.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
351.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
351.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
