The right way to write Swift scripts utilizing the brand new Command API in Vapor 4?


Shell scripts are necessities on the server aspect. Discover ways to construct Swift scripts on your backend apps utilizing property wrappers.

Vapor

Swift Argument Parser vs Vapor Instructions

Apple open-sourced a brand new library that may assist you a large number if you wish to construct scripts that written in Swift. The Swift Argument Parser was beforehand a part of the Swift Bundle Supervisor instruments, however now it’s even highly effective & has it is personal life (I imply repository). 😉

However Vapor already had a considerably comparable method to construct scripts, however in Vapor 4 the Command API is best than ever. Property Wrappers (out there from Swift 5.1) are utilized in each instances to deal with arguments, flags & choices. Personally I like this method so much.

Let me present you a easy hiya command:


import ArgumentParser

struct HelloCommand: ParsableCommand {
    @Argument(assist: "The title to say hiya")
    var title: String

    func run() throws {
        print("Hey (self.title)!")
    }
}
HelloCommand.essential()

Now I am going to present you how one can implement an identical command utilizing Vapor:


import Vapor

ultimate class HelloCommand: Command {
    
    let assist = "This command will say hiya to a given title."

    struct Signature: CommandSignature {
        @Argument(title: "title", assist: "The title to say hiya")
        var title: String
    }

    func run(utilizing context: CommandContext, signature: Signature) throws {
        print("Hey (signature.title)!")
    }
}

public func configure(_ app: Software) throws {
    app.instructions.use(HelloCommand(), as: "hiya")
}

As you may see they nearly seem like the identical.


For those who love scripting, you need to positively test swift-sh and Brisk


The Swift Argument Parser library is a light-weight resolution if you’re solely on the lookout for a easy Swift script. A very good instance is a software that manipulates recordsdata on the system or one thing comparable. It is only one little dependency, however it removes a lot boilerplate out of your scripts. It permits you to deal with the script itself, as a substitute of parsing the command line inputs. You’ll find extra detailed examples and an in depth documentation contained in the GitHub repository. 🙏


Vapor’s Command API is beneficial if you wish to carry out extra sophisticated duties together with your scripts. Something that is a part of your Vapor software will be triggered from a command, so you may simply create a backend software that reads (or writes) information from the database utilizing Fluent 4. That is the primary benefit of utilizing a Vapor command, as a substitute a stanadlone Swift script.




Arguments, choices, flags

Let’s prolong the hiya command with a brand new possibility and a flag. The principle distinction between an possibility and a flag is that an possibility has an related worth, however a flag is simply one thing that you just give to the command or not. Each choices and flags begin with a single - or a double sprint --, often the only dashed model makes use of a brief title for a similar factor. 🤓

Arguments are person supplied values learn so as (eg.: ./hiya joe bob john).

Now that you understand the essential definitions, right here is the instance:

ultimate class HelloCommand: Command {
        
    struct Signature: CommandSignature {

        @Argument(title: "title", assist: "The title to say hiya")
        var title: String

        @Possibility(title: "greeting", brief: "g", assist: "Greeting used")
        var greeting: String?

        @Flag(title: "capitalize", brief: "c", assist: "Capitalizes the title")
        var capitalize: Bool
    }

    let assist = "This command will say hiya to a given title."

    func run(utilizing context: CommandContext, signature: Signature) throws {
        let greeting = signature.greeting ?? "Hey"
        var title = signature.title
        if signature.capitalize {
            title = title.capitalized
        }
        print("(greeting) (title)!")
    }
}

Arguments are required by default, choices and flags are optionals. You’ll be able to have a customized title (brief and lengthy) for all the things, plus you may customise the assistance message for each element.

swift run Run hiya john


swift run Run hiya john --greeting Hello


swift run Run hiya john --greeting Hello --capitalized


swift run Run hiya john -g Szia -c

You’ll be able to name the command utilizing a number of kinds. Be happy to select a most well-liked model. ⭐️



Subcommands

