从 golang flag 迁移到 cmdr

基于 cmdr v1.0.3

从 golang flag 迁移到 cmdrjava

采用一个新的命令行解释器框架,最痛苦地莫过于编写数据结构或者流式定义了。咱们首先回顾一下 cmdr 和其它大多数三方加强命令行解释器都支持的最典型的两种命令行界面定义方式,而后再来研究一下 cmdr 新增的最平滑的迁移方案。c++

典型的方式

经过结构数据体定义

有的加强工具(例如 cobra,viper)采用结构体数据定义方式来完成界面指定,如同 cmdr 的这样:git

rootCmd = &cmdr.RootCommand{
    Command: cmdr.Command{
        BaseOpt: cmdr.BaseOpt{
            Name:            appName,
            Description:     desc,
            LongDescription: longDesc,
            Examples:        examples,
        },
        Flags: []*cmdr.Flag{},
        SubCommands: []*cmdr.Command{
            // generatorCommands,
            // serverCommands,
            msCommands,
            testCommands,
            {
                BaseOpt: cmdr.BaseOpt{
                    Short:       "xy",
                    Full:        "xy-print",
                    Description: `test terminal control sequences`,
                    Action: func(cmd *cmdr.Command, args []string) (err error) {
                        fmt.Println("\x1b[2J") // clear screen

                        for i, s := range args {
                            fmt.Printf("\x1b[s\x1b[%d;%dH%s\x1b[u", 15+i, 30, s)
                        }

                        return
                    },
                },
            },
            {
                BaseOpt: cmdr.BaseOpt{
                    Short:       "mx",
                    Full:        "mx-test",
                    Description: `test new features`,
                    Action: func(cmd *cmdr.Command, args []string) (err error) {
                        fmt.Printf("*** Got pp: %s\n", cmdr.GetString("app.mx-test.password"))
                        fmt.Printf("*** Got msg: %s\n", cmdr.GetString("app.mx-test.message"))
                        return
                    },
                },
                Flags: []*cmdr.Flag{
                    {
                        BaseOpt: cmdr.BaseOpt{
                            Short:       "pp",
                            Full:        "password",
                            Description: "the password requesting.",
                        },
                        DefaultValue: "",
                        ExternalTool: cmdr.ExternalToolPasswordInput,
                    },
                    {
                        BaseOpt: cmdr.BaseOpt{
                            Short:       "m",
                            Full:        "message",
                            Description: "the message requesting.",
                        },
                        DefaultValue: "",
                        ExternalTool: cmdr.ExternalToolEditor,
                    },
                },
            },
        },
    },

    AppName:    appName,
    Version:    cmdr.Version,
    VersionInt: cmdr.VersionInt,
    Copyright:  copyright,
    Author:     "xxx <xxx@gmail.com>",
}
//... More

它的问题在于,若是你有 docker 那样的较多的子命令以及选项须要安排的话,这个方案会至关难定位,写起来也很痛苦,改起来更痛苦。github

经过流式调用链方式定义

比结构体数据定义方案更好一点的是采用流式调用链方式。它可能长得像这样:golang

// root

    root := cmdr.Root(appName, "1.0.1").
        Header("fluent - test for cmdr - no version - hedzr").
        Description(desc, longDesc).
        Examples(examples)
    rootCmd = root.RootCommand()

    // soundex

    root.NewSubCommand().
        Titles("snd", "soundex", "sndx", "sound").
        Description("", "soundex test").
        Group("Test").
        Action(func(cmd *cmdr.Command, args []string) (err error) {
            for ix, s := range args {
                fmt.Printf("%5d. %s => %s\n", ix, s, cmdr.Soundex(s))
            }
            return
        })

    // xy-print

    root.NewSubCommand().
        Titles("xy", "xy-print").
        Description("test terminal control sequences", "test terminal control sequences,\nverbose long descriptions here.").
        Group("Test").
        Action(func(cmd *cmdr.Command, args []string) (err error) {
            fmt.Println("\x1b[2J") // clear screen

            for i, s := range args {
                fmt.Printf("\x1b[s\x1b[%d;%dH%s\x1b[u", 15+i, 30, s)
            }

            return
        })

    // mx-test

    mx := root.NewSubCommand().
        Titles("mx", "mx-test").
        Description("test new features", "test new features,\nverbose long descriptions here.").
        Group("Test").
        Action(func(cmd *cmdr.Command, args []string) (err error) {
            fmt.Printf("*** Got pp: %s\n", cmdr.GetString("app.mx-test.password"))
            fmt.Printf("*** Got msg: %s\n", cmdr.GetString("app.mx-test.message"))
            fmt.Printf("*** Got fruit: %v\n", cmdr.GetString("app.mx-test.fruit"))
            fmt.Printf("*** Got head: %v\n", cmdr.GetInt("app.mx-test.head"))
            return
        })
    mx.NewFlag(cmdr.OptFlagTypeString).
        Titles("pp", "password").
        Description("the password requesting.", "").
        Group("").
        DefaultValue("", "PASSWORD").
        ExternalTool(cmdr.ExternalToolPasswordInput)
    mx.NewFlag(cmdr.OptFlagTypeString).
        Titles("m", "message", "msg").
        Description("the message requesting.", "").
        Group("").
        DefaultValue("", "MESG").
        ExternalTool(cmdr.ExternalToolEditor)
    mx.NewFlag(cmdr.OptFlagTypeString).
        Titles("fr", "fruit").
        Description("the message.", "").
        Group("").
        DefaultValue("", "FRUIT").
        ValidArgs("apple", "banana", "orange")
    mx.NewFlag(cmdr.OptFlagTypeInt).
        Titles("hd", "head").
        Description("the head lines.", "").
        Group("").
        DefaultValue(1, "LINES").
        HeadLike(true, 1, 3000)

    // kv

    kvCmd := root.NewSubCommand().
        Titles("kv", "kvstore").
        Description("consul kv store operations...", ``)
