Implementing Sub-commands
Sub-commands are a way to split the functionality of your command-line application into logically independent commands having their own options and arguments. You have a top-level command—your application—and then you have a set of sub-commands, each having its own options and arguments. For example, the Go toolchain is distributed as a single application, go
, which is the top-level command. As a Go developer, you will interact with its various functionalities via dedicated sub-commands such as build
, fmt
, and test
.
You will recall from Chapter 1 that to create a command-line application, you first created a FlagSet
object. For creating an application with sub-commands, you will create one FlagSet
object per sub-command. Then, depending on which sub-command is specified, the corresponding FlagSet
object is used to parse the remaining command-line arguments (see Figure 2.1).
Figure 2.1: The main application looks at the command-line arguments and invokes the appropriate sub-command handler if possible.
Consider the main()
function of an application with two sub-commands, – cmd-a
and cmd-b
:
func main() { var err error if len(os.Args) < 2 { printUsage(os.Stdout) os.Exit(1) } switch os.Args[1] { case "cmd-a": err = handleCmdA(os.Stdout, os.Args[2:]) case "cmd-b": err = handleCmdB(os.Stdout, os.Args[2:]) default: printUsage(os.Stdout) } if err != nil { fmt.Println(err) } os.Exit(1) }
The os.Args
slice contains the command-line arguments that invoke the application. We will handle three input cases:
1 If the second argument is cmd-a, the handleCmdA() function is called.
2 If the second argument is cmd-b, the handleCmdB() function is called.
3 If the application is called without any sub-commands, or neither of those listed in case 1 or case 2 above, the printUsage() function is called to print a help message and exit.
The handleCmdA()
function is implemented as follows:
func handleCmdA(w io.Writer, args []string) error { var v string fs := flag.NewFlagSet("cmd-a", flag.ContinueOnError) fs.SetOutput(w) fs.StringVar(&v, "verb", "argument-value", "Argument 1") err := fs.Parse(args) if err != nil { return err } fmt.Fprintf(w, "Executing command A") return nil }
The above function looks very similar to the parseArgs()
function that you had implemented earlier as part of the greeter
application in Chapter 1. It creates a new FlagSet
object, performs a setup of the various options, and parses the specific slice of arguments. The handleCmdB()
function would perform its own setup for the cmd-b
sub-command.
The printUsage()
function is defined as follows:
func printUsage(w io.Writer) { fmt.Fprintf(w, "Usage: %s [cmd-a|cmd-b] -h\n", os.Args[0]) handleCmdA(w, []string{"-h"}) handleCmdB(w, []string{"-h"}) }
We first print a line of usage message for the application by means of the fmt.Fprintf()
function and then invoke the individual sub-command handler functions with -h
as the sole element in a slice of arguments. This results in those sub-commands displaying their own help messages.
The complete program is shown in Listing 2.1.
Listing 2.1: Implementing sub-commands in a command-line application
// chap2/sub-cmd-example/main.go package main import ( "flag" "fmt" "io" "os" ) // TODO Insert handleCmdaA() implementation as earlier func handleCmdB(w io.Writer, args []string) error { var v string fs := flag.NewFlagSet("cmd-b", flag.ContinueOnError) fs.SetOutput(w) fs.StringVar(&v, "verb", "argument-value", "Argument 1") err := fs.Parse(args) if err != nil { return err } fmt.Fprintf(w, "Executing command B") return nil } // TODO Insert printUsage() implementation as earlier func main() { var err error if len(os.Args) < 2 { printUsage(os.Stdout) os.Exit(1) } switch os.Args[1] { case "cmd-a": err = handleCmdA(os.Stdout, os.Args[2:]) case "cmd-b": err = handleCmdB(os.Stdout, os.Args[2:]) default: printUsage(os.Stdout) } if err != nil { fmt.Fprintln(os.Stdout, err) os.Exit(1) } }
Create a new directory chap2/sub-cmd-example/
, and initialize a module inside it:
$ mkdir -p chap2/sub-cmd-example $ cd chap2/sub-cmd-example $ go mod init github.com/username/sub-cmd-example
Next, save Listing 2.1 as a file main.go
within it. Build and run the application without any arguments:
$ go build -o application $ ./application Usage: ./application [cmd-a|cmd-b] -h Usage of cmd-a: -verb string Argument 1 (default "argument-value") Usage of cmd-b: -verb string Argument 1 (default "argument-value")
Try executing any of the sub-commands:
$ ./application cmd-a Executing command A $ ./application cmd-b Executing command B
You have now seen an example of how you can implement your command-line application with sub-commands by creating multiple FlagSet
objects. Each sub-command is constructed like a stand-alone command-line application. Thus, implementing sub-commands is a great way to separate unrelated functionalities of your application. For example, the go build
sub-command provides all of the build-related functionality and the go test
sub-command provides all of the testing-related functionality for a Go project.
Let's continue this exploration by discussing a strategy to make this scalable.
An Architecture for Sub-command-Driven Applications
As you develop your command-line application, it is a good idea to keep your main package lean and to create a separate package or packages for the sub-command implementations. Your main package will parse the command-line arguments and call the relevant sub-command handler function. If the arguments provided are not recognizable, a help message is displayed containing the usage message for all of the recognized sub-commands (see Figure 2.2).
Next, you lay down the foundation of a generic command-line network client, which you will build upon in later chapters. We will call this program mync
(short for my network client). For now, you will ignore the implementation of the sub-commands and come back to it in later chapters when you fill in the implementation.
Figure 2.2: The main package implements the root command. A sub-command is implemented