In this codelab you will learn the basics of writing a Go CLI tool.
os, os/exec and flag packagesgithub.com/spf13/cobra CLI librarygithub.com/spf13/viper config librarycobra and viper togethergithub.com/sirupsen/logrus logging libraryThe steps marked with a π are optional.
os)flag)In order to go through this codelab, you are going to need a working Go development environment.
The minimum required version is Go 1.13.
Run:
sudo snap install go --classic
Follow the instructions at https://golang.org/doc/install#tarball
Run:
brew install go
Download the package file at https://golang.org/dl/, open it, and follow the prompts.
Download the MSI file at https://golang.org/dl/, open it, and follow the prompts.
There are two ways of downloading the codelab contents.
The prefered way is git, which will allow you to keep track of your work and revert things if needed.
Run:
git clone https://github.com/nlepage/catption.git
Download https://github.com/nlepage/catption/archive/master.zip and unzip it.
The last thing you need is a Go friendly IDE.
If you don't already have one, here are some popular IDEs for Go:
Now open the codelab contents and you are ready π·, let's Go!
hello.goIn πcatption/codelab/chapter1 you will find a classic hello.go:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
β¨ Execute this program by running go run hello.go.
We would like to replace World by a variable in our message.
β¨ Create a new string variable:
var recipient = "Gopher"
β¨ Use fmt.Printf() to format the message with recipient.
As you can see the main function of a Go program has no parameters.
The command line arguments are available in the Args variable of the os package.
β¨ Use os.Args to fill the recipient variable.
Flags allow to change the behavior of commands, like the -r flag of rm which enables recursive removal.
The flag package allows to parse the flags contained in os.Args.
We would like our command to have a -u flag which uppercases the message:
$ hello -u capslock
HELLO CAPSLOCK!
β¨ Explore the flag package and parse the -u flag in hello.go.
π Congratulations! You have completed chapter 1.
os)flag)github/spf13/cobraCobra is a library for creating powerful modern CLI applications.

Cobra provides:
app server, app fetch, etc.cobra init appname & cobra add cmdnameapp srver... did you mean app server?)-h, --help, etc.π Explore cobra's documentation and API.
Let's see how to recreate our hello command using Cobra.
In πcatption/codelab/chapter2 you will find a new hello.go with the skeleton of a cobra app:
package main
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
var cmd = &cobra.Command{
RunE: func(_ *cobra.Command, args []string) error {
return nil
},
}
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
func sayHello(args []string) error {
if _, err := fmt.Printf("Hello %s!\n", strings.Join(args, " ")); err != nil {
return err
}
return nil
}
β¨ Fill the Use and Long fields of the cmdCommand struct, then execute go run hello.go -h to see the result.
β¨ Call sayHello in the RunE function of cmd in order to have a working hello command, execute go run hello.go cobra to see the result.
β¨ Finally fill the Version field of cmd, then execute go run hello-go --version to see the result.
Our hello command needs at least one command line argument.
β¨ Fill the Args field of cmd with the correct value in order to raise an error if hello doesn't receive any arguments.
π Congratulations! You have completed chapter 2.
github/spf13/cobraEnough of hello messages, let's start writing our cat caption CLI π±
In πcatption/codelab/chapter3 you will find a catption.go with a new command:
var (
top, bottom string
size, fontSize, margin float64
cmd = &cobra.Command{
Use: "catption",
Long: "Cat caption generator CLI",
Args: cobra.ExactArgs(1),
Version: "chapter3",
RunE: func(_ *cobra.Command, args []string) error {
var name = args[0]
cat, err := catption.LoadJPG(name)
if err != nil {
return err
}
cat.Top, cat.Bottom = top, bottom
cat.Size, cat.FontSize, cat.Margin = size, fontSize, margin
return cat.SaveJPG("out.jpg")
},
}
)
This command does 3 things:
out.jpgHowever the variables used to setup the catption have not been initialized.
β¨ In the init function, setup cmd's flags:
top and bottom string flagssize, fontSize and margin float flags (Use catption.DefaultSize, catption.DefaultFontSize and catption.DefaultMargin as default values)β¨ Play around with your new command, some pictures are available in πcats/
Flags shorthands allow users to type more concise commands.
β¨ Add some shorthands to cmd:
-t for --top-b for --bottom-s for --sizeπ Congratulations! You have completed chapter 3.
github.com/spf13/viperViper is a complete configuration solution for Go applications including 12-Factor apps.
It is designed to work within an application, and can handle all types of configuration needs and formats.

It supports:
π Explore viper's documentation and API.
Specifying the full path to the input JPEG file is not very userfriendly...
Let's use a config file to define directories where catption should look for JPEG files.
In πcatption/codelab/chapter4 the catption command now has a PreRunE function:
PreRunE: func(_ *cobra.Command, _ []string) error {
viper.SetConfigName("catption")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
return nil
},
This function tries to load a catption.* config file in the current directory.
β¨ Before the call to ReadInConfig, define the default value for the "dirs" config key (use the value of the dirs var).
β¨ After the call to ReadInConfig, set the value of the dirs var using the "dirs" config key.
β¨ Create a catption.* config file with the directories where you want catption to look for JPEG files.
Example catption.yaml:
dirs:
- "."
- "../../cats"
You can now try your configuration: go run catption.go -t "Hello" -b "World" dinner.jpg
Many applications read there config file from the user's config directory ($HOME/Library/Application Support on macOS for example).
β¨ Call viper.AddConfigPath a second time to read catption's config file from the user's config directory, in addition of current the directory.
π Congratulations! You have completed chapter 4.
github.com/spf13/viperSome of our users don't want to use config files.
We would like to offer them the possibility to override the dirs config key with a flag.
Luckily viper has the ability to read config values from cobra!
β¨ Create a new dir flag with the type slice of strings.
β¨ Bind the dir flag to viper's dirs config key.
Try it out: go run catption.go -t "Hello" -b "World" --dir "../../cats" --dir "." dinner.jpg
One of our users would like to deploy catption on a kubernetes cluster.
The easiest way for him/her to specify the input files directories is to use an environment variable.
β¨ Use viper's API to read the dirs config key from a CATPTION_DIRS environment variable.
Try it out: CATPTION_DIRS="../../cats" go run catption.go -t "Hello" -b "World" dinner.jpg
π Congratulations! You have completed chapter 5.
Some of our users don't know how to create a config file and add directories to it.
Let's help them by adding a new dir subcommand to catption, which will add a directory to the config file.
In πcatption/codelab/chapter6 we now have a dirCmd command, and a addDir function which implements adding a new directory to the config file.
β¨ Fill the fields of dirCmd: Use, Long, Args and RunE
β¨ In the init function, add dirCmd as a subcommand to cmd
Using a constant value for cmd's Version field is not very useful.
It would be nice to set this variable at compile time, with a git tag or commit hash.
β¨ Create a version variable at package level, and set cmd.Version's value with this variable.
β¨ Try changing the binary's version with build flags: go build -ldflags "-X main.version=1.0.0"
π Congratulations! You have completed chapter 6.
github.com/sirupsen/logrusWe've added some logs to catption using a library called logrus.
However we would like to be able to set the log level using a flag.
In πcatption/codelab/chapter7 we now have a logLevel variable used to set the log level.
This variable has the type logrus.Level.
In order to create a flag with a custom type, you must implement pflag's Value interface.
This is already done by the type logLevelValue:
type logLevelValue logrus.Level
var _ pflag.Value = new(logLevelValue)
func (l *logLevelValue) Set(value string) error {
lvl, err := logrus.ParseLevel(value)
if err != nil {
return err
}
*l = logLevelValue(lvl)
return nil
}
func (l *logLevelValue) String() string {
return logrus.Level(*l).String()
}
func (l *logLevelValue) Type() string {
return "string"
}
β¨ In the init function, create a new --logLevel flag for the logLevel variable.
It is possible to perform a type cast between pointer types, here is an example:
type Celsius float64
func example() {
var temperature float64
measureTemperature((*Celsius)(&temperature))
fmt.Println("temp:", temperature)
}
// measureTemperature stores a new measure in the t pointer
func measureTemperature(t *Celsius)
π Have a look at logrus's documentation and API
β¨ Add some new logs in catption.
π Congratulations! You have completed chapter 7.
github.com/sirupsen/logrusos/exec packageWe would like catption to open an image viewer as soon as the image has been written to disk.
Most operating systems have commands to open the appropriate viewer for a file:
xdg-open command on π§ Linuxopen command on π macOSstart command on π Windowsβ¨ Use the os/exec package to execute the appropriate command for your OS and display the image.
Some users don't have the same OS as you.
We would like to cross-compile catption to other systems, but the command for opening a viewer is system dependent!
The go compiler is able to include/exclude source files, based on their suffix.source_darwin.go will only be compiled when targeting macOS systems.
β¨ Create 3 files with each an openCmd string const:
open_linux.go for Linuxopen_darwin.go for macOSopen_windows.go for Windows⨠Use openCmd to call exec.Command
One of our users would like to run catption on a FreeBSD system.
xdg-open is also available on this system, it would be nice to use the same openCmd const for Linux and FreeBSD.
β¨ Rename open_linux.go to open_xdg.go.
β¨ Add build tags to open_xdg.go in order to target Linux and FreeBSD.
π Congratulations! You have completed chapter 8.
os/exec packageπ Congratulations! You have completed the codelab!
You now know the basics to build you own CLI with Go.
os, os/exec and flag packagesgithub.com/spf13/cobra CLI librarygithub.com/spf13/viper config librarycobra and viper togethergithub.com/sirupsen/logrus logging libraryThe fully working catption CLI source is available at the repositories root.