In this codelab you will learn the basics of writing a Go CLI tool.

What you'll learn

The steps marked with a 🎁 are optional.

What you'll learn

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.

🐧 Linux

snap

Run:

sudo snap install go --classic

tarbal

Follow the instructions at https://golang.org/doc/install#tarball

🍏 macOS

brew

Run:

brew install go

tarbal

Download the package file at https://golang.org/dl/, open it, and follow the prompts.

🏁 Windows

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.

git

Run:

git clone https://github.com/nlepage/catption.git

zip

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!

Run 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.

Format the message

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.

Read command line arguments

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.

What we've covered

What you'll learn

Cobra is a library for creating powerful modern CLI applications.

Cobra

Cobra provides:

πŸ‘€ 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
}

Describe the command

⌨ Fill the Use and Long fields of the cmdCommand struct, then execute go run hello.go -h to see the result.

Implement the command

⌨ 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.

Version the command

⌨ 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.

What we've covered

What you'll learn

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:

  1. Create a catption by loading a JPEG file
  2. Setup the catption's parameters
  3. Write the catption to out.jpg

However the variables used to setup the catption have not been initialized.

Define flags

⌨ In the init function, setup cmd's flags:

⌨ 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:

πŸŽ‰ Congratulations! You have completed chapter 3.

What we've covered

What you'll learn

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.

Viper

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.

Define default config values

⌨ Before the call to ReadInConfig, define the default value for the "dirs" config key (use the value of the dirs var).

Read config values

⌨ After the call to ReadInConfig, set the value of the dirs var using the "dirs" config key.

Create a config file

⌨ 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.

What we've covered

What you'll learn

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.

What we've covered

What you'll learn

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.

What we've covered

What you'll learn

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.

What we've covered

What you'll learn

We 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:

⌨ 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:

⌨ 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.

What we've covered

πŸŽ‰ Congratulations! You have completed the codelab!

You now know the basics to build you own CLI with Go.

What we've covered

The fully working catption CLI source is available at the repositories root.