When developers aren’t in their IDE text editors, they’re usually in the terminal.
As a developer, chances are high that you have used a command line interface (CLI) for your projects.
Most developer tools run on the command line for one main reason: easy configuration. CLI applications allow a degree of freedom that is not easily found in graphical user interface (GUI) applications.
Cobra is a Go library for building CLI applications. It is pretty popular and used in a lot of popular developer tools like the Github CLI, Hugo, and more.
In this tutorial, we’ll learn about Cobra by building a simple accounting CLI application that bills users, stores the information in a JSON file, records receipts, and tracks a user’s total balance.
Installing Cobra
There are two ways to create a Cobra application:
- Installing the Cobra Generator
- Manually adding Cobra to a Go application
For this tutorial, we’ll install the Cobra Generator. This provides an easy way to generate commands that give life to the application.
To begin, run the following command to install the Cobra Generator:
go get github.com/spf13/cobra/cobra
This installs Cobra in the GOPATH
directory to then generate Cobra applications.
Understanding Cobra CLI commands and flags
Before we can start building our app, we must understand the main components of a CLI
application.
When using Git to clone a project, we usually run the following:
git clone <url.to.project>
This includes:
git
, the application nameclone
, the commandurl.to.project
, the argument passed to the command and the project we wantgit
toclone
A CLI application usually comprises the application’s name, the command, flags, and arguments.
Consider this example:
npm install --save-dev nodemon
Here, npm
is the application that’s running and install
is the command. --save-dev
is a flag that passes to the install
command, while nodemon
is the argument passed to the command.
Cobra allows us to create commands and add flags to them really easily. For our application, we’ll create two commands: credit
and debit
. And, using various flags, we can specify items like the user making the transaction, the amount transacted, and the narration for the transaction.
Creating the Cobra app
To create a new Cobra application, run the following command:
cobra init --pkg-name github.com/<username>/accountant 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 github.com/<username>/accountant go mod tidy
We can now run this as we would any normal Go application:
go run .
However, we could also build the app via the following:
go build .
And the run the application via the following:
./accountant
The Cobra app entry point
The entry point to our Cobra app is main.go
and it is important 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:
cmd.Execute()
The root command, cmd/root.go,
contains the following:
- The
rootCmd
struct, which is a type ofcobraCommand
- The
Execute
function, which is called inmain.go
- The
init
function, which initializes the config and sets up the root flags - The
initConfig
function, which initializes any set configurations
Currently, running the application contains a bunch of Cobra-generated text. Let’s change that by modifying cmd\root.go
to the following so we can explain what our app is for:
package cmd import ( "github.com/spf13/cobra" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "accountant", Short: "An application that helps manage accounts of users", Long: ` This is a CLI that enables users to manage their accounts. You would be able to add credit transactions and debit transactions to various users. `, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { cobra.CheckErr(rootCmd.Execute()) }
Running the application should now give the following response:
This is a CLI that enables users to manage their accounts. You would be able to add credit transactions and debit transactions to various users. Usage: accountant [command]
Here, we removed the init
and initConfig
functions that Cobra generated. That is because we don’t need any environment variables for this application, and the root command is not doing much. Instead, all the features for the application are carried out by specific commands.
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 credit cobra add debit
This creates two new files: debit.go
and credit.go
in the /cmd
directory.
Upon inspecting the newly created files, add the following into the init
function:
rootCmd.AddCommand(debitCmd)
This line of code adds the newly-created command to the root command; now, the application is aware of the new command.
To run the debitCmd
command, we must build the application via go build .
and run the application like so:
./accountant debit
Adding a JSON storage layer
For this application, we’ll use a very simple storage layer. In this case, 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:
package database import ( "encoding/json" "os" "strings" ) type User struct { Username string `json:"username"` Balance int64 `json:"balance"` Transactions []Transaction `json:"transactions"` } type Transaction struct { Amount int64 `json:"amount"` Type string `json:"string"` Narration string `json:"narration"` } func getUsers() ([]User, error) { data, err := os.ReadFile("database/db.json") var users []User if err == nil { json.Unmarshal(data, &users) } return users, err } func updateDB(data []User) { bytes, err := json.Marshal(data) if err == nil { os.WriteFile("database/db.json", bytes, 0644) } else { panic(err) } } func FindUser(username string) (*User, error) { users, err := getUsers() if err == nil { for index := 0; index < len(users); index++ { user := users[index] if strings.EqualFold(user.Username, username) { return &user, nil } } } return nil, err } func FindOrCreateUser(username string) (*User, error) { user, err := FindUser(username) if user == nil { var newUser User newUser.Username = strings.ToLower(username) newUser.Balance = 0 newUser.Transactions = []Transaction{} users, err := getUsers() if err == nil { users = append(users, newUser) updateDB(users) } return &newUser, err } return user, err } func UpdateUser(user *User) { // Update the json with this modified user information users, err := getUsers() if err == nil { for index := 0; index < len(users); index++ { if strings.EqualFold(users[index].Username, user.Username) { // Update the user details users[index] = *user } } // update database updateDB(users) } }
Here, we define two structures: User
and Transaction
. The User
structure defines how to store and access a user’s information, such as a username
, balance
and transactions
. The Transaction
structure stores the transactions, including the amount
, type
and narration
.
We also have two functions that write to the database. getUsers
loads the database file and returns the stored user data while updateDB
writes the updated data to the database.
These functions are private to this package and need public functions for the commands to interact with them.
FindUser
finds a user in the database with a username and returns the user. If no user is found, it returns nil
. FindOrCreateUser
checks if there is a user with a username and returns it; if there is no user, it creates a new user with that user name and returns it.
UpdateUser
receives user data and updates the corresponding entry in the database.
These three functions are exported to use in commands when crediting and debiting users.
Implementing credit transactions with Cobra
Modify the credit
command with the following to create an adequate description for the command and add a usage section in the long description:
// cmd/credit.go var creditCmd = &cobra.Command{ Use: "credit", Short: "Create a credit transaction", Long: ` This command creates a credit transaction for a particular user. Usage: accountant credit <username> --amount=<amount> --narration=<narration>.`, Run: func(cmd *cobra.Command, args []string) { }, }
The long description then appears when a user tries to get help for this command.
Next, we must add the necessary flags for the credit
command: amount
and narration
.
Add the following after the creditCmd
definition:
var creditNarration string var creditAmount int64 func init() { rootCmd.AddCommand(creditCmd) creditCmd.Flags().StringVarP(&creditNarration, "narration", "n", "", "Narration for this credit transaction") creditCmd.Flags().Int64VarP(&creditAmount, "amount", "a", 0, "Amount to be credited") creditCmd.MarkFlagRequired("narration") creditCmd.MarkFlagRequired("amount") }
In the init
method, we attach the creditCmd
command to the root
command via rootCmd.AddCommand
.
Next, we must create a string flag, narration
, using the StringVarP
method. This method receives five parameters:
- A pointer to the variable where the value of the flag is stored
- The name of the flag
- A short name for the flag
- A default value for the flag
- A help message is provided when the user asks for help via the
--help
flag
Also, we must create a new flag, amount
, via the Int64VarP
method. This method is similar to StringVarP
but creates a 64bit integer flag.
After that, we must set both flags as required. By doing this, whenever the command is called without those flags, Cobra outputs an error stating that the flags are required.
Completing the credit command, we use the database functions to create transactions and add them to the users.
To do this, modify the run
function to look like the following:
var creditCmd = &cobra.Command{ ... Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { log.Fatal("Username not specified") } username := args[0] user, err := database.FindOrCreateUser(username) if err != nil { log.Fatal(err) } user.Balance = user.Balance + creditAmount creditTransaction := database.Transaction{Amount: creditAmount, Type: "credit", Narration: creditNarration} user.Transactions = append(user.Transactions, creditTransaction) database.UpdateUser(user) fmt.Println("Transaction created successfully") }, }
The run
function is the most important part of the command because it handles the main action of the command.
So, we want the command to have the following signature:
./accountant credit <username> --amount=<amount> --narration<narration>
The argument sent to the command here is the username
, more specifically, the first item in the args
array. This ensures that there is at least one argument passed to the command.
After getting the username, we can use the FindOrCreateUser
method from the database package to get the corresponding user information with that username.
If that operation is successful, we increment the user’s balance and add a new transaction with the amount and narration. Then, we update the database with the new user data.
Putting everything together, the credit command should look like this:
package cmd import ( "fmt" "log" "github.com/jameesjohn/accountant/database" "github.com/spf13/cobra" ) // creditCmd represents the credit command var creditCmd = &cobra.Command{ Use: "credit", Short: "Create a credit transaction", Long: ` This command creates a credit transaction for a particular user. Usage: accountant credit <username> --amount=<amount> --narration=<narration>.`, Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { log.Fatal("Username not specified") } username := args[0] user, err := database.FindOrCreateUser(username) if err != nil { log.Fatal(err) } user.Balance = user.Balance + creditAmount creditTransaction := database.Transaction{Amount: creditAmount, Type: "credit", Narration: creditNarration} user.Transactions = append(user.Transactions, creditTransaction) database.UpdateUser(user) fmt.Println("Transaction created successfully") }, } var creditNarration string var creditAmount int64 func init() { rootCmd.AddCommand(creditCmd) creditCmd.Flags().StringVarP(&creditNarration, "narration", "n", "", "Narration for this credit transaction") creditCmd.Flags().Int64VarP(&creditAmount, "amount", "a", 0, "Amount to be credited") creditCmd.MarkFlagRequired("narration") creditCmd.MarkFlagRequired("amount") }
With this, we have successfully implemented the credit
command.
Implementing debit transactions with Cobra
The debit
command looks similar to the credit
command. The only difference is the run
function. Debit
reduces a user’s balance while credit
increases the user’s balance.
The debit
command should look like the following:
./accountant debit <username> --amount=<amount> --narration=<narration>
The difference of the run
function for debit
comes when checking that the user’s balance is greater than the amount debited; we wouldn’t want to have negative balances in our database.
To do this, modify debit.go
to look like the following:
package cmd import ( "fmt" "log" "github.com/jameesjohn/accountant/database" "github.com/spf13/cobra" ) // debitCmd represents the debit command var debitCmd = &cobra.Command{ Use: "debit", Short: "Create a debit transaction", Long: ` This command creates a debit transaction for a particular user. Usage: accountant debit <username> --amount=<amount> --narration=<narration>.`, Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { log.Fatal("Username not specified") } username := args[0] user, err := database.FindOrCreateUser(username) if err != nil { log.Fatal(err) } if user.Balance > debitAmount { user.Balance = user.Balance - debitAmount debitTransaction := database.Transaction{Amount: debitAmount, Type: "debit", Narration: debitNarration} user.Transactions = append(user.Transactions, debitTransaction) database.UpdateUser(user) fmt.Println("Transaction created successfully") } else { fmt.Println("Insufficient funds!") } }, } var debitNarration string var debitAmount int64 func init() { rootCmd.AddCommand(debitCmd) debitCmd.Flags().StringVarP(&debitNarration, "narration", "n", "", "Narration for this debit transaction") debitCmd.Flags().Int64VarP(&debitAmount, "amount", "a", 0, "Amount to be debited") debitCmd.MarkFlagRequired("narration") debitCmd.MarkFlagRequired("amount") }
If the user has enough balance to perform the transaction, we reduce their balance by the amount debited, create a new debit transaction, and add the transaction to the user. Finally, we update the database with the updated user.
If the user does not have enough balance, we output an error message stating that they have an insufficient balance.
We can now use the accountant
to debit users:
./accountant debit henry --amount=40 --narration="Paid James"
The application can now be built by running go build
.
Conclusion
We just learned how to use Cobra to build CLI apps! Considering the amount of work Cobra does for us, it’s not hard to see why popular open source applications and tools use it for their CLI applications.
This project can be found here.
Get setup with LogRocket's modern error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID.
- Install LogRocket via NPM or script tag.
LogRocket.init()
must be called client-side, not server-side. - (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- ngrx middleware
- Vuex plugin
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>