Last updated at Mon, 26 Oct 2020 18:58:40 GMT

Go offers a simple way to build command-line tools using only standard libraries. So I put together a step-by-step example to help walk you through the process.

To write a Go program, you’ll need Go setup up on your computer. If you’re not familiar with Go and want to spend a little extra time learning, you can take the Go tour to get started!

In this example, we’ll create a command-line tool called stringparse, that will count the characters, words, or lines in a string. Here's our inteded usage to start (we'll add in some subcommands and more flags later):

 -metric string     Metric {chars|words|lines}. (default "chars")  -text string     Text to parse. (required)  -unique     Measure unique values of a metric.

Setting Up the Initial File

Create the main file for our tool stringparse.go.

package main 

func main() {
    // Execution of CLI tool behavior goes here 
}

That’s it!
To run and test your app as you’re building it, run the following:

$ go build stringparse.go
$ ./stringparse

Parsing Arguments

Arguments, Options, Flags -- What’s the difference?
These terms are often used interchangeably. In general, they refer to the list of strings following a CLI command. Here are the traditional definitions:

  • Arguments are all strings that follow a CLI command.
  • Options are arguments with dashes (single or double) that are followed by user input and modify the operation of the command.
  • Flags are boolean options that do not take user input.

Go does not stick to these definitions. In the context of its flag package, all strings beginning with dashes (single or double) are considered flags, and all strings without dashes that are not user input are considered arguments. All strings after the first argument are considered arguments.

Flag Package
Go’s flag package offers a standard library for parsing command line input.

Flags can be defined using flag.String(), Bool(), Int(), etc.

examplePtr := flag.String("example", "defaultValue", " Help text.")

examplePtr will reference the value for either -example or --example. The initial value at *examplePtr is “defaultValue”. Calling flag.Parse() will parse the command line input and write the value following -example or --example to *examplePtr.

Flag syntax:
-example -example=text -example text // Non-boolean flags only
- and -- may be used for flags

Let’s add some flags.

package main

import (
    "flag"
    "fmt"
)

func main() {
    textPtr := flag.String("text", "", "Text to parse.")
    metricPtr := flag.String("metric", "chars", "Metric {chars|words|lines};.")
    uniquePtr := flag.Bool("unique", false, "Measure unique values of a metric.")
    flag.Parse()

    fmt.Printf("textPtr: %s, metricPtr: %s, uniquePtr: %t\n", *textPtr, *metricPtr, *uniquePtr)
}

Boolean flags: A boolean flag will parse to a value of true if the flag has been set and no user input was provided. Using a default value of false will allow you to determine if the flag was set. A boolean flag may be set with user input using an “=”, but it's not required (see package docs for details).

There are a few more methods for creating flags using the flag package not mentioned here. If you’re interested, they're described in the package docs.

Arguments: After parsing, the arguments following the flags are available as the slice flag.Args() or individually as flag.Arg(i). The arguments are indexed from 0 through flag.NArg()-1.

Error Handling
Go automatically checks that user input is provided for all flags that require it. It also verifies that all flags provided are known. If either of these conditions are not met, a usage message will be automatically generated using the help text from each flag.

flag.PrintDefaults() can be used to print the usage message explicitly, in cases where you want to do any error handling yourself.

Determine if a Flag Was Set
To check if a flag was set, you can compare the value at *flagPtr to the flag's default value after parsing. In this context, you may want to choose the zero value of the flag’s type as the flag's default value. This avoids the possibility of a user providing the default value as input.

However, this method becomes tricky when the flag's type is Int or Float, since the zero value is 0, and that may be valid user input. Consult the docs for values allowed for Int and Float. You’ll have to get creative.

Make a Flag Required
To make an argument required, determine if the flag was set and then handle accordingly. If the required flag was not set, the user should be notified and program execution should be discontinued.

Let’s update the text flag to be required.

package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    textPtr := flag.String("text", "", "Text to parse. (Required)")
    metricPtr := flag.String("metric", "chars", "Metric {chars|words|lines};. (Required)")
    uniquePtr := flag.Bool("unique", false, "Measure unique values of a metric.")
    flag.Parse()

    if *textPtr == "" {
        flag.PrintDefaults()
        os.Exit(1)
    }

    fmt.Printf("textPtr: %s, metricPtr: %s, uniquePtr: %t\n", *textPtr, *metricPtr, *uniquePtr)
}

Choice Flags
A choice flag must have user input from within a certain set. To implement a choice flag, create a set of values, and check that the user input is in the set. If input is not in the set, notify the user accordingly and exit.

Sub Commands
Subcommands can be useful when your app needs to perform multiple functions, and each function has a unique set of flags and arguments. A common example of a CLI tool with subcommands is git:

git **commit** -m "message" git **push**

Subcommands are implemented by creating a flag.FlagSet. The FlagSet has a subcommand name, error handling behavior, and a set of flags associated with it. Add flags to your FlagSet by creating them the same way you would on your main command. The flag package contains constants for different error handling behaviors, which can be found in the package docs.

