Announcing Major Updates to Lightstep’s Distributed Tracing:

RCA in Three Clicks!

Technical


Getting Real with Command Line Arguments and go-flags


Ben Brown

by Ben Brown

Explore More Technical Blogs

Ben Brown

by Ben Brown


02-04-2020

Looking for Something?

No results for 'undefined'

At Lightstep, we automate or build tooling around repeated operational processes (like deployments). In service of this, we have a need for sophisticated command line argument parsing. go-flags gives us the power we need to write useful tools while delegating the complexities of argument parsing to a library.

Some of go-flags features are difficult to discover; this post is intended to be an unofficial guide to some of those features.

The Basics

The inputs for go-flags are the actual command line arguments and a structure describing the expected arguments for the program. go-flags relies on Go struct tags to describe how to interpret the arguments. go-flags then reads and unmarshals command line arguments into the appropriate type.

In the following example, there is one argument, Name, which defaults to the name “Unknown”. It can be set with the –name command line argument.

type Options struct {
   Name string `long:"name" description:"Your name, for a greeting" default:"Unknown"`
}
func main() {
   var opts Options
   parser := flags.NewParser(&opts, flags.Default)
   _, err := parser.Parse()
   if err != nil {
   log.Fatal(err)
   }
   fmt.Println("Hello", opts.Name)
}

Custom Types

At times you need to pass a command line argument that is fundamentally not a basic type (string, int, etc.). To accommodate this use case, go-flags provides support for types that know how to marshal and unmarshal themselves.

We can consider the example of an IPv4 address, used by a fictional tool that checks for host liveness.

During argument parsing, we want to ensure that the IP address supplied is well-formed IP. We also want to parse the IP so that we can do bulk operations - maybe checking liveness over a set of addresses.

The way to do this is to declare a custom type that implements the Marshaler interfaces.

In the following example, we can see what that looks like. In this case, the IP address is supplied in dot-decimal notation (xxx.yyy.zzz.qqq) with the –address command line argument.

The structure of the IP address is validated during parsing, as is the range of each octet.

Note: There is a gotcha here. The IPAddress struct must be a pointer in the options struct. The method receivers on MarshalFlag and UnmarshalFlag must also be pointers.

type IPAddress struct {
   net.IP
}
type Options struct {
   Address *IPAddress `long:"address" required:"true"`
}
func (a *IPAddress) UnmarshalFlag(arg string) error {
   v := net.ParseIP(arg)
   if v == nil {
   return fmt.Errorf("%q failed to parse", arg)
   }
   *a = IPAddress{v}
   return nil
}
func (a *IPAddress) MarshalFlag() (string, error) {
   return a.IP.String(), nil
}
func main() {
   var opts Options
   parser := flags.NewParser(&opts, flags.Default)
   _, err := parser.Parse()
   if err != nil {
   log.Fatal(err)
   }
   fmt.Println("Hello", opts.Address)
}

Nested Commands

Nested commands are useful when developing a monolithic binary with many distinct use cases.

The pattern typically goes liketool_binary subcategory command. We can refactor the above example to use this pattern, as shown in the example below.

There are a few things to notice here. One is that the parser is implicitly the top-level command, and subcommands are directly attached to it (via the AddCommand calls).

Another thing to notice is that Commands can be nested - the net command is not directly executable (as it has no Execute method); it exists purely for organization.

The last thing to notice is that the command that is actually executable, check-liveness, must implement the Commander interface. With this implemented, the below program can be run with the arguments net check-liveness --address 10.0.0.1.

type CheckLivenessCommand struct {
  Address *IPAddress `long:"address" required:"true"`
}
func (c *CheckLivenessCommand) Execute(args []string) error {
  fmt.Println("Checking liveness for", c.Address)
  return nil
}
func addNetCommands(parser *flags.Parser) error {
  cmd, err := parser.AddCommand(
      "net",
      "networking utils",
      "Utilities developed to ease operation and debugging of network connected services.",
      &struct{}{},
  )
  if err != nil {
      return err
  }
  const livenessHelp = "Checks the liveness of the specified IP"
  cmd, err = cmd.AddCommand(
      "check-liveness",
      livenessHelp, // short (--help)
      livenessHelp, // long (manpages)
      &CheckLivenessCommand{},
  )
  if err != nil {
      return err
  }
  return nil
}
func main() {
  var opts struct{}
  parser := flags.NewParser(&opts, flags.Default)
  err := addNetCommands(parser)
  if err != nil {
      log.Fatal(err)
  }
  _, err = parser.Parse()
  if err != nil {
      log.Fatal(err)
  }
}

Restricted Argument Values

go-flags provides a facility to restrict the set of legal values for an argument. These are called choices, and can be used on basic types like strings. This is useful to restrict the services your tool operates on, or the environments it operates in.

The example below illustrates the usage of choice. go-flags will validate that Service is one of the two choices.

type Options struct {
   Service string `long:"service" choice:"proxy" choice:"database"`
}

Common Gotcha — required:”false”

The idiomatic way to set an argument as required is to apply the struct tag required:”true”. The gotcha here is that any string - not just “true” - will set the tag as required. In particular, required:”false” will mark the argument as required.

Command Line Arguments in Practice

go-flags has allowed us to build full featured command line tools where argument parsing just works. Hopefully the above lessons learned will make your usage a little bit smoother.

If you want to learn more about how we build software and write code at Lightstep, check out our walkthrough on how we built our recent Slack integration.

Explore More Technical Blogs