How To Add Extra Commands To CLI Tools

In previous entries we've extensively covered how we can build a new command line tool from scratch.

While this is great for brand new ideas, sometimes all we need is to add an extra feature to an existing tool: in this article we're going to do just that.

Example

Let's say that we would like to add a new git feature for grading the current code base (😱):
in order to do so we will create a new git subcommand, $ git star, and we're also going to use ArgumentParser!

For simplicity's sake, our subcommand will ask for a rating from 1 to 5 as an input, and print it out in the terminal. But there's truly no limit on what can we can do.

Let's get started!

Project setup

First we need to create a new Swift Package Executable.

Naming Convention

The package name doesn't really matter, however I suggest you to follow name patterns such as git-star, where the prefix is the tool we're extending, and the postfix is our subcommand, as those are patterns used by similar tools, and also the final name of our binary.

In our example we will do the following:

$ mkdir git-star
$ cd git-star
$ swift package init --type executable

Adding ArgumentParser Dependency

Any swift command tool needs some kind of input: in this example we're going to use Swift's ArgumentParser, which we have covered previously.

TL;DR: your Package.swift should look like this:

// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "git-swift",
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "git-swift",
            dependencies: ["ArgumentParser"]),
        .testTarget(
            name: "git-swiftTests",
            dependencies: ["git-swift"]),
    ]
)

The Package.swift content.

main.swift

At this point all is left is to write the actual command, without further ado:

import ArgumentParser

struct GitStar: ParsableCommand {
    @Argument(help: "Your code rating (1-5 only).")
    var rating: Int

    func run() throws {
        let fullStars = String(repeating: "β˜…", count: rating)
        let emptyStars = String(repeating: "β˜†", count: 5 - rating)
        let stars: String = fullStars + emptyStars
        print("Your rating \(stars)")
    }

    func validate() throws {
        guard 1...5 ~= rating else {
            throw ValidationError("Only ratings between 1 to 5 allowed.")
        }
    }
}

GitStar.main()

The main.swift content.

The script is nothing out of the ordinary, with standard validate() and run() logics.

As we've seen in A Look Into ArgumentParser, it's important that the name of our ParsableCommand type translates correctly into the final command line (unless defined otherwise via CommandConfiguration).

The name GitStar translates into git-star, and we can verify so by running $ swift run git-star --help:

$ swift run git-star --help
> USAGE: git-star <rating>
> 
> ARGUMENTS:
>   <rating>                Your code rating (1-5 only).

> OPTIONS:
>   -h, --help              Show help information.

See the USAGE callout.

Note that this output has no relation whatsoever with the package name or product. If we rename the struct to FiveStars for example, the same command will have different output:

$ swift run git-star --help
> USAGE: five-stars <rating>
> 
> ARGUMENTS:
>   <rating>                Your code rating (1-5 only).

> OPTIONS:
>   -h, --help              Show help information.

See the USAGE callout.

Releasing The Script

We've covered this before:

$ swift build -c release
$ cp .build/release/git-star /usr/local/bin/git-star

The Caveat

When an executable placed in /usr/local/bin/ has a dash - in its name, and the original command (the prefix) accepts subcommands, then we can run our command with and without the dash, which means that we can now run $ git star from anywhere:

$ git star 5
> Your rating β˜…β˜…β˜…β˜…β˜…

$ git star
> Error: Missing expected argument '<rating>'
> Usage: git-star <rating>

$ git star -h
> USAGE: five-stars <rating>
> 
> ARGUMENTS:
>   <rating>                Your code rating (1-5 only).
> 
> OPTIONS:
>   -h, --help              Show help information.

The same approach works regardless of the origin/nature of the executable: even a renamed bash file would do the trick.

Conclusions

While we've built a basic example, it's easy to imagine how we can use this approach to add big, powerful features directly in git and any other tool.

Also please note that this tutorial doesn't work with all executables: some (πŸ‘‹πŸ» xcrun) require extra work (or different paths) in order to achieve the same result.

Lastly, if you need inspiration on what you can build with this new knowledge, please have a look at git-standup and swift-outdated.

Do you use any tool with this approach? Have you built or are you planning to make one yourself? Please let me know!

Thank you for reading and stay tuned for more articles!

β­‘β­‘β­‘β­‘β­‘

Further Reading

Explore Swift

Browse all