Название: Practical Go
Автор: Amit Saha
Издательство: John Wiley & Sons Limited
Жанр: Программы
isbn: 9781119773832
isbn:
parseArgs()
function for different values specified in the -n
option:
{ args: []string{"-n", "10"}, err: nil, config: config{numTimes: 10}, }, { args: []string{"-n", "abc"}, err: errors.New("invalid value \"abc\" for flag -n: parse error"), config: config{numTimes: 0}, },
The final two test configs test the name specified as a positional argument:
{ args: []string{"-n", "1", "John Doe"}, err: nil, config: config{numTimes: 1, name: "John Doe"}, }, { args: []string{"-n", "1", "John", "Doe"}, err: errors.New("More than one positional argument specified"), config: config{numTimes: 1}, },
When “John Doe” is specified in quotes, it is considered valid. However, when John Doe is specified without quotes, they are interpreted as two positional arguments and hence the function returns an error. The complete test is provided in Listing 1.8.
Listing 1.8: Test for parseArgs()
function
// chap1/flag-improvements/parse_args_test.go package main import ( "bufio" "bytes" "errors" "testing" ) func TestParseArgs(t *testing.T) { // TODO insert the test configs as per above tests := []struct { args []string config output string err error }{..} byteBuf := new(bytes.Buffer) for _, tc := range tests { c, err := parseArgs(byteBuf, tc.args) if tc.err == nil && err != nil { t.Fatalf("Expected nil error, got: %v\n", err) } if tc.err != nil && err.Error() != tc.err.Error() { t.Fatalf("Expected error to be: %v, got: %v\n", tc.err, err) } if c.numTimes != tc.numTimes { t.Errorf("Expected numTimes to be: %v, got: %v\n", tc.numTimes, c.numTimes) } gotMsg := byteBuf.String() if len(tc.output) != 0 && gotMsg != tc.output { t.Errorf("Expected stdout message to be: %#v, Got: %#v\n", tc.output, gotMsg) } byteBuf.Reset() } }
Save Listing 1.8 into a new file, parse_args_test.go
, in the same directory that you used for Listing 1.7. The test for the validateArgs()
function is the same as Listing 1.3, and you can find it in the validate_args_test.go
file in the flag-improvements
subdirectory of the book's code.
The unit test for the runCmd()
function remains the same as that of Listing 1.4, except for a new test configuration where the name is specified by the user via a positional argument. The tests slice is defined as follows:
tests := []struct { c config input string output string err error }{ // Tests the behavior when an empty string is // entered interactively as input. { c: config{numTimes: 5}, input: "", output: strings.Repeat("Your name please? Press the Enter key when done.\n", 1), err: errors.New("You didn't enter your name"), }, // Tests the behavior when a positional argument // is not specified and the input is asked from the user { c: config{numTimes: 5}, input: "Bill Bryson", output: "Your name please? Press the Enter key when done.\n" + strings.Repeat("Nice to meet you Bill Bryson\n", 5), }, // Tests the new behavior where the user has entered their name // as a positional argument { c: config{numTimes: 5, name: "Bill Bryson"}, input: "", output: strings.Repeat("Nice to meet you Bill Bryson\n", 5), }, }
The complete test is shown in Listing 1.9.
Listing 1.9: Test for runCmd()
function
// chap1/flag-improvements/run_cmd_test.go package main import ( "bytes" "errors" "strings" "testing" ) func TestRunCmd(t *testing.T) { // TODO Insert test cases from above tests := []struct{..} byteBuf := new(bytes.Buffer) for _, tc := range tests { r := strings.NewReader(tc.input) err := runCmd(r, byteBuf, tc.c) if err != nil && tc.err == nil { t.Fatalf("Expected nil error, got: %v\n", err) } if tc.err != nil && err.Error() != tc.err.Error() { t.Fatalf("Expected error: %v, Got error: %v\n", tc.err.Error(), err.Error()) } gotMsg := byteBuf.String() if gotMsg != tc.output { t.Errorf("Expected stdout message to be: %v, Got: %v\n", tc.output, gotMsg) } byteBuf.Reset() } }
Save the Listing 1.9 code to a new file, run_cmd_test.go
, in the same directory as Listing 1.8.
Now, run all of the tests:
$ go test -v === RUN TestParseArgs --- PASS: TestParseArgs (0.00s) === RUN TestRunCmd --- PASS: TestRunCmd (0.00s) === RUN TestValidateArgs --- PASS: TestValidateArgs (0.00s) PASS ok github.com/practicalgo/code/chap1/flag-improvements 0.376s
Summary
We started off the chapter implementing a basic command-line interface by directly parsing the command-line arguments. You then saw how you can make use of the flag
package to define a standard command-line interface. Instead of implementing the parsing and validating the arguments ourselves, you learned to use the package's built-in support for user-specified arguments and data type validation. All throughout the chapter, you wrote well-encapsulated functions to make unit testing straightforward.
In the next chapter, you will continue your journey into the flag
package by learning to implement command-line applications with sub-commands, introducing robustness into your applications and more.
CHAPTER 2 Advanced Command-Line Applications
In this chapter, you will learn how to use the flag
package to implement command-line applications with sub-commands. Then, you will see how you can enforce predictable behavior in your command-line applications using contexts. Finally, you will learn how to combine contexts and handling operating system signals in your application. Let's jump in.
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).