Golang标志在遇到第一个非选项时停止解析

8
我正在开发一个小型的CLI工具,用于在开发或生产环境中启动我的应用程序。
我希望它能够按照以下方式工作:
```` ````
app run --dev or app run --prod

目前它仅在我的命令之前解析标记,而不是在我的命令之后解析标记。 因此,这个可以工作。

app --dev run or app --prod run

有什么办法可以修复这个问题,以便我可以在我的命令之后使用它?这是我的代码:

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()
    }
}

为什么不直接调用 ENV=dev yourapp run 呢?这样可以避免你所做的奇怪的 SetEnv 操作,直接设置(并且仅针对该运行)即可。 - elithrar
2个回答

11

传统上,UNIX选项解析器getopt()在遇到第一个非选项后停止解析。glibc改变了这种行为以支持任意位置的选项,这是一个有争议的决定。而flag包则实现了传统的行为。

你可以在解析标志之前对参数数组进行排列。这就是glibc所做的。

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
}

这段代码对 args 进行排列,使得选项位于前面,程序名称不受影响。 permutateArgs 返回排列后第一个非选项的索引。使用方法如下:

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

// process non-options
for i := range os.Args[optind:] {
    // ...
}

@AnthonyDeMeulemeester 请再试一次。代码现在应该可以工作了。 - fuz
这是一个很棒的答案——尽管它不保留非标志参数的顺序。例如,给定参数a -x b c -y,你得到的是-x -y b c a,而不是-x -y a b c。一个保留顺序的解决方案会很棒! - briceburg
仅适用于布尔标志。对于需要取值的标志,比如 -o result.txt,该方法将把 'result.txt' 视为参数而非选项标志。 - CodeExpress
@CodeExpress 是的,确实如此。将代码适应考虑其他类型的选项也不应该太困难。 - fuz
谢谢。您还可以更轻松地处理非选项,使用 flag.Args() - tbrodbeck

1
这只是flag包处理参数的方式。参数处理总是有一定的约定,而flag包明显不遵循许多人习惯的gnu风格。
一种方法是在命令之前对选项进行排序:
// 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)
}

许多软件包在子命令中使用单独的FlagSets,这提供了很好的分离性,但当子命令之间可能存在不同的标志时,这种方法就行不通了。唯一的解决方案是在每个级别上复制标志。
然而,最好的答案仍然是遵循flag软件包使用的惯例,而不是试图与其抗争。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接