In the first article we setted up our environment, then we created an HTTP REST API server in the second article and today, we will create our first CLI (Command Line Interface) application in Go.
Initialization
We created our Git repository in the previous article, so now we just have to retrieve it locally:
$ git clone https://github.com/scraly/learning-go-by-examples.git
$ cd learning-go-by-examples
We will create a folder go-gopher-cli
for our CLI application and go into it:
$ mkdir go-gopher-cli
$ cd go-gopher-cli
Now, we have to initialize Go modules (dependency management):
$ go mod init github.com/scraly/learning-go-by-examples/go-gopher-cli
go: creating new go.mod: module github.com/scraly/learning-go-by-examples/go-gopher-cli
This will create a go.mod
file like this:
module github.com/scraly/learning-go-by-examples/go-gopher-cli
go 1.16
Before to start our super CLI application, as good practices, we will create a simple code organization.
Create the following folders organization:
.
├── README.md
├── bin
├── go.mod
That's it? Yes, the rest of our code organization will be created shortly ;-).
Cobra
Cobra is both a library for creating powerful modern CLI applications and a program for generating applications and batch files.
Using Cobra is easy. First, you can use the go get
command to download the latest version. This command will install the cobra library and its dependencies:
$ go get -u github.com/spf13/cobra@latest
Then install the Cobra CLI:
$ go install github.com/spf13/cobra-cli@latest
The cobra
binary is now in the bin/
directory of your $GOPATH, which is itself in your PATH, so you can use it directly.
We will start by generating our CLI application with the cobra init
command followed by the package. The command will generate the application with the correct file structure and imports:
$ cobra-cli init
Your Cobra application is ready at
/workspace/learning-go-by-examples/go-gopher-cli
Our application is initialized, a main.go
file and a cmd/
folder has been created, our code organization is now like this:
.
├── LICENSE
├── bin
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go
Let's create our CLI
What do we want?
Personally, what I like is that when I use a CLI, I want someone to explain to me the goal of the CLI and how to use it.
So, first of all, at the execution of our CLI, we want to display:
- a short description
- a long description
- using our app
In order to do this, we have to modify the cmd/root.go
file:
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "go-gopher-cli",
Short: "Gopher CLI in Go",
Long: `Gopher CLI application written in Go.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
In the root.go
file we have two external imports:
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
We already get cobra so you need now to get viper dependency:
$ go get github.com/spf13/viper@v1.8.1
Our go.mod
file should be like this one:
module github.com/scraly/learning-go-by-examples/go-gopher-cli
go 1.16
require (
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/viper v1.8.1 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/text v0.3.6 // indirect
)
Now, we want to add a get command in our CLI application. For that, we will use the cobra add
command of the cobra CLI:
$ cobra-cli add get
get created at /workspace/learning-go-by-examples/go-gopher-cli
This command add a new get.go
file. Now our application structure should be like this:
.
├── LICENSE
├── bin
├── cmd
│ ├── get.go
│ └── root.go
├── go.mod
├── go.sum
└── main.go
It's time to execute our application:
$ go run main.go
Gopher CLI application written in Go.
Usage:
go-gopher-cli [command]
Available Commands:
completion generate the autocompletion script for the specified shell
get A brief description of your command
help Help about any command
Flags:
--config string config file (default is $HOME/.go-gopher-cli.yaml)
-h, --help help for go-gopher-cli
-t, --toggle Help message for toggle
Use "go-gopher-cli [command] --help" for more information about a command.
By default, an usage message is displayed, perfect!
$ go run main.go get
get called
OK, the get command answered too.
Let's implement our get command
What do we want?
Yes, good question, we want a gopher CLI which will retrieve our favorite Gophers by name!
We will now modify the cmd/get.go
file like this:
var getCmd = &cobra.Command{
Use: "get",
Short: "This command will get the desired Gopher",
Long: `This get command will call GitHub respository in order to return the desired Gopher.`,
Run: func(cmd *cobra.Command, args []string) {
var gopherName = "dr-who.png"
if len(args) >= 1 && args[0] != "" {
gopherName = args[0]
}
URL := "https://github.com/scraly/gophers/raw/main/" + gopherName + ".png"
fmt.Println("Try to get '" + gopherName + "' Gopher...")
// Get the data
response, err := http.Get(URL)
if err != nil {
fmt.Println(err)
}
defer response.Body.Close()
if response.StatusCode == 200 {
// Create the file
out, err := os.Create(gopherName + ".png")
if err != nil {
fmt.Println(err)
}
defer out.Close()
// Writer the body to file
_, err = io.Copy(out, response.Body)
if err != nil {
fmt.Println(err)
}
fmt.Println("Perfect! Just saved in " + out.Name() + "!")
} else {
fmt.Println("Error: " + gopherName + " not exists! :-(")
}
},
}
Let's explain this block code step by step
When get
command is called:
- we first initialize a variable gopherName with our default Gopher name
- then we retrieve the gopher name passed in parameter
- we initialize the URL of the gopher
- and finally we log it
var gopherName = "dr-who"
if len(args) >= 1 && args[0] != "" {
gopherName = args[0]
}
URL := "https://github.com/scraly/gophers/raw/main/" + gopherName + ".png"
fmt.Println("Try to get '" + gopherName + "' Gopher...")
Then, we:
- try to retrieve the gopher thanks to net/http package
- if gopher is retrieved, we create a file and put the image content into it
- else, we log an error message
// Get the data
response, err := http.Get(URL)
if err != nil {
fmt.Println(err)
}
defer response.Body.Close()
if response.StatusCode == 200 {
// Create the file
out, err := os.Create(gopherName + ".png")
if err != nil {
fmt.Println(err)
}
defer out.Close()
// Writer the body to file
_, err = io.Copy(out, response.Body)
if err != nil {
fmt.Println(err)
}
fmt.Println("Perfect! Just saved in " + out.Name() + "!")
} else {
fmt.Println("Error: " + gopherName + " not exists! :-(")
}
Defer???
Wait a minute please, what is it?
There is a simple but important rule that is language agnostic: if you open a connection, you must close it! :-)
Forgetting to close a connection / a response body... can cause resource/memory leaks in a long running programs.
That's why the defer exists. So in our code, we open a connection to the file when we create it and then we defer the execution of out.Close() which will be executed at the end of the function:
// Create the file
out, err := os.Create(gopherName + ".png")
if err != nil {
fmt.Println(err)
}
defer out.Close()
So the best practice is to add your closing statement with a defer word right after your opening, so you don't forget it.
Test it!
Let's test the help of our get
command right now:
$ go run main.go get -h
This get command will call GitHub respository in order to return the desired Gopher.
Usage:
go-gopher-cli get [flags]
Flags:
-h, --help help for get
Global Flags:
--config string config file (default is $HOME/.go-gopher-cli.yaml)
And now it's time to test our get command:
$ go run main.go get friends
Try to get 'friends' Gopher...
Perfect! Just saved in friends.png!
We can also test with an unknown Gopher:
$ go run main.go get awesome
Try to get 'awesome' Gopher...
Error: awesome not exists! :-(
Build it!
Your application is now ready, you just have to build it.
For that, like the previous article, we will use Taskfile in order to automate our common tasks.
So, for this app too, I created a Taskfile.yml
file with this content:
version: "3"
tasks:
build:
desc: Build the app
cmds:
- GOFLAGS=-mod=mod go build -o bin/gopher-cli main.go
run:
desc: Run the app
cmds:
- GOFLAGS=-mod=mod go run main.go
clean:
desc: Remove all retrieved *.png files
cmds:
- rm *.png
Thanks to this, we can build our app easily:
$ task build
task: [build] GOFLAGS=-mod=mod go build -o bin/gopher-cli main.go
$ ll bin/gopher-cli
-rwxr-xr-x 1 aurelievache staff 9,7M 16 jul 21:10 bin/gopher-cli
Let's test it again with our fresh executable binary:
$ ./bin/gopher-cli get 5th-element
Try to get '5th-element' Gopher...
Perfect! Just saved in 5th-element.png!
$ file 5th-element.png
5th-element.png: PNG image data, 1156 x 882, 8-bit/color RGBA, non-interlaced
Cool! :-)
... And clean it!
Each time you will execute the CLI app, an image file will be created locally, so if you want to clean your folder after gophers retrieving, I created a clean task :-)
$ task clean
task: [clean] rm *.png
Goodbye Gophers!
Conclusion
As we have seen in this article, it's possible to create a simple CLI application in few minutes, thanks to cobra and viper, two awesome Go libraries.
All the code is available in: https://github.com/scraly/learning-go-by-examples/tree/main/go-gopher-cli
In the following articles we will create others kind/types of applications in Go.
Hope you'll like it.
Top comments (7)
There might be a very minute change that is required. By default get request gets dr-who gopher, but in the default gopherName you have mentioned complete dr-who.png and then the another .png gets added which returns an error. So you might want to change the line from:
var gopherName = "dr-who.png"
to:
var gopherName = "dr-who"
thanks, I've changed the app so maybe I added an issue :)
I'll take a look :)
What a Brilliant Blog on Building CLI in Go. I have one Doubt i didn't see any where we used viper used.
Thank you
thanks for the article!!
This is pretty cool but I have a question here, how do I pass in arguments when using the task command? I know I can use the executable file in the bin directory instead but how do I do the same thing with the run task?
If that is not possible, then what is the reason for that task?
Hi, yes it's possible to define variable for example: taskfile.dev/#/usage?id=dynamic-va...
Thank you, I guess I should have looked at the documentation first, my bad.