Dealing with Command Line Options in Golang: flag package

Photo by Jason Leung on Unsplash

If you decide to write a command line tool, you’ll most likely want to get user input as command line options and arguments. There are mainly three ways to handle this.

  • os.Args function
  • flag package
  • Libraries: go-flags, cli, cobra

I’m going to look at flag package in this post.

This is not about how to write a command line tool, and I don’t cover handling environmental variables nor subcommands.

Example

Let’s assume we’re supposed to parse the following command in the following sections.

./go-curl -v -X "GET" https://example.com

where -v is a flag to print verbose message, -X is an option to specify the HTTP method, and it takes a url as an argument.

The flag package

flag is go official utility package that makes it easy to parse command line input. You can take options in bool, int, uint, string, time.Duration, float, and types you define. I show you the entire code first:

func main() {
v := flag.Bool("v", false, "Makes curl verbose during the operation.")
var X string
flag.StringVar(&X, "X", "GET", "(HTTP) Specifies a custom request method to use when communicating with the HTTP server.")
flag.Parse() args := flag.Args() // do curl here
fmt.Printf("v: %t, X: %s, args: %v\n", *v, X, args)
os.Exit(0)
}

There are two ways to define options.

1. Declare the variable as a pointer type

v := flag.Bool("v", false, "Makes  curl  verbose  during the operation.")

The functions Bool, String, Duration, etc. returns a pointer value that contains the option value. You can specify the option name, the default value, and the description. As it’s a pointer, you need to dereference (*v) when accessing it. By using a bool value, you can define the option without a parameter.

2. Use ~Var function

var X string
flag.StringVar(&X, "X", "GET", "(HTTP) Specifies a custom request method to use when communicating with the HTTP server.")

You can declare the variable first and call the function with Var suffix. Although it becomes multiple lines, you don’t need to care about pointers.

After declaring the options, you need call Parse function before using them.

flag.Parse()

Once you parse, you can get the arguments by calling flag.Args(). This is exactly the values after all the options. For example, if the input is ./go-curl -v arg -opt value, the function returns ["arg", "-opt", "value"].

What if user put wrong options?

The process exit with code 2 as shown below:

$ ./go-curl -unknown-option
flag provided but not defined: -unknown-option
Usage of ./go-curl:
-X string
(HTTP) Specifies a custom request method to use when communicating with the HTTP server. (default "GET")
-v Makes curl verbose during the operation.
$ echo $?
2

However, you might want to return your own exit code on exception. Or you might want to continue the process without exiting. This is where FlagSet comes in.

FlagSet

flag package uses FlagSet struct underneath. When you call flag functions without receiver, it delegates the process to the default FlagSet instance. If you want to change the exceptional behaviour you have three options of how to deal with it.

  • ContinueOnError … Parse method returns error so you can handle it as you like.
  • ExitOnError … Parse exits with status code 2.
  • PanicOnError … Parse calls panic on exception.

For example, if you want to return 100 as exit status code on exception:

func main() {
fs := flag.NewFlagSet("curl", flag.ContinueOnError)
v := fs.Bool("v", false, "Makes curl verbose during the operation.")
var X string
fs.StringVar(&X, "X", "GET", "(HTTP) Specifies a custom request method to use when communicating with the HTTP server.")

if err := fs.Parse(os.Args[1:]); err != nil {
os.Exit(100)
}
...
}

(Be aware fs.Parse takes []string as input as opposed to the previous section.)

You can use FlagSet to implement subcommands as well. See the references below for details.

FlagSet prints nice error message to stderr like you saw in the previous section. Occasionally you might want to suppress this message in testing for example. There’s a function SetOutput to change the output destination.

References

Conclusion

One drawback of the flag package is its syntax as it doesn’t follow the UNIX conventions. Or, you might find it’s not satisfactory in implementing complex subcommands. The libraries such as go-cmdline or cobra could be your help.

--

--