I wanted to be able to specify a particular command-line flag more than once in a Go program. I was about to throw my hands up in despair because I didn’t think that the Go flag package could process multiple instances of a command-line flag. I was wrong.
While I was tempted to write my own command-line options parser, I chose to find the idiomatic way to approach the problem. If I have learned nothing else from my GoMentors, I have learned to try to follow the idioms and to try not to reinvent the wheel.
I found an example by visiting http://golang.org/pkg/flag/ . I had to search for the string “FlagSet” in my browser. Immediately under the paragraph where the word “FlagSet” first appears, is a clickable item labeled “Example”. Click the example item and take a look at the code.
I copied the code and toyed with it until I thought I understood it. Then, I tried to simplify it and rewrite it. My example program will simply accept one or more command-line flags with the label -i. Each argument to -i should be an integer. I want to be able to specify -i multiple times on the command-line. The program should populate a slice of integers while adhering to the above command-line syntax.
Here’s my code … flagstuff.go :
// Copyright 2013 - by Jim Lawless // License: MIT / X11 // See: http://www.mailsend-online.com/license2013.php // // Bear with me ... I'm a Go noob. package main import ( "flag" "fmt" "strconv" ) // Define a type named "intslice" as a slice of ints type intslice []int // Now, for our new type, implement the two methods of // the flag.Value interface... // The first method is String() string func (i *intslice) String() string { return fmt.Sprintf("%d", *i) } // The second method is Set(value string) error func (i *intslice) Set(value string) error { fmt.Printf("%s\n", value) tmp, err := strconv.Atoi(value) if err != nil { *i = append(*i, -1) } else { *i = append(*i, tmp) } return nil } var myints intslice func main() { flag.Var(&myints, "i", "List of integers") flag.Parse() if flag.NFlag() == 0 { flag.PrintDefaults() } else { fmt.Println("Here are the values in 'myints'") for i := 0; i < len(myints); i++ { fmt.Printf("%d\n", myints[i]) } } }
Let’s dissect the code one section at a time … not necessarily in the order presented in the source code above.
First, I define a type called intslice that refers to a slice of ints:
type intslice []int
Later, I define a variable named myints of type intslice.
var myints intslice
Later in the code, I’m going to be calling flag.Var() passing in &myints as the first argument. The type of the first value to flag.Var() must conform to the flag.Value interface which is defined as:
type Value interface { String() string Set(string) error }
I must now define a String() method and a Set() method for my intslice type:
func (i *intslice) String() string { return fmt.Sprintf("%d", *i) } func (i *intslice) Set(value string) error { fmt.Printf("%s\n", value) tmp, err := strconv.Atoi(value) if err != nil { *i = append(*i, -1) } else { *i = append(*i, tmp) } return nil }
The above methods will be called by the parsing engine in the flag package when I invoke flag.Parse(). In the String() method, I need to return a string-representation of the argument. In the Set() method, I then need to append the string value to the specified intslice variable by first converting value to an int variable named tmp. If an error occurs during conversion, I append an int value of -1 to the intslice variable.
The main body looks like this:
func main() { flag.Var(&myints, "i", "List of integers") flag.Parse() if flag.NFlag() == 0 { flag.PrintDefaults() } else { fmt.Println("Here are the values in 'myints'") for i := 0; i < len(myints); i++ { fmt.Printf("%d\n", myints[i]) } } }
Here are a few sample command-line invocations and the output that they produce ( I’ve added a blank line between each command and the counterpart response lines for clarity):
No parameters. I’ve added a check to make sure that more than zero flags are specified.
flagstuff -i=[]: List of integers
Let’s specify an invalid flag (-x):
flagstuff -x flag provided but not defined: -x Usage of flagstuff: -i=[]: List of integers
Let’s specify -i without an argument:
flagstuff -i Usage of flagstuff: -i=[]: List of integers
Now, let’s specify a single -i parameter with an integer value:
flagstuff -i 5 5 Here are the values in 'myints' 5
At each invocation of the intslice.Set() method, I display the string that has been passed in so that I could observe the mechanics of the parsing process. In each example that provide arguments for -i, we’ll first see those values, then we’ll see what the slice contains via the for loop that occurs just a little later in the code.
Let’s specify a string instead of an int as an argument:
flagstuff -i twelve twelve Here are the values in 'myints' -1
Note that this causes the error condition in the call to strconv.Atoi(). I have chosen to add the value -1 to the slice when the argument doesn’t cleanly parse as an integer. You may choose to handle the error differently.
Here is an example with three valid integers:
flagstuff -i 5 -i 6 -i 7 5 6 7 Here are the values in 'myints' 5 6 7
Note that the example at golang.org contains a section that splits the string value passed to Set() based on the presence of the comma character. This allows that code to also accept multiple arguments to a single command-line flag. I have chosen to avoid doing that to simplify my example.
Knowing how to handle multiple occurrences of a given flag without customizing the command-line parser is going to be very helpful for a couple of programs that I plan to write. I’m glad that I spent the time going over the example golang.org code. I hope to tinker with more exotic command-line processing features of the flag package in the near future.
Why not just use flag.String() and accept a comma-delimited string of ints?
Hi, David.
To be honest, I would have loved to parse the entire command-line without using the flag library. I have a process that I’ve used in other languages, but alas … it would not be idiomatic Go. I was hesitant to manually parse an item that had already been parsed as a string. I chose integers arbitrarily for this example. It’s more likely that my command-line code will have string-based parms that can occur multiple times. I thought that the integers were more illustrative of what needed to be done so that one could snap a new “type” into the flag engines.
Long answer to a short question. The short answer is … I wanted to use my preferred parameter format and remain harmonic with Go’s idioms. I hope I accomplished that.