//...More

这种方式颇有效地改进的痛苦之源。要提及来,也没有什么缺点了。因此这也是 cmdr 主要推荐你采用的方案。docker

经过结构 Tag 方式定义

这种方式被有一些第三方解释器所采用,能够算是比较有价值的定义方式。其特色在于直观、易于管理。数据结构

它的典型案例多是这样子的:app

type argT struct {
    cli.Helper
    Port int  `cli:"p,port" usage:"short and long format flags both are supported"`
    X    bool `cli:"x" usage:"boolean type"`
    Y    bool `cli:"y" usage:"boolean type, too"`
}

func main() {
    os.Exit(cli.Run(new(argT), func(ctx *cli.Context) error {
        argv := ctx.Argv().(*argT)
        ctx.String("port=%d, x=%v, y=%v\n", argv.Port, argv.X, argv.Y)
        return nil
    }))
}

不过,因为 cmdr 没有打算支持这种方案,因此这里仅介绍到这个程度。框架

说明一下, cmdr 之因此不打算支持这种方案,是由于这样作好处当然明显,坏处也一样使人烦恼:复杂的定义可能会由于被嵌套在 Tag 内而致使难以编写,例如多行字符串在这里就很难过。

cmdr 新增的兼容 flag 的定义方式

那么,咱们回顾了两种或者三种典型的命令行界面定义方式以后,能够发现他们和 flag 以前的区别是比较大的,当你一开始设计你的 app 时,若是为了便宜和最快开始而采用了 flag 方案的话(毕竟,这是golang自带的包嘛),再要想切换到一个加强版本的话,不管哪个都会令你痛一下。工具

flag 方式

咱们看看当你采用 flag 方式时,你的 main 入口多是这样的:

// old codes

package main

import "flag"

var (
      serv           = flag.String("service", "hello_service", "service name")
      host           = flag.String("host", "localhost", "listening host")
      port           = flag.Int("port", 50001, "listening port")
      reg            = flag.String("reg", "localhost:32379", "register etcd address")
      count          = flag.Int("count", 3, "instance's count")
      connectTimeout = flag.Duration("connect-timeout", 5*time.Second, "connect timeout")
)

func main(){
      flag.Parse()
      // ...
}

迁移到 cmdr

为了迁移为使用 cmdr,你能够简单地替换 import "flag" 语句为这样:

import (
  // “flag”
  "github.com/hedzr/cmdr/flag"
)

其它内容一概不变,也就是说完整的入口如今像这样:

// new codes

package main

import (
  // “flag”
  "github.com/hedzr/cmdr/flag"
)

var (
      serv           = flag.String("service", "hello_service", "service name")
      host           = flag.String("host", "localhost", "listening host")
      port           = flag.Int("port", 50001, "listening port")
      reg            = flag.String("reg", "localhost:32379", "register etcd address")
      count          = flag.Int("count", 3, "instance's count")
      connectTimeout = flag.Duration("connect-timeout", 5*time.Second, "connect timeout")
)
  
func main(){
    flag.Parse()
    // ...
}

怎么样,足够简单吧?

引入加强特性

那么咱们如今指望引入更多 cmdr 专有特性怎么办呢?

例如想要全名(完整单词)做为长选项,补充短选项定义,这能够经过以下的序列来达成:

import (
    // “flag”
      "github.com/hedzr/cmdr"
      "github.com/hedzr/cmdr/flag"
)

var(
    // uncomment this line if you like long opt (such as --service)
    treatAsLongOpt = flag.TreatAsLongOpt(true)
  
    serv = flag.String("service", "hello_service", "service name",
                       flag.WithShort("s"),
                       flag.WithDescription("single line desc", `long desc`))
)

相似的能够完成其余加强特性的定义。

可用的加强特性

全部 cmdr 特性被浓缩在几个少许的接口中了。此外,某些特性是当你使用 cmdr 时就当即得到了,无需其它表述或设定(例如短选项的组合,自动的帮助屏,多级命令等等)。

全部的这些须要指定适当参数的特性,包含在以下的这些定义中:

