Skip to content
/ Yaap Public

Yet Another (Swift) Argument Parser

License

Notifications You must be signed in to change notification settings

hartbit/Yaap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Yaap version 1.0.0 swift 5.1 license MIT

Yaap is Yet Another (Swift) Argument Parser that represents executable commands as types, and arguments as properties of those types. It supports:

  • Strongly-typed argument and option parsing
  • Automatic help and usage message generation
  • Multiple command routing
  • Smart error messages with suggestion on typos

Here's a self-contained example of a rand executable that generates random numbers in a configurable interval to standard output, with everything from --help documentation, usage generation, and --version printing.

class RandomCommand: Command {
    let name = "rand"
    let documentation = "Generates a random number that lies in an interval."

    @Argument(documentation: "Exclusive maximum value")
    var maximum: Int

    @Option(shorthand: "m", documentation: "Inclusive minimum value")
    var minimum: Int = 0

    let help = Help()
    let version = Version("0.1.0")

    func run(outputStream: inout TextOutputStream, errorStream: inout TextOutputStream) throws {
        guard maximum > minimum else {
            throw InvalidIntervalError(minimum: minimum, maximum: maximum)
        }

        outputStream.write(Int.random(in: minimum..<maximum).description)
        outputStream.write("\n")
    }
}

struct InvalidIntervalError: LocalizedError {
    let minimum: Int
    let maximum: Int

    var errorDescription: String? {
        return "invalid interval [\(minimum), \(maximum))"
    }
}

RandomCommand().parseAndRun()

Installation

Yaap can be installed as a Swift Package Manager dependency. Here's the declaration for depending on the latest stable version:

let package = Package(
    dependencies: [
        .package(url: "https://github.com/hartbit/Yaap.git", from: "1.0.0")
    ]
)

Usage

Commands

In Yaap, a command is a self-contained operation defined as a class conforming to the Command protocol: arguments (if any) are defined as properties and execution logic is defined in a run(outputStream:errorStream) function. Simple programs only need one command but can grow more as necessary.

A command must also define a name (the executable name), that will appear in the usage description, and an optional documentation property, that will appear in the help output.

class HelloWorldCommand: Command {
    let name = "hello-world"
    let description = "My first command"

    func run(outputStream: inout TextOutputStream, errorStream: inout TextOutputStream) throws {
        outputStream.write("Hello World")
    }
}

Commands can parse command-line arguments and run themselves with the parseAndRun function:

let command = HelloWorldCommand()
command.parseAndRun()

Any errors thrown by run(outputStream:errorStream) will be caught and reported to the standard error stream.

Arguments

Mandatory arguments are defined using the generic Argument type and are parsed in the order they are declared in the command. They can also be configured with an optional name and documentation that will show in the help output:

class SplitCommand: Command {
    let name = "split"

    @Argument(documentation: "The string to split.")
    var string: String

    @Argument(name: "separator", documentation: "The seperator to split the string with.")
    var sep: Character

    func run(outputStream: inout TextOutputStream, errorStream: inout TextOutputStream) throws {
        outputStream.write(string.split(separator: sep).joined(separator: "\n"))
        outputStream.write("\n")
    }
}
$ split "The Swift Programming Language" " "
The
Swift
Programming
Language

Options

Optional arguments are defined using the generic Option type and must provide a defaultValue. The are parsed using the --option value or --option=value syntax where option is the name of the property, which can be customized with an optional name parameter. There is also an optional shorthand parameter to allow parsing them with a single character syntax of -o value or -o=value. Again, documentation can be provided for the help output:

class SplitCommand: Command {
    let name = "split"

    @Argument
    var string: String

    @Option(name: "separator", shorthand: "s", documentation: "The seperator to split the string with.")
    var sep: Character = " "

    func run(outputStream: inout TextOutputStream, errorStream: inout TextOutputStream) throws {
        outputStream.write(string.split(separator: sep).joined(separator: "\n"))
        outputStream.write("\n")
    }
}
$ split a,b,c,d --separator ,
a
b
c
d

Sub-commands

Help

Yaap comes with a built-in Help property that parses --help/-h arguments and prints the command's detailed documentation to standard output. It can be configured with a different name and shorthand syntax. Using the RandomCommand example from above:

class RandomCommand: Command {
    let name = "rand"
    let documentation = "Generates a random number that lies in an interval."
    let help = Help()

    @Argument(documentation: "Exclusive maximum value")
    var maximum: Int

    @Option(shorthand: "m", documentation: "Inclusive minimum value")
    var minimum: Int = 0

    func run(outputStream: inout TextOutputStream, errorStream: inout TextOutputStream) throws {
        // ...
    }
}
$ rand --help
OVERVIEW: Generates a random number that lies in an interval.

USAGE: rand [options] <maximum>

ARGUMENTS:
  maximum          Exclusive maximum value

OPTIONS:
  --help, -h       Display available options [default: false]
  --minimum, -m    Inclusive minimum value [default: 0]

Version

Yaap also comes with a built-in Version property that allows commands to respond to a --version/-v argument by printing their version number to standard output. The property can be customized to respond to a different argument name and optional shorthand syntax:

class MyCommand: Command {
    let name = "program"
    let version = Version("4.2", name: "ver", shorthand: nil)

    func run(outputStream: inout TextOutputStream, errorStream: inout TextOutputStream) throws {
        // ...
    }
}
$ program --ver
4.2

Thanks

I'd like to thank SwiftCLI for being a major influence in designing Yaap.