Using Go os.Args and FlagSet to create cmdline subcommands

TL/DR With Go flag package is very easy to define and parse global command line options. However, if your application needs subcommands, using OS args and creating flag sets is great way to define options for each subcommand.

Imagine if you want to create a clone of taskwarrior with task add and task remove as subcommands. The first step is to parse the command line arguments to discover which subcommand the user is trying to access.

package main

import (
	"fmt"
	"os"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Command missing")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "add":
		// add a task
	case "remove":
		// remove a task
	default:
		fmt.Println("Command not defined")
		os.Exit(1)
	}
}

os.Args is a string slice with the commands passed from the command line. The first element, os.Args[0], is always the name of the program. To get the subcommand, you must first verify that there is indeed at least a second argument, otherwise accessing the second element would be out bounds and crash the program. A switch over os.Args[1] is a quick way to accomplish that. It is always a good practice to add a default case to handle unknown inputs.

Global Flags

The flag package is very handy to define command line flags, such as -v or --source=test. Flags are important even if your program is not a command line application, because it allows you to inject information after compile time. If you are creating a web service, for example, the port number and an api secret could be passed from the command line.

package main

import (
	"flag"
	"fmt"
)

func main() {
	port := flag.String("port", "8080", "Server port")
	flag.Parse()
	fmt.Printf("Running on port %s\n", *port)
}

Both short and long flags are defined by default, so there is no difference between task -port 8080 and task --port 8080.

The flag package also adds little goodies when you call Parse. The first one is a default help text that shows by calling the program with -h or -help. The second one is a warning when flags that are not defined are called.

You cannot, however, define a flag as required. If a flag is vital for your program, you must validate and throw an error manually.

package main

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

func main() {
	secret := flag.String("api-secret", "", "API secret for Foo service")
	flag.Parse()
	if *secret == "" {
		fmt.Println("--api-secret is required")
		os.Exit(1)
	}
}

Flag Sets

When you call flag.String(...) and flag.Parse() you are actually using a global flag set defined inside the flag package. If you need more control over the flags, like setting error behaviors for example, creating you own flag set(s) is the way to go.

package main

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

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Command missing")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "add":
		f := flag.NewFlagSet("add", flag.ExitOnError)
		tag := f.String("tag", "", "Default task tag")
		f.Parse(os.Args[2:])
		fmt.Println(*tag)
	default:
		fmt.Println("Command not defined")
		os.Exit(1)
	}
}

Using flag sets is good way to isolate flag options by subcommand. In this case, the --tag flag is only valid for the add subcommand. When using a set, the Parse method needs a string slice to operate on. Usually you will pass a subset of the os.Args removing the parts already parsed, in this case the os.Args 0 and 1.

Helping users

By default each flag set has a built-in helper text, listing all the flags available, their default values and description. As state above, the help information appears by passing -h to the program or you can show programmatically by calling f.PrintDefaults.

To create a custom error message when the parsing fails, you can reassign the Usage function from the flag set:

f := flag.NewFlagSet("add", flag.ExitOnError)
f.Usage = func() {
  fmt.Println("Help me, Obi-Wan Kenobi.")
}