I've written a reimplementation of the redis-benchmark in Go and part of that is parsing options from the command line. I evaluated several libraries but nothing I found seem to do what I had in mind.
Here is the code:
// ParseArguments parses a string array and returns a populated Options struct
func ParseArguments(arguments []string) Options {
options := defaultOptions
args := arguments[1:]
var errOptions Options
for i := 0; i < len(args); i++ {
if args[i] == "--help" || args[i] == "-h" {
return Options{ShowHelp: true, HelpText: helpText}
} else if args[i] == "--host" || args[i] == "-H" {
i++
if i >= len(args) {
return buildHelp("Error: Incorrect parameters specified")
}
options.Host = args[i]
} else if args[i] == "--requests" || args[i] == "-n" {
options.Requests, errOptions = parseNumber(args, &i)
if errOptions.ShowHelp {
return errOptions
}
} else if args[i] == "--clients" || args[i] == "-c" {
options.Connections, errOptions = parseNumber(args, &i)
if errOptions.ShowHelp {
return errOptions
}
} else if args[i] == "--tests" || args[i] == "-t" {
i++
if i >= len(args) {
return buildHelp("Error: Incorrect parameters specified")
}
options.Tests = strings.Split(args[i], ",")
for i := range options.Tests {
options.Tests[i] = strings.ToUpper(options.Tests[i])
}
} else if args[i] == "--port" || args[i] == "-p" {
options.Port, errOptions = parseNumber(args, &i)
if errOptions.ShowHelp {
return errOptions
}
} else {
return buildHelp(fmt.Sprintf("Error: Invalid parameter: %v", args[i]))
}
}
return options
}
(For more context, the rest of the file is on github).
When gocyclo is run over the code the function gets a complexity of 20. I'm interested in suggestions that could reduce this to below the threshold of 10.
2 Answers 2
You should definitely rely on a library to achieve this. The flag
package from standard library is nice, but go-flags is even better
With this library, the equivalent code is:
package main
import (
"fmt"
"os"
"github.com/jessevdk/go-flags"
)
type options struct {
Help bool `short:"h" long:"help" description:"show help message"`
Requests int `short:"n" long:"requests" default:"1"`
Clients int `short:"c" long:"clients" default:"1"`
Tests []string `short:"t" long:"tests" env-delim:","`
Port int `short:"p" long:"port" default:"9000"`
}
func main() {
var opts options
p := flags.NewParser(&opts, flags.Default&^flags.HelpFlag)
_, err := p.Parse()
if err != nil {
fmt.Printf("fail to parse args: %v", err)
os.Exit(1)
}
if opts.Help {
p.WriteHelp(os.Stdout)
os.Exit(0)
}
fmt.Printf("tests: %v\n", opts)
}
All errors (type error, required flag missing ect...) are handle directly by the library
Now the complexity of the function is 3
You can do all this just fine with the standard library. The only tricky bit is the CSV format for --tests (consider using []string directly, and let the caller specify multiple -t/--tests arguments). I had to guess the Options type, but it should be obvious how to change it for, say, uints.
package main
import (
"flag"
"fmt"
"strings"
)
type Options struct {
Requests int
Connections int
Tests []string
Port int
}
func main() {
options := ParseArguments([]string{"-c", "10", "--requests", "20", "-t", "foo,bar"})
fmt.Printf("%+v\n", options)
// {Requests:20 Connections:10 Tests:[FOO BAR] Port:0}
}
func ParseArguments(arguments []string) Options {
var defaultOptions Options
options := defaultOptions
fs := flag.NewFlagSet("main", flag.ExitOnError)
fs.IntVar(&options.Requests, "requests", defaultOptions.Requests, "requests description")
fs.IntVar(&options.Requests, "n", defaultOptions.Requests, "requests description")
fs.IntVar(&options.Connections, "clients", defaultOptions.Connections, "clients description")
fs.IntVar(&options.Connections, "c", defaultOptions.Connections, "clients description")
fs.IntVar(&options.Port, "port", defaultOptions.Port, "port description")
fs.IntVar(&options.Port, "p", defaultOptions.Port, "port description")
tests := csv{}
fs.Var(&tests, "tests", "tests description")
fs.Var(&tests, "t", "tests description")
fs.Parse(arguments)
options.Tests = tests
return options
}
type csv []string
func (vs *csv) String() string {
return strings.Join(*vs, ",")
}
func (vs *csv) Set(arg string) error {
values := strings.Split(arg, ",")
for i := range values {
values[i] = strings.ToUpper(values[i])
}
*vs = values // or append(), if repeated args are okay
return nil
}
flag
from the standard library? \$\endgroup\$struct
with typed fields. Will look at it again. \$\endgroup\$