This tutorial will guide us through building an interactive CLI app with Go, Cobra and promptui. We will learn how to prompt the user for input data and persist this data to an SQLite database. Unlike the previous app 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:
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:
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 databasestudybuddy note- displays information about commands related to notesstudybuddy note new- opens a prompt to collect data from the userstudybuddy 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
| |
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
| |
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
| |
To add module requirements and sums, let’s run
| |
Then we should also update descriptions in cmd/root.go file
| |
And that’s our basic cobra app. Let’s build it and try it out. Run the following command to build the current project
| |
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
| |
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
| |
Then let’s install the go-sqlite3 package so we can use it in our project
| |
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
| |
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
dbto 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 forerrthat we will use later in thesql.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 thedbor theerrvariables. We want to assign the return value fromsql.Open()to the package-leveldbvariable 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
| |
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:
| |
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 calledcreateTableSQLto hold the SQL statement we need to create a table with the necessary columns. i.e.idNote,word,definition,category - We prepare the
SQLstatement using thedb.Prepare()method on the package-leveldbvariable 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
| |
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
| |
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.
| |
Still in the cmd/init.go file, we will:
- import the
datapackage we just created - Inside the
Runfunction of theinitCmd, we will call the function we just created above. i.e.CreateTable()
| |
Now we can build our app
| |
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.
| |
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
| |
Now we just have to do two things:
- First, we have to update the command descriptions
- Second, we have to remove the
Runfield. RemovingRunmakes this command behave more like thehelpcommand
Our whole noteCmd variable should look like this:
| |
And that’s it for the base note command. Let’s build the app
| |
Then run the binary from within the project directory
| |
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:
| |
For this command as well, we will need to update the descriptions
| |
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:
| |
Every prompt has a similar shape, so we can go ahead and create a custom promptContent struct type
| |
We should probably also import the promptui package along with the other packages we need before we continue
| |
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
| |
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.
| |
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
indexto-1because this index will never actually be present insideitems - As long as the index is
-1, the prompt is kept open and more categories can be appended to theitemsslice - Once the user chooses an item, the prompt will close and the result will be returned
| |
Once we’re done with our promptGetSelect function, we can return to our createNewNote() function to capture the category
| |
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:
| |
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
| |
At the bottom of our createNewNote() function, call data.InsertNote() and pass in the word, definition and category data we collected from the user:
| |
Then to tie everything up, we have to call our completed createNewNote() function inside Run:
| |
Let’s build everything so we can test our changes
| |
Then run the note new command to trigger out little “interview”:
| |
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
SQLquery to select everything from thestudybuddytable and order the results byword - 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
| |
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:
| |
Now we can open up cmd/list.go and update it. In the following code:
- We import the
datapackage. - We update the
ShortandLongdescriptions - We call
data.DisplayAllNotes()insideRun
| |
Build the app
| |
And run the following command to see a list of all our notes:
| |
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
testyourself. Display each word in a question and execute a prompt to collect theanswerto 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. Of course, you can use the same techniques to build all sorts of other fun projects. You’ll be able to find all the code in github repo for this project.
Congratulations, you did great. Keep learning and keep coding. Bye for now.