Let’s add two new subcommands, list and count. For the count subcommand, let's add a new metric choice called substring, and a new --substring flag. We'll implement choice flag functionality for the --metric flag as well. Please read the inline comments for an explanation of all implementation details.

package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    // Subcommands
    countCommand := flag.NewFlagSet("count", flag.ExitOnError)
    listCommand := flag.NewFlagSet("list", flag.ExitOnError)

    // Count subcommand flag pointers
    // Adding a new choice for --metric of 'substring' and a new --substring flag
    countTextPtr := countCommand.String("text", "", "Text to parse. (Required)")
    countMetricPtr := countCommand.String("metric", "chars", "Metric {chars|words|lines|substring}. (Required)")
    countSubstringPtr := countCommand.String("substring", "", "The substring to be counted. Required for --metric=substring")
    countUniquePtr := countCommand.Bool("unique", false, "Measure unique values of a metric.")

    // List subcommand flag pointers
    listTextPtr := listCommand.String("text", "", "Text to parse. (Required)")
    listMetricPtr := listCommand.String("metric", "chars", "Metric <chars|words|lines>. (Required)")
    listUniquePtr := listCommand.Bool("unique", false, "Measure unique values of a metric.")

    // Verify that a subcommand has been provided
    // os.Arg[0] is the main command
    // os.Arg[1] will be the subcommand
    if len(os.Args) < 2 {
        fmt.Println("list or count subcommand is required")
        os.Exit(1)
    }

    // Switch on the subcommand
    // Parse the flags for appropriate FlagSet
    // FlagSet.Parse() requires a set of arguments to parse as input
    // os.Args[2:] will be all arguments starting after the subcommand at os.Args[1]
    switch os.Args[1] {
    case "list":
        listCommand.Parse(os.Args[2:])
    case "count":
        countCommand.Parse(os.Args[2:])
    default:
        flag.PrintDefaults()
        os.Exit(1)
    }

    // Check which subcommand was Parsed using the FlagSet.Parsed() function. Handle each case accordingly.
    // FlagSet.Parse() will evaluate to false if no flags were parsed (i.e. the user did not provide any flags)
    if listCommand.Parsed() {
        // Required Flags
        if *listTextPtr == "" {
            listCommand.PrintDefaults()
            os.Exit(1)
        }
        //Choice flag
        metricChoices := map[string]bool{"chars": true, "words": true, "lines": true}
        if _, validChoice := metricChoices[*listMetricPtr]; !validChoice {
            listCommand.PrintDefaults()
            os.Exit(1)
        }
        // Print
        fmt.Printf("textPtr: %s, metricPtr: %s, uniquePtr: %t\n", *listTextPtr, *listMetricPtr, *listUniquePtr)
    }

    if countCommand.Parsed() {
        // Required Flags
        if *countTextPtr == "" {
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        // If the metric flag is substring, the substring flag is required
        if *countMetricPtr == "substring" && *countSubstringPtr == "" {
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        //If the metric flag is not substring, the substring flag must not be used
        if *countMetricPtr != "substring" && *countSubstringPtr != "" {
            fmt.Println("--substring may only be used with --metric=substring.")
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        //Choice flag
        metricChoices := map[string]bool{"chars": true, "words": true, "lines": true, "substring": true}
        if _, validChoice := metricChoices[*listMetricPtr]; !validChoice {
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        //Print
        fmt.Printf("textPtr: %s, metricPtr: %s, substringPtr: %v, uniquePtr: %t\n", *countTextPtr, *countMetricPtr, *countSubstringPtr, *countUniquePtr)
    }

}

Defining Your Own Flag Types (and List Items)
Go allows you to define your own flag types! All you need to do is create a type which implements the flag.Value interface.

type Value interface {
    String() string
    Set(string) error
}

String() should convert your type to a string. Set() will be called during flag.Parse to set the value of your flag from it’s string input. In this example, we’ll create a flag type to represent a comma separated list of strings, and add a new flag to the count subcommand, --substringList. A custom flag type isn't required here, but it is a good example.  Read inline comments for an explanation.

package main

import (
    "flag"
    "fmt"
    "os"
    "strings"
)

// Create a new type for a list of Strings
type stringList []string

// Implement the flag.Value interface
func (s *stringList) String() string {
    return fmt.Sprintf("%v", *s)
}

func (s *stringList) Set(value string) error {
    *s = strings.Split(value, ",")
    return nil
}

func main() {
    // Subcommands
    countCommand := flag.NewFlagSet("count", flag.ExitOnError)
    listCommand := flag.NewFlagSet("list", flag.ExitOnError)

    // Count subcommand flag pointers
    // Adding a new choice for --metric of 'substring' and a new --substring flag
    countTextPtr := countCommand.String("text", "", "Text to parse. (Required)")
    countMetricPtr := countCommand.String("metric", "chars", "Metric {chars|words|lines|substring}. (Required)")
    countSubstringPtr := countCommand.String("substring", "", "The substring to be counted. Required for --metric=substring")
    countUniquePtr := countCommand.Bool("unique", false, "Measure unique values of a metric.")

    // Use flag.Var to create a flag of our new flagType
    // Default value is the current value at countStringListPtr (currently a nil value)
    var countStringList stringList
    countCommand.Var(&countStringList, "substringList", "A comma seperated list of substrings to be counted.")

    // List subcommand flag pointers
    listTextPtr := listCommand.String("text", "", "Text to parse. (Required)")
    listMetricPtr := listCommand.String("metric", "chars", "Metric <chars|words|lines>. (Required)")
    listUniquePtr := listCommand.Bool("unique", false, "Measure unique values of a metric.")

    // Verify that a subcommand has been provided
    // os.Arg[0] is the main command
    // os.Arg[1] will be the subcommand
    if len(os.Args) < 2 {
        fmt.Println("list or count subcommand is required")
        os.Exit(1)
    }

    // Switch on the subcommand
    // Parse the flags for appropriate FlagSet
    // FlagSet.Parse() requires a set of arguments to parse as input
    // os.Args[2:] will be all arguments starting after the subcommand at os.Args[1]
    switch os.Args[1] {
    case "list":
        listCommand.Parse(os.Args[2:])
    case "count":
        countCommand.Parse(os.Args[2:])
    default:
        flag.PrintDefaults()
        os.Exit(1)
    }

    // Check which subcommand was Parsed using the FlagSet.Parsed() function. Handle each case accordingly.
    // FlagSet.Parse() will evaluate to false if no flags were parsed (i.e. the user did not provide any flags)
    if listCommand.Parsed() {
        // Required Flags
        if *listTextPtr == "" {
            listCommand.PrintDefaults()
            os.Exit(1)
        }
        //Choice flag
        metricChoices := map[string]bool{"chars": true, "words": true, "lines": true}
        if _, validChoice := metricChoices[*listMetricPtr]; !validChoice {
            listCommand.PrintDefaults()
            os.Exit(1)
        }
        // Print
        fmt.Printf("textPtr: %s, metricPtr: %s, uniquePtr: %t\n",
            *listTextPtr,
            *listMetricPtr,
            *listUniquePtr,
        )
    }

    if countCommand.Parsed() {
        // Required Flags
        if *countTextPtr == "" {
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        // If the metric flag is substring, the substring or substringList flag is required
        if *countMetricPtr == "substring" && *countSubstringPtr == "" && (&countStringList).String() == "[]" {
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        //If the metric flag is not substring, the substring flag must not be used
        if *countMetricPtr != "substring" && (*countSubstringPtr != "" || (&countStringList).String() != "[]") {
            fmt.Println("--substring and --substringList may only be used with --metric=substring.")
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        //Choice flag
        metricChoices := map[string]bool{"chars": true, "words": true, "lines": true, "substring": true}
        if _, validChoice := metricChoices[*listMetricPtr]; !validChoice {
            countCommand.PrintDefaults()
            os.Exit(1)
        }
        //Print
        fmt.Printf("textPtr: %s, metricPtr: %s, substringPtr: %v, substringListPtr: %v, uniquePtr: %t\n",
            *countTextPtr,
            *countMetricPtr,
            *countSubstringPtr,
            (&countStringList).String(),
            *countUniquePtr,
        )
    }
}  

Help Text
It’s important for your CLI tool to have a message directing users on how to run it. It’s good practice to include a --help or -h flag with your command, which will print usage instructions.

Go will create this usage message for you, by combining the the help text fields of all your flags. The message can be printed explicitly using flag.PrintDefaults(). Usage, help, and man pages commonly use a small syntax to describe the valid command form:

  • angle brackets for required parameters: ping <hostname>
  • square brackets for optional parameters: mkdir [-p] <dirname>
  • ellipses for repeated items: cp <source1> [source2...] <dest>
  • vertical bars for choice of items: netstat {-t|-u}

I haven't strictlly followed this pattern in my example, but using it can make it clear what the use cases are.

Make It Runnable from the Command-Line

$ go build stringparse.go

This creates an executable file, stringparse. All we need to do now is copy this file to a directory on our system path. The simplest way to do this is to copy to /usr/local/bin.

$ cp stringparse /usr/local/bin

Testing It Out
Now that we're finished it's time to run our CLI tool. Right now, stringparse will parse the flags and print the data (below are some examples). Once you reach this point, you're free to implement the functionality needed using the data you've now parsed. Happy coding!

$ stringparse list --text "Hackers beware." --metric words --unique
textPtr: Hackers beware., metricPtr: words, uniquePtr: true
$ stringparse count --text "She sells sea shells by the sea shore" --metric substring --substringList se,sh,ea,he
textPtr: She sells sea shells by the sea shore, metricPtr: substring, substringPtr: , substringListPtr: [se sh ea he], uniquePtr: false
$ stringparse count --text "Komand Security"
textPtr: Komand Security, metricPtr: chars, substringPtr: , substringListPtr: [], uniquePtr: false

Conclusion
I hope you've enjoyed our example on building CLI tools with Go. We also write articles on things we've learned from our tech stack, too:

   

NEVER MISS A BLOG

   

Get the latest stories, expertise, and news about security today.