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.go
In π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/cobra
Cobra is a library for creating powerful modern CLI applications.
Cobra provides:
app server
, app fetch
, etc.cobra init appname
& cobra add cmdname
app 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 cmd
Command 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/cobra
Enough 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.jpg
However 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/viper
Viper 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/viper
Some 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/logrus
We'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/logrus
os/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.