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.
There are two ways to create a Cobra 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.
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 want git
to clone
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.
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 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:
rootCmd
struct, which is a type of cobraCommand
Execute
function, which is called in main.go
init
function, which initializes the config and sets up the root flagsinitConfig
function, which initializes any set configurationsCurrently, 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.
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
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.
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:
--help
flagAlso, 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.
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
.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]