flag.WithTitles(short, long string, aliases ...string) (opt Option)

定义短选项,长选项,别名。

综合来讲,你必须在某个地方定义了一个选项的长名字,由于这是内容索引的依据,若是长名字缺失,那么可能会有意料以外的错误。

别名是随意的。

若是能够,尽量提供短选项。

短选项通常来讲是一个字母,然而使用两个甚至更多字母是被容许的,这是为了提供多种风格的命令行界面的兼容性。例如 wget, rar 都采用了双字母的短选项。而 golang flag 自身支持的是任意长度的短选项,没有长选项支持。cmdr 在短选项上的宽松和兼容程度,是其它几乎全部第三方命令行参数解释器所不能达到的。

flag.WithShort(short string) (opt Option)

提供短选项定义。

flag.WithLong(long string) (opt Option)

提供长选项定义。

flag.WithAliases(aliases ...string) (opt Option)

提供别名定义。别名是任意多、可选的。

flag.WithDescription(oneLine, long string) (opt Option)

提供描述行文本。

oneLine 提供单行描述文本,这一般是在参数被列表时。long 提供的多行描述文本是可选的,你能够提供空字串给它,这个文本在参数被单独显示帮助详情时会给予用户更多的描述信息。

flag.WithExamples(examples string) (opt Option)

能够提供参数用法的命令行实例样本。

这个字串能够是多行的,它遵守必定的格式要求,咱们之前的文章中对该格式有过描述。这样的格式要求是为了在 man/info page 中可以得到更视觉敏锐的条目,因此你能够自行断定要不要遵照规则。

flag.WithGroup(group string) (opt Option)

命令或者参数都是能够被分组的。

分组是能够被排序的。给 group 字串一个带句点的前缀,则这个前缀会被切割出来用于排序,排序规则是 A-Z0-9a-z 按照 ASCII 顺序。因此:

  • 1001.c++, 1100.golang, 1200.java, …;
  • abcd.c++, b999.golang, zzzz.java, …;

是有顺序的。

因为第一个句点以前的排序用子串被切掉了,所以你的 group 名字能够不受这个序号的影响。

给分组一个空字串,意味着使用内置的 分组,这个分组被排列在其余全部分组以前。

给分组一个 cmdr.UnsortedGroup 常量,则它会被概括到最后一个分组中。值得注意的是,最后一个分组,依赖的是 cmdr.UnsortedGroup 常量的具体值zzzz.unsorted,因此,你仍然有机会定义一个别的序号来绕过这个“最后”。

flag.WithHidden(hidden bool) (opt Option)

hidden为true是,该选项不会被列举到帮助屏中。

flag.WithDeprecated(deprecation string) (opt Option)

通常来讲,你须要给 deprecation 提供一个版本号。这意味着,你提醒最终用户该选项从某个版本号开始就已经被废弃了。

按照 Deprecated 的礼貌规则,咱们废弃一个选项时,首先标记它,并给出替代提示,而后在若干次版本迭代以后正式取消它。

flag.WithAction(action func(cmd *Command, args []string) (err error)) (opt Option)

按照 cmdr 的逻辑,一个选项在被显式命中时,你能够提供一个即时的响应动做,这可能容许你完成一些特别的操做,例如为相关联的其它一组选项调整默认值什么的。

flag.WithToggleGroup(group string) (opt Option)

若是你打算定义一组选项,带有互斥效果,如同 radio button group 那样,那么你能够为它们提供相同的 WithToggleGroup group name这个名字和 WithGroup group name 没有任何关联关系

flag.WithDefaultValue(val interface{}, placeholder string) (opt Option)

提供选项的默认值以及占位符。

默认值的数据类型至关重要,由于这个数据类型时后续抽取该选项真实值的参考依据。

例如,int数据必定要提供一个 int 数值,Duration数据必定要提供一个 3*time.Second 这样的确切数值。

flag.WithExternalTool(envKeyName string) (opt Option)

提供一个环境变量名,例如 EDITOR。那么,若是该选项的 value 部分没有在命令行中被提供时,cmdr 会搜索环境变量的值,将其做为控制台终端应用程序运行,并收集该运行的结果(一个临时文件的文件内容)用于为该选项复制。

如同 git commit -m 那样。

flag.WithValidArgs(list ...string) (opt Option)

提供一个枚举表,用于约束用户所提供的值。

flag.WithHeadLike(enable bool, min, max int64) (opt Option)

当该选项被设定为 enable=true 时,识别用户输入的诸如 -1973, -211 之类的整数短选项,将其整数数值做为本选项的数值。

如同 head -9 等效于 head -n 9 那样。

结束语

好了。不少内容。不过仍是堆出来了,本身欣慰一下。

真正的结束语

嗯,cmdrv1.0.3 是一个 pre-release 版本,咱们已经提供一个 flag 的最平滑迁移的基本实现。

最近的日子里,咱们会考虑完成子命令部分,并最终释出 v1.1.0,请期待。

若是认为这样作有价值的话,考虑去鼓励一下。

相关文章
相关标签/搜索