In this article by Aaron Torres, author of the book, Go Cookbook, we will cover the following recipes:
(For more resources related to this topic, see here.)
This article will expand on other uses for these arguments by constructing a command that supports nested subcommands. This will demonstrate Flagsets and also using positional arguments passed into your application.
This recipe requires a main function to run. There are a number of third-party packages for dealing with complex nested arguments and flags, but we'll again investigate doing so using only the standard library.
You need to perform the following steps for the installation:
All code will be run and modified from this directory.
package main
import (
"flag"
"fmt"
"os"
)
const version = "1.0.0"
const usage = `Usage:
%s [command]
Commands:
Greet
Version
`
const greetUsage = `Usage:
%s greet name [flag]
Positional Arguments:
name
the name to greet
Flags:
`
// MenuConf holds all the levels
// for a nested cmd line argument
type MenuConf struct {
Goodbye bool
}
// SetupMenu initializes the base flags
func (m *MenuConf) SetupMenu() *flag.FlagSet {
menu := flag.NewFlagSet("menu", flag.ExitOnError)
menu.Usage = func() {
fmt.Printf(usage, os.Args[0])
menu.PrintDefaults()
}
return menu
}
// GetSubMenu return a flag set for a submenu
func (m *MenuConf) GetSubMenu() *flag.FlagSet {
submenu := flag.NewFlagSet("submenu", flag.ExitOnError)
submenu.BoolVar(&m.Goodbye, "goodbye", false, "Say goodbye instead of hello")
submenu.Usage = func() {
fmt.Printf(greetUsage, os.Args[0])
submenu.PrintDefaults()
}
return submenu
}
// Greet will be invoked by the greet command
func (m *MenuConf) Greet(name string) {
if m.Goodbye {
fmt.Println("Goodbye " + name + "!")
} else {
fmt.Println("Hello " + name + "!")
}
}
// Version prints the current version that is
// stored as a const
func (m *MenuConf) Version() {
fmt.Println("Version: " + version)
}
package main
import (
"fmt"
"os"
"strings"
)
func main() {
c := MenuConf{}
menu := c.SetupMenu()
menu.Parse(os.Args[1:])
// we use arguments to switch between commands
// flags are also an argument
if len(os.Args) > 1 {
// we don't care about case
switch strings.ToLower(os.Args[1]) {
case "version":
c.Version()
case "greet":
f := c.GetSubMenu()
if len(os.Args) < 3 {
f.Usage()
return
}
if len(os.Args) > 3 {
if.Parse(os.Args[3:])
}
c.Greet(os.Args[2])
default:
fmt.Println("Invalid command")
menu.Usage()
return
}
} else {
menu.Usage()
return
}
}
$./cmdargs -h
Usage:
./cmdargs [command]
Commands:
Greet
Version
$./cmdargs version
Version: 1.0.0
$./cmdargs greet
Usage:
./cmdargs greet name [flag]
Positional Arguments:
name
the name to greet
Flags:
-goodbye
Say goodbye instead of hello
$./cmdargs greet reader
Hello reader!
$./cmdargs greet reader -goodbye
Goodbye reader!
If you copied or wrote your own tests go up one directory and run go test, and ensure all tests pass.
Flagsets can be used to set up independent lists of expected arguments, usage strings, and more. The developer is required to do validation on a number of arguments, parsing in the right subset of arguments to commands, and defining usage strings. This can be error prone and requires a lot of iteration to get it completely correct.
The flag package makes parsing arguments much easier and includes convenience methods to get the number of flags, arguments, and more. This recipe demonstrates basic ways to construct a complex command-line application using arguments, including a package-level config, required positional arguments, multi-leveled command usage, and how to split these things into multiple files or packages if needed.
Unix pipes are useful when passing the output of one program to the input of another. Consider the following example:
$ echo "test case" | wc -l
1
In a Go application, the left-hand side of the pipe can be read in using os.Stdin and acts like a file descriptor. To demonstrate this, this recipe will take an input on the left-hand side of a pipe and return a list of words and their number of occurrences. These words will be tokenized on white space.
Refer to the Getting Ready section of the Using command-line arguments recipe.
package main
import (
"bufio"
"fmt"
"os"
)
// WordCount takes a file and returns a map
// with each word as a key and it's number of
// appearances as a value
func WordCount(f *os.File) map[string]int {
result := make(map[string]int)
// make a scanner to work on the file
// io.Reader interface
scanner := bufio.NewScanner(f)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
result[scanner.Text()]++
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading input:", err)
}
return result
}
func main() {
fmt.Printf("string: number_of_occurrencesnn")
for key, value := range WordCount(os.Stdin) {
fmt.Printf("%s: %dn", key, value)
}
}
go build
echo "some string" | ./pipes
You should see the following output:
$ echo "test case" | go run pipes.go
string: number_of_occurrences
test: 1
case: 1
$ echo "test case test" | go run pipes.go
string: number_of_occurrences
test: 2
case: 1
If you copied or wrote your own tests, go up one directory and run go test, and ensure that all tests pass.
Working with pipes in go is pretty simple, especially if you're familiar with working with files.
This recipe uses a scanner to tokenize the io.Reader interface of the os.Stdin file object. You can see how you must check for errors after completing all of the reads.
Coloring an ANSI terminal application is handled by a variety of code before and after a section of text that you want colored. This recipe will explore a basic coloring mechanism to color the text red or keep it plain. For a more complete application, take a look at https://github.com/agtorre/gocolorize, which supports many more colors and text types implements the fmt.Formatter interface for ease of printing.
Refer to the Getting Ready section of the Using command line arguments recipe.
package ansicolor
import "fmt"
//Color of text
type Color int
const (
// ColorNone is default
ColorNone = iota
// Red colored text
Red
// Green colored text
Green
// Yellow colored text
Yellow
// Blue colored text
Blue
// Magenta colored text
Magenta
// Cyan colored text
Cyan
// White colored text
White
// Black colored text
Black Color = -1
)
// ColorText holds a string and its color
type ColorText struct {
TextColor Color
Text string
}
func (r *ColorText) String() string {
if r.TextColor == ColorNone {
return r.Text
}
value := 30
if r.TextColor != Black {
value += int(r.TextColor)
}
return fmt.Sprintf("33[0;%dm%s33[0m", value, r.Text)
}
package main
import (
"fmt"
"github.com/agtorre/go-cookbook/chapter2/ansicolor"
)
func main() {
r := ansicolor.ColorText{ansicolor.Red, "I'm red!"}
fmt.Println(r.String())
r.TextColor = ansicolor.Green
r.Text = "Now I'm green!"
fmt.Println(r.String())
r.TextColor = ansicolor.ColorNone
r.Text = "Back to normal..."
fmt.Println(r.String())
}
go build
./example
You should see the following with the text colored if your terminal supports the ANSI coloring format:
$ go run main.go
I'm red!
Now I'm green!
Back to normal...
If you copied or wrote your own tests, go up one directory and run go test, and ensure that all the tests pass.
This application makes use of a struct keyword to maintain state of the colored text. In this case, it stores the color of the text and the value of the text. The final string is rendered when you call the String() method, which will either return colored text or plain text depending on the values stored in the struct. By default, the text will be plain.
In this article, we demonstrated basic ways to construct a complex command-line application using arguments, including a package-level config, required positional arguments, multi-leveled command usage, and how to split these things into multiple files or packages if needed. We saw how to work with Unix pipes and explored a basic coloring mechanism to color text red or keep it plain.
Further resources on this subject: