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.

\$\endgroup\$
2
  • \$\begingroup\$ Uhm... Why exactly do you not want to use flag from the standard library? \$\endgroup\$
    – Ainar-G
    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\$ 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

\$\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

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.