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 application which prints the name you entered a specified number of times. Usage of %s: ` fmt.Fprintf(w, usageString, fs.Name()) fmt.Fprintln(w) fs.PrintDefaults() }
Once we set the Usage
attribute of the FlagSet
object to a custom function, it is called whenever there is an error parsing the specified options. Note that the preceding function is defined as an anonymous function so that it can access the specified writer object, w
, to display the custom usage message. Inside the function, we access the name of the FlagSet
using the Name()
method. Then we print a new line and call the PrintDefaults()
method, which prints the various options that have been defined along with their type and default values. The updated parseArgs()
function is as follows:
func parseArgs(w io.Writer, args []string) (config, error) { c := config{} fs := flag.NewFlagSet("greeter", flag.ContinueOnError) fs.SetOutput(w) fs.Usage = func() { var usageString = ` A greeter application which prints the name you entered a specified number of times. Usage of %s: <options> [name]` fmt.Fprintf(w, usageString, fs.Name()) fmt.Fprintln(w) fmt.Fprintln(w, "Options: ") fs.PrintDefaults() } fs.IntVar(&c.numTimes, "n", 0, "Number of times to greet") err := fs.Parse(args) if err != nil { return c, err } if fs.NArg()> 1 { return c, errInvalidPosArgSpecified } if fs.NArg() == 1 { c.name = fs.Arg(0) } return c, nil }
Next, you will implement the final improvement. The greeter program will now allow specifying the name via a positional argument as well. If one is not specified, you will ask for the name interactively.
Accept Name via a Positional Argument
First, update the config
struct to have a name
field of type string
as follows:
type config struct { numTimes int name string }
Then the greetUser()
function will be updated to the following:
func greetUser(c config, w io.Writer) { msg := fmt.Sprintf("Nice to meet you %s\n", c.name) for i := 0; i < c.numTimes; i++ { fmt.Fprintf(w, msg) } }
Next, we update the custom error value as follows:
var errInvalidPosArgSpecified = errors.New("More than one positional argument specified")
We update the parseArgs()
function now to look for a positional argument and, if one is found, set the name
attribute of the config
object appropriately:
if fs.NArg()> 1 { return c, errInvalidPosArgSpecified } if fs.NArg() == 1 { c.name = fs.Arg(0) }
The runCmd()
function is updated so that it only asks the user to input the name interactively if not specified, or if an empty string was specified:
func runCmd(rd io.Reader, w io.Writer, c config) error { var err error if len(c.name) == 0 { c.name, err = getName(rd, w) if err != nil { return err } } greetUser(c, w) return nil }
The complete program with all of the preceding changes is shown in Listing 1.7.
Listing 1.7: Greeter program with user interface updates
// chap1/flag-improvements/main.go package main import ( "bufio" "errors" "flag" "fmt" "io" "os" ) type config struct { numTimes int name string } var errInvalidPosArgSpecified = errors.New("More than one positional argument specified") // TODO Insert definition of getName() as Listing 1.5 // TODO Insert definition of greetUser() as above // TODO Insert updated definition of runCmd() as above // TODO Insert definition of validateArgs as Listing 1.5 // TODO Insert definition of parseArgs() as above func main() { c, err := parseArgs(os.Stderr, os.Args[1:]) if err != nil { if errors.Is(err, errInvalidPosArgSpecified) { fmt.Fprintln(os.Stdout, err) } os.Exit(1) } err = validateArgs(c) if err != nil { fmt.Fprintln(os.Stdout, err) os.Exit(1) } err = runCmd(os.Stdin, os.Stdout, c) if err != nil { fmt.Fprintln(os.Stdout, err) os.Exit(1) } }
Create a new directory, chap1/flag-improvements/
, and initialize a module inside it:
$ mkdir -p chap1/flag-improvements $ cd chap1/flag-improvements $ go mod init github.com/username/flag-improvements
Next, save Listing 1.7 as main.go
. Build it as follows:
$ go build -o application
Run the built application code with -help
, and you will see the custom usage message:
$ ./application -help A greeter application which prints the name you entered a specified number of times. Usage of greeter: <options> [name] Options: -n int Number of times to greet
Now let's specify a name as a positional argument:
$ ./application -n 1 "Jane Doe" Nice to meet you Jane Doe
Next let's specify a bad input—a string as value to the -n
option:
$ ./flag-improvements -n a "Jane Doe" invalid value "a" for flag -n: parse error A greeter application which prints the