3
\$\begingroup\$

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.

asked Oct 4, 2017 at 12:47
\$\endgroup\$
2
  • \$\begingroup\$ Uhm... Why exactly do you not want to use flag from the standard library? \$\endgroup\$ Commented Oct 4, 2017 at 23:42
  • \$\begingroup\$ I was looking for something that would just take the string arguments and return a struct with typed fields. Will look at it again. \$\endgroup\$ Commented Oct 5, 2017 at 9:04

2 Answers 2

5
\$\begingroup\$

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

answered Oct 5, 2017 at 8:11
\$\endgroup\$
0
\$\begingroup\$

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
}

https://play.golang.org/p/bqNgCt63QL

answered Oct 10, 2017 at 11:32
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.