动态解析命令行参数
本文属于介绍 NWPC 消息平台 系列文章。
之前的文章《适用于NMC监控平台的数值预报产品消息》中提到数值预报中心的数值预报业务系统使用命令行客户端向消息平台发送消息。 虽然目前只发送确定性预报模式的GRIB2产品消息,但需要考虑后续如何进行扩展。
不同消息需要传递的参数不完全一样,可以为每种消息单独开发命令行接口。例如nwpc-oper/nwpc-message-client为产品消息和ecflow_client命令单独开发两个子命令。
不过,产品消息也可能有多个不同的类型,例如上面文章中提到集合预报模式的产品比确定性模式多一个表示集合成员的字段。 为每个的消息类型都各自开发一个命令行接口会显著增加系统的复杂性。而将所有可能的参数都添加到同一个命令中会导致命令行接口过于复杂。
一个可行的办法是开发一个命令行接口,针对不同的消息类型,使用不同的命令行参数。
目前业务系统使用的消息发送命令行程序都是用GOLANG实现,使用Cobra库开发命令行接口。 本文介绍如何在Cobra基础上通过设置消息类型等参数,实现动态解析命令行参数。
命令行参数解析
在之前的文章《使用Cobra构建命令行程序》中介绍如何使用Cobra创建命令行程序。
下面代码截取自nwpc-oper/nwpc-message-client项目,使用Cobra默认的方式构建命令行参数。
brokerCmd := &cobra.Command{
Use: "broker",
Short: "A broker for NWPC Message Client",
Long: brokerDescription,
RunE: bc.runCommand,
}
brokerCmd.Flags().StringVar(&bc.brokerAddress, "address", ":33383",
"broker rpc address, use tcp port.")
brokerCmd.Flags().BoolVar(&bc.disableDeliver, "disable-deliver", false,
"disable deliver messages to message queue, just for debug.")
上述方法必须在进行参数解析前就设置好参数选项,无法动态设置参数。
不过cobra.Command
提供DisableFlagParsing
选项可以关闭内置的参数解析,用户可以手动编写命令行解析程序。
下面介绍如何使用pflag库实现动态参数解析。
动态解析参数
创建pflag.NewFlagSet
,配置一组命令行参数信息,用于解析通用的参数。
因为没有包含诸如--start-time
等与消息类型相关的参数,直接调用Parse()
会因为无法识别某些参数而出错。
需要设置ParseErrorsWhitelist
,忽略无法识别的参数。
var sendFlagSet = pflag.NewFlagSet("send", pflag.ContinueOnError)
sendFlagSet.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
sendFlagSet.SortFlags = false
sendFlagSet.StringVar(&source, "source", "", "message source")
sendFlagSet.StringVar(&messageType, "type", "", "message type")
// ...skip...
sendFlagSet.BoolVar(&help, "help", false, "show help information.")
if err := sendFlagSet.Parse(args); err != nil {
cmd.Usage()
log.Fatal(err)
}
对于GRIB2消息,可以创建另一个pflag.NewFlagSet
,单独解析。
var prodGribFlagSet = pflag.NewFlagSet("prod_grid", pflag.ContinueOnError)
prodGribFlagSet.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
prodGribFlagSet.SortFlags = false
prodGribFlagSet.StringVar(&startTime, "start-time", "", "start time, such as 2019062400")
prodGribFlagSet.StringVar(&forecastTime, "forecast-time", "", "forecast time, such as 000")
if err := prodGribFlagSet.Parse(args); err != nil {
log.Fatalf("argument parse fail: %s", err)
}
命令行程序根据sendFlagSet
解析的source
和type
变量,判断需要发送哪种类型的消息,创建对应的参数解析器,解析与消息相关的参数。
例如source=nwpc_grapes_gfs, type=prod_grib
说明发送GRIB2消息,程序会创建prodGribFlagSet
,进行下一步解析。
后续增加其它种类消息时,可以创建新的pflag.NewFlagSet
,并增加判断条件。
进一步
上面只介绍了最基本的参数解析,想要实现完整的命令行接口,还需要考虑下面两个问题。
必备参数
pflag.FlagSet
没有内置的required参数检查,可以在解析参数后检查变量的值,判断是否设置。
例如nmc-message-client项目手动检查变量值,如果是默认值,就说明用户没有在命令行中设置该参数,程序直接出错。
if source == "" {
log.Fatal("source option is required")
}
if messageType == "" {
log.Fatal("messageType option is required")
}
如果必备参数太多,就不适合手动检测。下面参考Cobra的方式实现必备参数检测。
使用 FlagSet 的SetAnnotation
方法为参数设置标记。
operFlagSet.SetAnnotation("start-time", commands.RequiredOption, []string{"true"})
使用 FlagSet 的 VisitAll
方法遍历所有选项,通过flag.Changed
属性判断变量是否被设置。
如果任意一个添加了RequiredOption
的选项未被设置,则返回错误。
func CheckRequiredFlags(commandFlags *pflag.FlagSet) error {
var missingFlagNames []string
commandFlags.VisitAll(func(flag *pflag.Flag) {
requiredAnnotation, found := flag.Annotations[RequiredOption]
if !found {
return
}
if (requiredAnnotation[0] == "true") && !flag.Changed {
missingFlagNames = append(missingFlagNames, flag.Name)
}
})
if len(missingFlagNames) > 0 {
return fmt.Errorf(`required flag(s) "%s" not set`, strings.Join(missingFlagNames, `", "`))
}
return nil
}
帮助消息
如果使用自定义的参数解析代码,Cobra就不会自动生成参数的帮助消息,需要手动设置。
使用SetUsageFunc
和SetHelpFunc
函数设置自定义帮助。
ecFlowClientCmd.SetUsageFunc(func(*cobra.Command) error {
ec.printHelp()
return nil
})
ecFlowClientCmd.SetHelpFunc(func(*cobra.Command, []string) {
ec.printHelp()
})
在printHelp()
函数中,使用FlagSet
的PrintDefaults
函数打印参数信息。
下面代码打印mainFlags和targetFlags两个参数解析器的参数信息。
fmt.Fprintf(helpOutput, "Main Flags:\n")
mainFlags.PrintDefaults()
fmt.Fprintf(helpOutput, "Target Flags:\n")
targetFlags.PrintDefaults()
参考
NMC消息平台客户端,包括消息发送和接收。
NWPC消息平台客户端,包括消息发送和接收。