Wednesday, March 31, 2010

grep -e works in Go

I've been working on a rewrite of 9base in Go. 9base is a subset of Plan 9 from User Space, which is a *nix port of Plan 9. My project, goblin, can be found on bitbucket.

While working on writing grep, I ran into a limitation of Go's flag package--each flag can only have one value. Basically, grep written in Go using the built-in flag package would be unable to be called like this (search expression can be passed in to grep with -e):

grep -e foo -e bar -e baz file.txt


I'm not certain what would happen if it was called like that, although my guess is that the baz value would be used. The only thing I'm willing to guarantee here is that only one of the three values passed in via -e would be used. You would be expecting to have grep use an expression like (foo|bar|baz), but you'd be getting the results of baz. One option I investigated was using a different library for flag parsing, optparse. I am planning on writing a brief description/tutorial of this package later.

After a little bit of conversation in the golang-nuts mailing list, Rob Pike decided to change the flag package (code review). His change was very simple; He opened the API up a little bit to allow custom types. Support for multiple types isn't built in to the flag package, but I believe that this is a good thing. The flag package is kept very simple, but it can now be extended if needed.

In the code review, Rob gave an example of how to use the modified flag library to support multiple flags. It's simple really, all you have to do is create a new type that satisfies the flag.Value interface, i.e. it needs to have a Set and String function.

I took the example, added to it, and made it work the same as the flag package--without any global variables. Here's an example of how to use my version (multiflag) (note the lack of a default value):



package main

import (
"flag"
"fmt"
"./multiflag"
)

func main() {
argList := multiflag.StringArray("e", "Can be used multiple times")
flag.Parse()
fmt.Print(*argList)
}



Assuming that this file was compiled into a binary named grep, if it was called like the grep example above (grep -e foo -e bar -e baz), this program would print out [foo bar baz].

And here is the code that makes it all possible (requires Go to be updated to at least 6070517caba0):



package multiflag

import (
"flag"
"fmt"
)

type stringArrayValue struct {
p *[]string
}

func StringArray(name string, usage string) *[]string {
p := new([]string)
StringArrayVar(p, name, usage)
return p
}

func StringArrayVar(p *[]string, name string, usage string) {
flag.Var(&stringArrayValue{p}, name, usage)
}

func (f *stringArrayValue) String() string {
return fmt.Sprint(*f.p)
}

func (f *stringArrayValue) Set(s string) bool {
if *f.p == nil {
*f.p = make([]string, 1)
} else {
nv := make([]string, len(*f.p)+1)
copy(nv, *f.p)
*f.p = nv
}
(*f.p)[len(*f.p)-1] = s
return true
}


No comments: