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) } }
At the top, we are importing the cmd
package, which is a sub-package containing the implementation of the sub-commands. Since we will initialize a module for the application, we specify the absolute import path for the cmd
package. The main()
function calls the handleCommand()
function with all of the arguments specified starting from the second argument:
err := handleCommand(os.Args[1:])
If the handleCommand()
function finds that it has received an empty slice, implying that no command-line arguments were specified, it returns a custom error value:
if len(args) < 1 { err = errInvalidSubCommand }
If command-line arguments were specified, a switch..case
construct is defined to call the appropriate command handler function based on the first element of the slice, args
:
1 If this element is http or grpc, the appropriate handler function is called.
2 If the first element is -h or -help, it calls the printUsage() function.
3 If it matches none of the conditions above, the printUsage() function is called and a custom error value is returned.
The printUsage()
function prints a message first using fmt.Fprintf(w, "Usage: mync [http|grpc] -h\n")
and then calls the sub-command implementations with the argument slice containing onl y "-h"
.
Create a new directory, chap2/sub-cmd-arch
, and initialize a module inside it:
$ mkdir -p chap2/sub-cmd-arch $ cd chap2/sub-cmd-arch $ go mod init github.com/username/chap2/sub-cmd-arch/
Save Listing 2.2 as main.go
in the above directory.
Next let's look at HandleHttp()
function, which handles the http
sub-command (see Listing 2.3).
Listing 2.3: Implementation of the HandleHttp()
function
// chap2/sub-cmd-arch/cmd/httpCmd.go package cmd import ( "flag" "fmt" "io" ) type httpConfig struct { url string verb string } func HandleHttp(w io.Writer, args []string) error { var v string fs := flag.NewFlagSet("http", flag.ContinueOnError) fs.SetOutput(w) fs.StringVar(&v, "verb", "GET", "HTTP method") fs.Usage = func() { var usageString = ` http: A HTTP client. http: <options> server` fmt.Fprintf(w, usageString) fmt.Fprintln(w) fmt.Fprintln(w) fmt.Fprintln(w, "Options: ") fs.PrintDefaults() } err := fs.Parse(args) if err != nil { return err } if fs.NArg() != 1 { return ErrNoServerSpecified } c := httpConfig{verb: v} c.url = fs.Arg(0) fmt.Fprintln(w, "Executing http command") return nil }
The HandleHttp()
function creates a FlagSet
object and configures it with an option, a custom usage, and other error handling.
Create a new subdirectory, cmd
, inside the directory that you created earlier, and save Listing 2.3 as httpCmd.go
.
The HandleGrpc()
function is implemented in a similar fashion (see Listing 2.4).
Listing 2.4: Implementation of the HandleGrpc()
function
// chap2/sub-cmd-arch/cmd/grpcCmd.go package cmd import ( "flag" "fmt" "io" ) type grpcConfig struct { server string method string body string } func HandleGrpc(w io.Writer, args []string) error { c := grpcConfig{} fs := flag.NewFlagSet("grpc", flag.ContinueOnError) fs.SetOutput(w) fs.StringVar(&c.method, "method", "", "Method to call") fs.StringVar(&c.body, "body", "", "Body of request") fs.Usage = func() { var usageString = ` grpc: A gRPC client. grpc: <options> server` fmt.Fprintf(w, usageString) fmt.Fprintln(w) fmt.Fprintln(w) fmt.Fprintln(w, "Options: ") fs.PrintDefaults() } err := fs.Parse(args) if err != nil { return err } if fs.NArg() != 1 { return ErrNoServerSpecified } c.server = fs.Arg(0) fmt.Fprintln(w, "Executing grpc command") return nil }
Save Listing 2.4 as grpcCmd.go
in the cmd
subdirectory.
The custom error value, ErrNoServerSpecified
, is created in a separate file in the cmd
package as shown in Listing 2.5.
Listing 2.5: Custom error values
// chap2/sub-cmd-arch/cmd/errors.gopackage cmd import "errors" var ErrNoServerSpecified = errors.New("You have to specify the remote server.")
In the cmd
subdirectory, save Listing 2.5 as errors.go
. You will end up with a source tree structure that looks like the following:
. |____cmd | |____grpcCmd.go | |____httpCmd.go | |____errors.go |____go.mod |____main.go
From the root directory of the module, build the application:
$ go build -o application
Try running the build
application with different arguments, starting with -help
or -h
:
$ ./application --help Usage: mync [http|grpc] -h http: A HTTP client. http: <options> server Options: -verb string HTTP method (default "GET") grpc: A gRPC client. grpc: <options> server Options: -body string Body of request -method string Method to call
Before we move on, let's make sure that we have unit tests for the functionality implemented by the main
and cmd
packages.
Testing the Main Package
First, let's write the unit test for the main
package. The handleCommand()
is the key function that also calls the other functions in the package. It is declared as follows:
err := handleCommand(w io.Writer, args []string)
In the test, we will call the