Practical Go. Amit Saha
Чтение книги онлайн.
Читать онлайн книгу Practical Go - Amit Saha страница 16
3 The arguments to parse were being passed as a parameter, args .
The function is well encapsulated and avoids using any global state. A test for the function is shown in Listing 1.6.
Listing 1.6: Test for the parseArgs()
function
//chap1/flag-parse/parse_args_test.go package main import ( "bytes" "errors" "testing" ) func TestParseArgs(t *testing.T) { tests := []struct { args []string err error numTimes int }{ { args: []string{"-h"}, err: errors.New("flag: help requested"), numTimes: 0, }, { args: []string{"-n", "10"}, err: nil, numTimes: 10, }, { args: []string{"-n", "abc"}, err: errors.New("invalid value \"abc\" for flag -n: parse error"), numTimes: 0, }, { args: []string{"-n", "1", "foo"}, err: errors.New("Positional arguments specified"), numTimes: 1, }, } byteBuf := new(bytes.Buffer) for _, tc := range tests { c, err := parseArgs(byteBuf, tc.args) if tc.result.err == nil && err != nil { t.Errorf("Expected nil error, got: %v\n", err) } if tc.result.err != nil && err.Error() != tc.result.err.Error() { t.Errorf("Expected error to be: %v, got: %v\n", tc.result.err, err) } if c.numTimes != tc.result.numTimes { t.Errorf("Expected numTimes to be: %v, got: %v\n", tc.result.numTimes, c.numTimes) } byteBuf.Reset() } }
Save Listing 1.6 into the directory in which you saved Listing 1.5. Name the file parse_args_test.go
.
The unit test for the runCmd()
function remains the same as that seen in Listing 1.4, except for the absence of the first test, which was used to test the behavior of runCmd()
when printUsage
was set to true. The test cases we want to test are as follows:
tests := []struct { c config input string output string err error }{ { 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"), }, { 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), }, }
You can find the complete test in the run_cmd_test.go
file in the flag-parse
subdirectory of the book's code.
The test for the validateArgs()
function is the same as the one used in Listing 1.3. You can find it in the validate_args_test.go
file in the flag-parse
subdirectory of the book's code. Now, run all of the tests:
$ go test -v === RUN TestSetupFlagSet --- PASS: TestSetupFlagSet (0.00s) === RUN TestRunCmd --- PASS: TestRunCmd (0.00s) === RUN TestValidateArgs --- PASS: TestValidateArgs (0.00s) PASS ok github.com/practicalgo/code/chap1/flag-parse 0.610s
Great. Now you have rewritten the parsing logic of the greeter application to use the flag
package and then updated the unit tests so that they test the new behavior. Next, you are going to work on improving the user interface of the application in a few ways. Before doing that, however, let's complete Exercise 1.2.
EXERCISE 1.2: HTML GREETER PAGE CREATOR In this exercise, you will update the greeter program to create an HTML page, which will serve as the home page for the user. Add a new option,
-o
, to the application, which will accept the filesystem path as a value. If the
-o
is specified, the greeter program will create an HTML page at the path specified with the following contents:
<h1>Hello Jane Clancy</h1>
, where Jane Clancy is the name entered. You may choose to use the
html/template
package for this exercise.
Improving the User Interface
In the following sections, you are going to improve the user interface of the greeter application in three ways:
Remove the duplicate error messages
Customize the help usage message
Allow the user to enter their name via a positional argument
While implementing these improvements, you will learn how to create custom error values, customize a FlagSet
object to print a customized usage message, and access positional arguments from your application.
Removing Duplicate Error Messages
You may have noticed that errors were being displayed twice. This is caused by the following code snippet in the main()
function:
c, err := parseArgs(os.Stderr, os.Args[1:]) if err != nil { . fmt.Println(err) os.Exit(1) }
When the Parse()
function call encountered an error, it was displaying that error to the output writer instance set in the fs.SetOutput()
call. Subsequently, the returned error was also being printed in the main()
function via the snippet above. It may seem like an easy fix not to print the error in the main()
function. However, that will mean that any custom errors returned, such as when positional arguments are specified, will also not be shown. Hence, what we will do is create a custom error value and return that instead. We will only print the error if it matches that custom error, else we will skip printing it.
A custom error value can be created as follows:
var errPosArgSpecified = errors.New("Positional arguments specified")
Then, in the parseArgs()
function, we return the following error:
if fs.NArg() != 0 { . return c, errPosArgSpecified }
Then in main()
, we update the code as follows:
c, err := parseArgs(os.Stderr, os.Args[1:]) if err != nil { if errors.Is(err, errPosArgSpecified) { fmt.Fprintln(os.Stdout, err) } os.Exit(1) }
The errors.Is()
function is used to check whether the error value err
matches the error value errPosArgSpecified
. The error is displayed only if a match is found.
Customizing Usage Message
If you compare Listing 1.5 to Listing 1.1, you will notice that there is no custom usageString
specified. This is because the flag
package automatically constructs one based on the FlagSet
name and the options defined. However, what if you wanted to customize it? You can do so by setting the Usage
attribute of the FlagSet
object to a function as follows:
fs.Usage = func() { var usageString = ` A greeter