Mastering CLI Applications with Cobra
As developers, we spend a significant amount of time in the terminal, and chances are high that you’ve used a command line interface (CLI) for your projects. CLI applications have become increasingly popular due to their ease of configuration and flexibility. One such popular Go library for building CLI applications is Cobra.
What is Cobra?
Cobra is a powerful tool that allows you to create CLI applications with ease. It’s widely used in popular developer tools like the Github CLI, Hugo, and more. In this tutorial, we’ll dive into the world of Cobra by building a simple accounting CLI application that bills users, stores information in a JSON file, records receipts, and tracks a user’s total balance.
Getting Started with Cobra
To begin, we need to install the Cobra Generator, which provides an easy way to generate commands that give life to the application. Run the following command to install the Cobra Generator:
go get -u github.com/spf13/cobra/cobra
Understanding CLI Commands and Flags
Before we start building our app, let’s understand the main components of a CLI application. A CLI application typically comprises the application’s name, the command, flags, and arguments. For example, when using Git to clone a project, we usually run the following command:
git clone url.to.project
Here, git
is the application name, clone
is the command, and url.to.project
is the argument passed to the command.
Creating the Cobra App
To create a new Cobra application, run the following command:
cobra init accountant
This command creates a new folder, accountant
, and creates a main.go
file, a LICENSE
file, and a cmd
folder with a root.go
file. Note that this command does not create a go.mod
file, so we must initialize the Go module by ourselves:
go mod init accountant
The Cobra App Entry Point
The entry point to our Cobra app is main.go
, and it’s essential to keep the main package lean so we can keep different aspects of the application separate. Looking at the Cobra-generated main.go
file, we find that the main function has just one function: executing the root command.
Creating Commands in Cobra
Our application should be able to handle two main features: debiting and crediting users. Thus, we must create two commands: debit
and credit
. Run the following to generate those commands:
cobra add debit
cobra add credit
Adding a JSON Storage Layer
For this application, we’ll use a simple storage layer. We’ll store our data in a JSON file and access them via a Go module. In the root directory, create a database
folder, then create a db.go
file and a db.json
file. Add the following to db.go
to interact with the database:
“`go
type User struct {
Username string
Balance int64
Transactions []Transaction
}
type Transaction struct {
Amount int64
Type string
Narration string
}
func getUsers() ([]User, error) {
// load database file and return stored user data
}
func updateDB(users []User) error {
// write updated data to the database
}
func FindUser(username string) *User {
// find a user in the database with a username and return it
}
func FindOrCreateUser(username string) *User {
// find a user in the database with a username and return it; if no user, create a new user
}
func UpdateUser(user *User) error {
// update the corresponding entry in the database
}
“`
Implementing Credit Transactions with Cobra
Modify the credit
command to create an adequate description for the command and add a usage section in the long description:
“`go
func init() {
creditCmd.Flags().StringP(“narration”, “n”, “”, “Transaction narration”)
creditCmd.Flags().Int64P(“amount”, “a”, 0, “Transaction amount”)
rootCmd.AddCommand(creditCmd)
}
func run(args []string) error {
// get the username from the args array
username := args[0]
user, err := FindOrCreateUser(username)
if err!= nil {
return err
}
// increment the user’s balance and add a new transaction
user.Balance += amount
transaction := Transaction{Amount: amount, Type: “credit”, Narration: narration}
user.Transactions = append(user.Transactions, transaction)
// update the database with the new user data
err = UpdateUser(user)
if err!= nil {
return err
}
return nil
}
“`
Implementing Debit Transactions with Cobra
The debit
command looks similar to the credit
command. The only difference is the run
function, which reduces a user’s balance while crediting increases the user’s balance.
go
func run(args []string) error {
// get the username from the args array
username := args[0]
user, err := FindOrCreateUser(username)
if err!= nil {
return err
}
// check if the user has enough balance to perform the transaction
if user.Balance < amount {
return errors.New("insufficient balance")
}
// reduce the user's balance and add a new transaction
user.Balance -= amount
transaction := Transaction{Amount: amount, Type: "debit", Narration: narration}
user.Transactions = append(user.Transactions, transaction)
// update the database with the new user data
err = UpdateUser(user)
if err!= nil {
return err
}
return nil
}
With this, we have successfully implemented the credit
and debit
commands using Cobra. Our application can now be built by running go build
.