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
$ 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!