Making CLIs in Golang with Cobra and PTerm
- Mahmoud Mousa
- Golang
- July 30, 2024
Introduction
Hi everyone, welcome back to the blog! Today, we’re diving into the world of Command Line Interfaces (CLIs) using Go. Go is an excellent language for building CLIs due to its simplicity and efficiency. Many popular tools like Docker, Kubernetes, and Hugo are built using Go. In this tutorial, we’ll create a simple CLI application that accepts user input, saves it to a SQLite database, and lists the stored entries in a neat table.
Tools We’ll Use
- Cobra CLI: A widely-used framework for building CLIs in Go. It powers many well-known tools like Docker and Kubernetes.
- Pterm: A library that provides components and utilities for accepting user input and displaying information in a visually appealing way.
Getting Started
Step 1: Install Cobra-cli
To be able to use Cobra-cli in your machine, you need to install it. Cobra-cli makes using Cobra super simple.
Cobra will also help us add commands, sub-commands and much more. We will explore this soon. For now let’s intall cobra-cli
first.
First, we need to install Cobra CLI. Run the following command:
go install github.com/spf13/cobra-cli@latest
This will install Cobra CLI as an executable on your machine. This also requires you have Go installed on your machine. If you don’t refer to go docs for insturctions on how to do that.
Step 2: Create a Go Project
Starting out our CLI is a go project after all. So we need to add a new project folder and init a go module. I have called this folder go-cli-basics
. Feel free to call it whatever you want.
So let’s create a new directory for the project and initialize a Go module:
mkdir go-cli-basics
cd go-cli-basics
go mod init github.com/yourusername/go-cli-basics
Step 3: Initialize Cobra CLI
Cobra-cli will allow us to scaffold a CLI application super easily. This means Cobra-cli will do the heavy work on your behalf and get things started. This will save us time. Cobra will take care of many things for us. Including author details and license. We won’t cover that here, but you can learn more at cobra website.
For now let’s initialize Cobra CLI in your project directory:
cobra-cli init
After this command is run you should see a cmd
directory and a main.go
file.
.
├── cmd
│ └── root.go
├── go.mod
└── main.go
The cmd
directory is the main module in this project. It will includeall your commands and subcommands; it mainly has business logic and all the code that will run in your CLI. You can also add other modules when needed like utils
- which I usually do - or anything else. This is the basic skeleton for your CLI application.
What does this root.go
file has? Let’s explore more.
Step 4: Explore the Root Command
Open the cmd/root.go
file. You’ll see the root command setup.
You’ll see the root command defined with properties like Use
, Short
, and Long
. These properties define the name and descriptions of your CLI. They will be used to describe your CLIs when you call the CLI command and passing the help flag -h
Open the root.go
file. You’ll see the root command, which is the entry point of your CLI. Here’s a brief overview of the important parts:
- Use: The name of the CLI, which users will type to run it.
- Short and Long Descriptions: Help descriptions that describe what the CLI does.
- Run: The function that runs whenever the root command is triggered.
By default, the root command displays help information. Let’s modify it to print a simple message:
var rootCmd = &cobra.Command{
Use: "cli-basics",
Short: "CLI Basics",
Long: `A simple CLI application built with Cobra.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Root command executed")
},
}
Run the CLI with:
go run main.go
You should see “Root command executed” printed to the terminal.
Using Flags
Flags allow users to pass options to commands. The root command has a default flag called toggle
. Let’s access this flag’s value:
toggle, _ := cmd.Flags().GetBool("toggle")
if toggle {
fmt.Println("Toggle is true")
}
Now, run the CLI with the toggle
flag:
go run main.go --toggle
You should see “Toggle is true” printed to the terminal.
Step 5: Add a New Command with Cobra-cli
Cobra also makes it very easy to add commands to your CLI. You don’t to have to worry about boilerplat code at all and you can focus on your core logic.
Let’s add a new command called add
:
cobra-cli add add
This creates a new file cmd/add.go
. Open it and you’ll see a similar structure to root.go
.
Notice this part of the file:
func init() {
rootCmd.AddCommand(addCmd)
}
This means this command is being added to the root command. So when your CLI is installed, you can call this using CLI_NAME add
Modify the Run
function to accept user input using Pterm.
Step 6: Install Pterm
We can install Pterm very easily as a go package like so. Install Pterm by running:
go get github.com/pterm/pterm
Make sure to visit Pterm website to learn more about the library. It can do a lot more than you think. For this post we will use it to accept input from the user and display data in a table only.
Step 7: Accept User Input using Pterm
So let’s add a command called add
🃟 . In this command we will add new entries to our database. This command will use pterm to accept input from the user.
Modify cmd/add.go
to accept user input:
package cmd
import (
"fmt"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
var addCmd = &cobra.Command{
Use: "add",
Short: "Add a new entry",
Long: `Add a new entry to the database.`,
Run: func(cmd *cobra.Command, args []string) {
firstName, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("Please enter your first name").Show()
lastName, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("Please enter your last name").Show()
fmt.Println("First Name:", firstName)
fmt.Println("Last Name:", lastName)
},
}
func init() {
rootCmd.AddCommand(addCmd)
}
Notice we used DefaultInteractiveTextInput
from pterm
. This is default text input that will pause execution and wait for user to input text.
You can change many options about this default input. For now we simply changed the text that is displayed and we ignored the errors. Make sure you handle errors in your application tho! 😄
Awesome so now we can take user input and store the value. Let’s do something useful with this value now
Step 8: Setup SQLite Database
Let’s setup a super simple Sqlite database. I have another post on how to use SQlite Database with go. You can read that here
If you already know how to do this, we can just keep going for now. We will need to install gorm
and sqlite adapter for go. Gorm is the most widely used ORM for go. It’s a fantastic resource that we can cover in another blog post soon. For now you can learn more about gorm on their website.
Anyways, let’s intall those two needed packages:
go get gorm.io/gorm
go get gorm.io/driver/sqlite
Modify cmd/add.go
to save user input to the database:
package cmd
import (
"fmt"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
FirstName string
LastName string
}
var addCmd = &cobra.Command{
Use: "add",
Short: "Add a new entry",
Long: `Add a new entry to the database.`,
Run: func(cmd *cobra.Command, args []string) {
firstName, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("Please enter your first name").Show()
lastName, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("Please enter your last name").Show()
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
db.AutoMigrate(&User{})
user := User{FirstName: firstName, LastName: lastName}
db.Create(&user)
pterm.Info.Println("Saved info to database")
},
}
func init() {
rootCmd.AddCommand(addCmd)
}
Notice how we added this part here to define the shape of the data in the database:
type User struct {
gorm.Model
FirstName string
LastName string
}
Also notice that we run AutoMigrate
. This command will make the tables in our database in the same way to match our data that we described in this struct.
Those basics are important. Once we run this command, our database is ready to accept entries. We do that by running db.Create
function and passing in a variable of the correct struct that matches the structure in the database.
Now that we can accept input from the user and save it in our database, let’s make another command to list the entries in a nice table from pterm
Step 9: List Entries
Add a new command to list entries:
cobra-cli add list
Modify cmd/list.go
to fetch and display entries from the database:
To do this we will use the default display table in pterm. You can always customise the table, but for our case here just using the simple table is suffient.
package cmd
import (
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var listCmd = &cobra.Command{
Use: "list",
Short: "List all entries",
Long: `List all entries in the database.`,
Run: func(cmd *cobra.Command, args []string) {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
var users []User
db.Find(&users)
tableData := pterm.TableData{{"First Name", "Last Name"}}
for _, user := range users {
tableData = append(tableData, []string{user.FirstName, user.LastName})
}
pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
},
}
func init() {
rootCmd.AddCommand(listCmd)
}
Notice how we added .WithHasHeader().WithData(tableData)
Notice the WithHasHeader
this makes sure we have the header or First Name
and Last Name
in the table.
Notice also how we have to loop over the data and append it to the table data in an elegant way in go. I really love the go syntax although it doesn’t have many bills and whisles.
Running the CLI
To run the CLI, use the following commands:
Add a new entry:
go run main.go add
List all entries:
go run main.go list
You wil be able to see now your data in the table:
Conclusion
In this tutorial, we built a simple CLI application using Go, Cobra, and Pterm. We learned how to:
- Set up a Go project with Cobra CLI.
- Accept user input using Pterm.
- Save user input to a SQLite database using Gorm.
- List entries from the database in a table format.
This is just the beginning. You can expand this project by adding more commands, handling errors, and improving the user interface.
If you enjoyed this post, you will probably find the ones below interesting too.
That's not good! 😢
Thank you! You just made my day! 💙