golang flag stops parsing after the first non-option
Asked Answered
H

2

8

i am building a little cli tool that boots my app in development or production.

the way i want it to work is like this:

app run --dev or app run --prod

Atm it doest parses the flags after my command but only before my command. So this works

app --dev run or app --prod run

Any idee how to fix it this so i can use it after my command? here is my code

func main() {
    //flag.Usage := usage
    flag.Parse()
    args := flag.Args()
    if len(args) == 0 {
        Usage()
        os.Exit(0)
    }

    if *dev {
        os.Setenv("ENV", "development")
    }

    if *prod {
        os.Setenv("ENV", "production")
    }

    switch {
    // Run
    case args[0] == "run" && len(args) == 1:
        os.Setenv("port", *port)
        log.Printf("Booting in %s", os.Getenv("ENV"))
        Run()

    // Help
    case args[0] == "help" && len(args) == 1:
        Usage()
    }
}
Hair answered 4/8, 2014 at 7:3 Comment(1)
Why not just call ENV=dev yourapp run - which avoids the weird SetEnv dance you have going and just sets it directly (and only for that run).Baldric
A
14

Traditionally, the UNIX option parser getopt() stops parsing after the first non-option. The glibc altered this behavior to support options in arbitrary positions, a controversial decision. The flag package implements the traditional behavior.

One thing you could do is permuting the arguments array before parsing the flags. That's what the glibc does:

func permutateArgs(args []string) int {
    args = args[1:]
    optind := 0

    for i := range args {
        if args[i][0] == '-' {
            tmp := args[i]
            args[i] = args[optind]
            args[optind] = tmp
            optind++
        }
    }

    return optind + 1
}

This code permutates args such that options are in front, leaving the program name untouched. permutateArgs returns the index of the first non-option after permutation. Use this code like this:

optind := permutateArgs(os.Args)
flags.Parse()

// process non-options
for i := range os.Args[optind:] {
    // ...
}
Accusal answered 4/8, 2014 at 7:15 Comment(5)
@AnthonyDeMeulemeester Please try again. The code should work now.Accusal
this is an awesome answer -- although it does not preserve the ordering of non flag arguments. for instance, given the args a -x b c -y, you get -x -y b c a instead of -x -y a b c. A solution that preserves ordering would be great!Cowberry
Also works only for the Bool flags. For flags that take values, like -o result.txt, this method will treat 'result.txt' as an argument and not as an option flag.Carolinecarolingian
@Carolinecarolingian Yes, indeed. It shouldn't be too difficult to adapt the code to also consider other kinds of options.Accusal
Thank you. You can also more easily process the non-options with flag.Args()British
G
1

This is simply the way the flag package deals with arguments. Argument handling is always somewhat convention driven, and the flag package definitely does not follow the gnu-style that many are accustomed to.

One way is to sort the options before the commands:

// example argument sorting using sort.Stable
type ArgSlice []string

func (p ArgSlice) Len() int      { return len(p) }
func (p ArgSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

func (p ArgSlice) Less(i, j int) bool {
    if len(p[i]) == 0 {
        return false
    }
    if len(p[j]) == 0 {
        return true
    }
    return p[i][0] == '-' && p[j][0] != '-'
}

func main() {

    args := []string{"cmd", "-a", "arg", "-b", "-c"}
    sort.Stable(ArgSlice(args))
    // if sorting os.Args, make sure to omit the first argument
    // sort.Stable(ArgSlice(os.Args[1:]))

    fmt.Println(args)
}

Many packages use separate FlagSets for subcommands, which provides nice separation, but this would not work when there are possibly different flags between subcommands. The only solution there is to duplicate the flags across each level.

However, the best answer is still to follow the conventions used by the flag package, and not try to fight it.

Gladdie answered 4/8, 2014 at 15:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.