Go:小命令接几个参数,`flag` 标准库就能顶一下

35 次浏览6 条回复

写 Go 小工具时,如果只是接一两个开关,不一定马上上 cobra 这类框架。标准库的 flag 对轻量命令还挺够用。

环境:Go 1.20+。保存成 flag-demo.go

package main

import (
    "flag"
    "fmt"
)

func main() {
    name := flag.String("name", "mesh", "要打印的名字")
    count := flag.Int("count", 1, "打印次数")
    dryRun := flag.Bool("dry-run", false, "只展示参数")

    flag.Parse()

    fmt.Printf("name=%s count=%d dryRun=%v args=%v\n", *name, *count, *dryRun, flag.Args())

    if *dryRun {
        return
    }
    for i := 0; i < *count; i++ {
        fmt.Println(*name)
    }
}

跑一下:

go run flag-demo.go -name dev -count 2 extra.txt

flag.Args() 会留下没有被解析成选项的普通参数。等命令开始有子命令、配置文件、复杂帮助信息时,再换更完整的 CLI 框架也不晚。

这个点挺适合小工具。补一句容易踩的:Go 这个 flag 解析遇到第一个普通参数就停了,所以参数最好放在位置参数前面。

比如 go run flag-demo.go extra.txt -count 2 这种,后面的 -count 就不会按 flag 处理了。要做 git 那种子命令风格,一般就给每个子命令单独建个 flag.NewFlagSet

还有个小细节:默认帮助信息会直接打到 stderr,脚本里如果想自己接管输出,可以用 flag.CommandLine.SetOutput(...) 或者单独建 FlagSet 后设输出。

做那种被别的程序调用的小命令时,这个还挺有用,不然 help 和错误信息混在一起有时不好收。

如果想把错误处理得舒服一点,也可以别直接用默认的 flag.CommandLine,自己建一个 FlagSet,然后看 Parse 返回的 error。

这样在测试里传一组参数进去也方便,不用真的从 os.Args 读。小命令一开始这么写,后面要拆子命令时也没那么痛。

再补个小口子:如果参数值不是 string/int/bool 这种内置类型,可以实现 flag.Value 然后用 flag.Var 接进去。

比如逗号分隔列表、日志级别这种,先在一个小类型里把 Set(string) 写清楚,比后面到处手动 split/校验要干净一点。再复杂就别硬撑标准库了。

bool 参数还有个小坑:-dry-run 单独写就已经是 true 了。

如果要显式关掉,通常写 -dry-run=false。像 -dry-run false 这种,false 很容易变成后面的普通参数,脚本里拼命令时尤其容易看漏。

flag 的帮助信息其实也可以轻微定制一下。

比如给 flag.Usage 赋一个函数,里面先打印一行自己的用法说明,再调 flag.PrintDefaults(),这样不用整套 CLI 框架也能把 -h 输出整理得像样一点。