Practical Go. Amit Saha
Чтение книги онлайн.
Читать онлайн книгу Practical Go - Amit Saha страница 19
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 in its own package.
Let's look at the implementation of the main
package first. Here, we will only have a single file, main.go
, to start (see Listing 2.2).
Listing 2.2: Implementation of the main
package
// chap2/sub-cmd-arch/main.go package main import ( "errors" "fmt" "github.com/username/chap2/sub-cmd-arch/cmd" "io" "os" ) var errInvalidSubCommand = errors.New("Invalid sub-command specified") func printUsage(w io.Writer) { fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n") cmd.HandleHttp(w, []string{"-h"}) cmd.HandleGrpc(w, []string{"-h"}) } func handleCommand(w io.Writer, args []string) error { var err error if len(args) < 1 { err = errInvalidSubCommand } else { switch args[0] { case "http": err = cmd.HandleHttp(w, args[1:]) case "grpc": err = cmd.HandleGrpc(w, args[1:]) case "-h": printUsage(w) case "-help": printUsage(w) default: err = errInvalidSubCommand } } if errors.Is(err, cmd.ErrNoServerSpecified) || errors.Is(err, errInvalidSubCommand) { fmt.Fprintln(w, err) printUsage(w) } return err } func main() { err := handleCommand(os.Stdout, os.Args[1:]) if err != nil { os.Exit(1) } }