How to build an interactive CLI app with Go, Cobra & promptui
Tags: go, tutorial, beginners
Published: 2021-07-05 11:56:22

*Originally posted on [divrhino.com](https://divrhino.com/articles/build-interactive-cli-app-with-go-cobra-promptui/)*

{% youtube so3VZwdWcBg %}

This tutorial will guide us through building an interactive CLI app with [Go](https://golang.org/dl/), [Cobra](https://github.com/spf13/cobra) and [promptui](https://github.com/manifoldco/promptui). We will learn how to prompt the user for input data and persist this data to an [SQLite](https://sqlite.org/index.html) database. Unlike the [previous app](https://dev.to/divrhino/building-a-command-line-tool-with-go-and-cobra-3mjd) we built, this experience will feel more like a two way conversation.

## Prerequisites

To follow along with this tutorial, you will need to have Go and the Cobra generator installed.

**Installation guides:**

- [Go](https://golang.org/dl/)
- [Cobra Generator](https://github.com/spf13/cobra/blob/master/cobra/README.md)

## Demo of what we're building

We will build a note-taking app called `Studybuddy`. The app will ask us a series of questions and save our answers as a note. We will then be able to see all the notes we've saved. The following video demonstrates some interactive behaviour we will build:

![Interactive CLI app demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vohrewlvddnjaozkawpg.gif)

## Commands we will implement

This article will focus on implementing interactivity with `promptui`. We will create several commands to achieve our goal.

- `studybuddy init` - creates a database
- `studybuddy note` - displays information about commands related to notes
- `studybuddy note new` - opens a prompt to collect data from the user
- `studybuddy note list` - displays all the notes we've created

## Setting up a cobra app

We will be using the cobra package to give us the functionality we need to build our CLI app. We will only need the ability to create a handful of `commands` and `subcommands`, so our `cobra` app will be very simple.

Starting in our `Sites` folder, or wherever you keep your projects, we will create a new project folder called `studybuddy`. Then we will change into it

```bash
mkdir -p studybuddy && cd studybuddy
```

Using the cobra generator, we can initialise a new cobra app. It is a good idea to name your project using the URL where it can be downloaded. I will use my Github repo URL as the name of my package. Please feel free to substitute the following command with your own Github account or personal website

```bash
cobra init --pkg-name github.com/divrhino/studybuddy
```

We will be using `go modules` to manage our project dependencies, so we can set it up using the same package name we used in the above `cobra init` command

```bash
go mod init github.com/divrhino/studybuddy
```

To add module requirements and sums, let's run

```bash
go mod tidy
```

Then we should also update descriptions in `cmd/root.go` file

```go
Use:   "studybuddy",
Short: "Use studybuddy to learn and retain vocabulary",
Long:  `Learn a new language with the studybuddy CLI app by your side`,
```

And that's our basic `cobra` app. Let's build it and try it out. Run the following command to build the current project

```bash
go build .
```

An executable binary will be created in your project folder. We have not installed it in our `GOPATH` so we can't execute it simply by running `studybuddy` in the terminal. Instead we can run it relative to the current project directory

```bash
./studybuddy
```

If all has gone according to plan, we should get a print out in the terminal showing a bunch of information about our new CLI app.

## Opening the database connection

In order to read and write to our database, we will need to open the database connection pool. To do this, we must first create a new `SQLite` database. As the name suggests, `SQLite` is a "lite" database, so it is less complicated than something like `Postgres`. This makes it a great choice for a situation like ours where the primary focus is not the `database`.

All our database-related functionality should be kept in an appropriate place. Let's create a `data` folder and also create a file called `data.go` within it

```bash
mkdir data
touch data/data.go
```

Then let's install the `go-sqlite3` package so we can use it in our project

```bash
go get github.com/mattn/go-sqlite3
```

Opening up `data/data.go`, we can start by importing the `database/sql` and `go-sqlite3` packages. These are the 2 packages we need to work with `SQLite` in our app

```go
package data

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
)
```

We can create our function that will open up the database connection pool. We can start writing our code after the imports. In the following code:

- We set up a package-level variable called `db` to hold our database connection pool. This variable will be used in several functions.
- We set up `OpenDatabase()` function.
- Then inside the body of `OpenDatabase()`, we declare a variable for `err` that we will use later in the `sql.Open()` function
- Next we use `sql.Open()` to open up a connection pool, passing in our driver name (`sqlite3`) and the path to our database. Notice that we are not re-declaring the `db` or the `err` variables. We want to assign the return value from `sql.Open()` to the package-level `db` variable we declared previously
- We do some quick error handling before we move on
- Then we just return a `db.Ping()` to verify that the connection is alive

```go
var db *sql.DB

func OpenDatabase() error {
	var err error

	db, err = sql.Open("sqlite3", "./sqlite-database.db")
	if err != nil {
		return err
	}

	return db.Ping()
}
```

Now that our `OpenDatabase()` function is ready, will call it from `func main()`. We do this so the entire app can have access to the connection pool. Our entire `main.go` file should look something like this:

```go
package main

import (
	"github.com/divrhino/studybuddy/cmd"
	"github.com/divrhino/studybuddy/data"
)

func main() {
	data.OpenDatabase()
	cmd.Execute()
}
```

## Creating the database table

Next, we will need to set up a table into which we can insert our study notes.

We can create a function to create a new table called `studybuddy`. Inside `data/data.go`, and in the following code:

- we set up `CreateTable()` function
- Inside the body of the `CreateTable()` function, we set up a variable called `createTableSQL` to hold the SQL statement we need to create a table with the necessary columns. i.e. `idNote`, `word`, `definition`, `category`
- We prepare the `SQL` statement using the `db.Prepare()` method on the package-level `db` variable we created earlier
- If `db.Prepare()` returns an error, we do some quick error handling
- Then we execute the statement using `Exec()`
- Here as well, just so we can see what's happening, we log a message using `log.Println`

```go
func CreateTable() {
	createTableSQL := `CREATE TABLE IF NOT EXISTS studybuddy (
		"idNote" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
		"word" TEXT,
		"definition" TEXT,
		"category" TEXT
	  );`

	statement, err := db.Prepare(createTableSQL)
	if err != nil {
		log.Fatal(err.Error())
	}

	statement.Exec()
	log.Println("Studybuddy table created")
}
```

We will be using this `CreateTable()` function in our `init` command, which we will create next.

## The `init` command

Our first custom command will be the `init` command. We want to run this command to create a new database table. The database connection pool will also open because once we start using the app, `func main()` will call the `OpenDatabase()` function.

Making sure we're in our project directory, let's create our `init` command

```bash
cobra add init
```

Then let's open the newly created `cmd/init.go` file to quickly update the `short` and `long` descriptions. Our short and long descriptions do not have to be the same. But since we don't want to spend too much time worrying about the content here, we will just have them be the same.

```go
Short: "Initialise a new studybuddy database and table",
Long:  `Initialise a new studybuddy database and table.`,
```

Still in the `cmd/init.go` file, we will:

- import the `data` package we just created
- Inside the `Run` function of the `initCmd`, we will call the function we just created above. i.e. `CreateTable()`

```go
package cmd

import (
	"github.com/divrhino/studybuddy/data"
	"github.com/spf13/cobra"
)

var initCmd = &cobra.Command{
	Use:   "init",
	Short: "Initialise a new studybuddy database and table",
	Long:  `Initialise a new studybuddy database and table.`,
	Run: func(cmd *cobra.Command, args []string) {
		data.CreateTable()
	},
}

func init() {
	rootCmd.AddCommand(initCmd)
}
```

Now we can build our app

```bash
go build .
```

In the terminal, we can now run the following command. We will know that it's working when a file called `sqlite-database.db` appears in our project root.

```bash
./studybuddy init
```

## The base `note` command

With the initial database setup sorted out, we can move on to adding commands that are directly related to note-taking. The next command we will implement is the base `note` command. This command won't actually do a whole lot. It will just allow us to get more information about its `subcommands`

Using the cobra generator, we can go ahead and `add` the `note` command

```bash
cobra add note
```

Now we just have to do two things:

- First, we have to update the command descriptions
- Second, we have to remove the `Run` field. Removing `Run` makes this command behave more like the `help` command

Our whole `noteCmd` variable should look like this:

```go
var noteCmd = &cobra.Command{
	Use:   "note",
	Short: "A note can be anything you'd like to study and review.",
	Long:  `A note can be anything you'd like to study and review.`,
}
```

And that's it for the base `note` command. Let's build the app

```bash
go build .
```

Then run the binary from within the project directory

```bash
./studybuddy note
```

If everything was built correctly, we should get a print out in the terminal with some information about our new `note` command.

## The `note new` command

This is the most involved section of the tutorial because this is where the bulk of the interactivity occurs. Here we will utilise `promptui` to help us accept user input data. Then we will persist it to our database.

 

First we need to create a `subcommand` under the previously-created `note` command. When used, the `subcommand` will take this form: `studybuddy note new`. In the terminal, execute the following line:

```bash
cobra add new -p 'noteCmd'
```

For this command as well, we will need to update the descriptions

```go
Use:   "new",
Short: "Creates a new studybuddy note",
Long:  `Creates a new studybuddy note`,
```

We will need a way to get user input. We can use the `promptui` package to help us. We will have to install it first by running the following command in the terminal:

```bash
go get github.com/manifoldco/promptui
```

Every prompt has a similar shape, so we can go ahead and create a custom `promptContent` struct type

```go
type promptContent struct {
	errorMsg string
	label    string
}
```

We should probably also import the `promptui` package along with the other packages we need before we continue

```go
package cmd

import (
	"errors"
	"fmt"
	"os"

	"github.com/manifoldco/promptui"
	"github.com/spf13/cobra"
)
```

A prompt from the `promptui` package has a few key concepts:

- validate - a function to validate the user's input
- templates - used to style text for different state messages
- prompt - the actual prompt struct. Running the prompt will allow us to collect data from the user. This feedback will be stored as `result`

```go
func promptGetInput(pc promptContent) string {
	validate := func(input string) error {
		if len(input) <= 0 {
			return errors.New(pc.errorMsg)
		}
		return nil
	}

	templates := &promptui.PromptTemplates{
		Prompt:  "{{ . }} ",
		Valid:   "{{ . | green }} ",
		Invalid: "{{ . | red }} ",
		Success: "{{ . | bold }} ",
	}

	prompt := promptui.Prompt{
		Label:     pc.label,
		Templates: templates,
		Validate:  validate,
	}

	result, err := prompt.Run()
	if err != nil {
		fmt.Printf("Prompt failed %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Input: %s\n", result)

	return result
}
```

After prompting the user for data, we want to use this data to create a new note. Using the custom `promptContent` struct we created, above, we will set up a prompt to capture a `word` and a `definition` for the word.

```go
func createNewNote() {
	wordPromptContent := promptContent{
		"Please provide a word.",
		"What word would you like to make a note of?",
	}
	word := promptGetInput(wordPromptContent)

	definitionPromptContent := promptContent{
		"Please provide a definition.",
		fmt.Sprintf("What is the definition of %s?", word),
	}
	definition := promptGetInput(definitionPromptContent)
}
```

While we can prompt the user for text `input`, we can also give the user some options to choose from. We achieve this by using `SelectWithAdd` from `promptui`. In the following code:

- We start with some pre-determined `items` , which will be of type `[]string`
- We give the user the option of adding their own category by adding the label `Other`
- We set the initial `index` to `-1` because this index will never actually be present inside `items`
- As long as the index is `-1`, the prompt is kept open and more categories can be appended to the `items` slice
- Once the user chooses an item, the prompt will close and the result will be returned

```go
func promptGetSelect(pc promptContent) string {
	items := []string{"animal", "food", "person", "object"}
	index := -1
	var result string
	var err error

	for index < 0 {
		prompt := promptui.SelectWithAdd{
			Label:    pc.label,
			Items:    items,
			AddLabel: "Other",
		}

		index, result, err = prompt.Run()

		if index == -1 {
			items = append(items, result)
		}
	}

	if err != nil {
		fmt.Printf("Prompt failed %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Input: %s\n", result)

	return result
}
```

Once we're done with our `promptGetSelect` function, we can return to our `createNewNote()` function to capture the `category`

```go
func createNewNote() {
	wordPromptContent := promptContent{
		"Please provide a word.",
		"What word would you like to make a note of?",
	}
	word := promptGetInput(wordPromptContent)

	definitionPromptContent := promptContent{
		"Please provide a definition.",
		fmt.Sprintf("What is the definition of the %s?", word),
	}
	definition := promptGetInput(definitionPromptContent)

	categoryPromptContent := promptContent{
		"Please provide a category.",
		fmt.Sprintf("What category does %s belong to?", word),
	}
	category := promptGetSelect(categoryPromptContent)
}
```

We are able to capture input data from the user about notes, but we haven't created a function to insert the notes into our database, so let's do that now. Back in `data/data.go`, we need to create another method that interacts with our database. This time we want to `INSERT` data:

```go
func InsertNote(word string, definition string, category string) {
	insertNoteSQL := `INSERT INTO studybuddy(word, definition, category) VALUES (?, ?, ?)`
	statement, err := db.Prepare(insertNoteSQL)
	if err != nil {
		log.Fatalln(err)
	}
	_, err = statement.Exec(word, definition, category)
	if err != nil {
		log.Fatalln(err)
	}

	log.Println("Inserted study note successfully")
}
```

Now we're ready to insert notes into the database. Jumping back into our new command in the file `new.go`, we'll have to import the `data` package into `cmd/new.go`

```go
import (
	...

	"github.com/divrhino/studybuddy/data"

	...
	
)
```

At the bottom of our `createNewNote()` function, call `data.InsertNote()` and pass in the `word`, `definition` and `category` data we collected from the user:

```go
func createNewNote() {
	wordPromptContent := promptContent{
		"Please provide a word.",
		"What word would you like to make a note of?",
	}
	word := promptGetInput(wordPromptContent)

	definitionPromptContent := promptContent{
		"Please provide a definition.",
		fmt.Sprintf("What is the definition of the %s?", word),
	}
	definition := promptGetInput(definitionPromptContent)

	categoryPromptContent := promptContent{
		"Please provide a category.",
		fmt.Sprintf("What category does %s belong to?", word),
	}
	category := promptGetSelect(categoryPromptContent)

	data.InsertNote(word, definition, category)
}
```

Then to tie everything up, we have to call our completed `createNewNote()` function inside `Run`:

```go
var newCmd = &cobra.Command{
	Use:   "new",
	Short: "Create a new note to study",
	Long:  `Create a new note to study.`,
	Run: func(cmd *cobra.Command, args []string) {
		createNewNote()
	},
}
```

Let's build everything so we can test our changes

```bash
go build .
```

Then run the `note new` command to trigger out little "interview":

```bash
./studybuddy note new 
```

Add a few notes so we have something to see in the next section.

## The `note list` command

This final command will give us the ability to display all our notes. We will need a way to retrieve all our records from the database.

Back in the `data/data.go` files, we can create a function to do that. In the following code:

- we set up a function called `DisplayAllNotes()`
- We then set up an `SQL` query to select everything from the `studybuddy` table and order the results by `word`
- We do some quick error handling
- Then we refer the closing of the row
- We loop through the rows and print out the columns in a loosely formatted string

```go
func DisplayAllNotes() {
	row, err := db.Query("SELECT * FROM studybuddy ORDER BY word")
	if err != nil {
		log.Fatal(err)
	}

	defer row.Close()

	for row.Next() {
		var idNote int
		var word string
		var definition string
		var category string
		row.Scan(&idNote, &word, &definition, &category)
		log.Println("[", category, "] ", word, "—", definition)
	}
}
```

We should also remember to create our `list` subcommand. When used, the `subcommand` will take this form: `studybuddy note list`. In the terminal, execute the following line:

```go
cobra add list -p 'noteCmd'
```

Now we can open up `cmd/list.go` and update it. In the following code:

- We import the `data` package.
- We update the `Short` and `Long` descriptions
- We call `data.DisplayAllNotes()` inside `Run`

```go
package cmd

import (
	"github.com/divrhino/studybuddy/data"
	"github.com/spf13/cobra"
)

// listCmd represents the list command
var listCmd = &cobra.Command{
	Use:   "list",
	Short: "See a list of all notes you've added",
	Long:  `See a list of all notes you've added.`,
	Run: func(cmd *cobra.Command, args []string) {
		data.DisplayAllNotes()
	},
}

func init() {
	noteCmd.AddCommand(listCmd)
}
```

Build the app

```bash
go build .
```

And run the following command to see a list of all our notes:

```bash
./studybuddy note list
```

## Going further

Interactive CLI apps are a simple, yet powerful tool to have at your disposal. You can combine them with webscrapers or other techniques to build custom productivity apps and beef up your personal workflow.

If you’d like to extend this project further, you can consider doing the following:

- Add a feature that lets you `test` yourself. Display each word in a question and execute a prompt to collect the `answer` to test questions.
- Add a feature that allows you to see which words you have trouble remembering.

## Conclusion

In this tutorial we learnt how to build an interactive CLI app that we can use to save fun and interesting new words.

If you enjoyed this article and you'd like more, consider subscribing to [Div Rhino on YouTube](https://www.youtube.com/c/divrhino).

Congratulations, you did great. Keep learning and keep coding. Bye for now, <3.

{% github divrhino/studybuddy %}