When command-line applications develop bigger, it may be helpful to divide them into a gaggle of smaller applications, offering an interface by subcommands. Utilities equivalent to git and the Swift bundle supervisor are capable of present diversified interfaces for every of their sub-functions by implementing subcommands equivalent to git department or swift bundle init.

Vapor can deal with command teams in a very cool approach. I am going to add an additional static property to call our instructions, since I do not wish to repeat myself or bloat the code with pointless strings:

ultimate class HelloCommand: Command {
    
    static var title = "hiya"
        
    
}

struct WelcomeCommandGroup: CommandGroup {
    
    static var title = "welcome"

    let assist: String
    let instructions: [String: AnyCommand]
    
    var defaultCommand: AnyCommand? {
        self.instructions[HelloCommand.name]
    }

    init() {
        self.assist = "search engine optimisation command group assist"

        self.instructions = [
            HelloCommand.name: HelloCommand(),
        ]
    }
}

public func configure(_ app: Software) throws {

    app.instructions.use(WelcomeCommandGroup(), as: WelcomeCommandGroup.title)
}


That is it, we simply moved our hiya command beneath the welcome namespace.

swift run Run welcome hiya john --greeting "Hello" --capitalize

For those who learn the Swift Argument Parser docs, you may obtain the very same conduct by a customized CommandConfiguration. Personally, I want Vapor’s method right here… 🤷‍♂️



Ready for async duties

Vapor builds on prime of SwiftNIO together with EventLoops, Futures & Guarantees. A lot of the API is asynchronous, however within the CLI world it’s important to anticipate the async operations to complete.

ultimate class TodoCommand: Command {
    
    static let title = "todo"

    struct Signature: CommandSignature { }
        
    let assist = "This command will create a dummy Todo merchandise"

    func run(utilizing context: CommandContext, signature: Signature) throws {
        let app = context.software
        app.logger.discover("Creating todos...")
        
        let todo = Todo(title: "Look ahead to async duties...")
        strive todo.create(on: app.db).wait()
        
        app.logger.discover("Todo is prepared.")
    }
}

There’s a throwing wait() technique that you could make the most of to “keep within the loop” till all the things is completed. You can even get a pointer for the applying object by utilizing the present context. The app has the database connection, so you may inform Fluent to create a brand new mannequin. Additionally you need to use the built-in logger to print data to the console whereas the person waits. ⏳




Utilizing ConsoleKit with out Vapor

Let’s speak about overheads. Vapor comes with this neat instructions API, but in addition bundles numerous different core issues. What if I simply need the goodies for my Swift scripts? No downside. You need to use the underlying ConsoleKit by including it as a dependency.


import PackageDescription

let bundle = Bundle(
    title: "myProject",
    platforms: [
       .macOS(.v10_15)
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/console-kit", from: "4.1.0"),
    ],
    targets: [
        .target(name: "myProject", dependencies: [
            .product(name: "ConsoleKit", package: "console-kit"),
        ])
    ]
)

You continue to need to do some extra work in your essential.swift file, however nothing severe:

import ConsoleKit
import Basis

let console: Console = Terminal()
var enter = CommandInput(arguments: CommandLine.arguments)
var context = CommandContext(console: console, enter: enter)

var instructions = Instructions(enableAutocomplete: true)
instructions.use(HelloCommand(), as: HelloCommand.title, isDefault: false)

do {
    let group = instructions.group(assist: "Utilizing ConsoleKit with out Vapor.")
    strive console.run(group, enter: enter)
}
catch {
    console.error("(error)")
    exit(1)
}

This manner you may do away with many of the community associated core packages (which might be included by default in the event you use Vapor). This method solely fetches swift-log as a 3rd occasion dependency. 😍




Abstract

ConsoleKit in Vapor is a good way to write down CLI instruments and small scripts. The brand new Swift Argument Parser is a extra light-weight resolution for a similar downside. In case your plan is to keep up databases by scripts otherwise you carry out numerous networking or asynchronous operations it may be higher to go together with Vapor, since you may all the time develop by importing a brand new element from the ecosystem.