Migration from golang flag to cmdr

Be based on cmdr v1.0.3

From golang flag to cmdr

With a new command line interpreter framework, the most painful thing is to write data structures or streaming definitions. Let's first look back. cmdr The two most typical command-line interface definitions supported by most other three-party enhanced command-line interpreters will be studied later. cmdr New smoothest migration scheme.

Typical way

Definition of Structured Data Volume

Some enhancement tools (such as cobra, viper) use structured data definition to complete interface specification, such as cmdr In this way:

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

The problem is that if you have more subcommands and options like docker that need to be arranged, the solution will be quite difficult to locate, painful to write, and even more painful to change.

Definition through streaming call chains

Better than the structured data definition scheme is the streaming call chain approach. It may look like this:

    // 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

This is a painful source of effective improvement. Speaking of it, there are no shortcomings. So this is also the case. cmdr The main recommendation is the scheme you adopt.

Definition by Structural Tag

This method has been adopted by some third-party interpreters, which can be regarded as a valuable way of defining. Its characteristics are intuitive and easy to manage.

Its typical case may be as follows:

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

However, since cmdr does not intend to support such a solution, this is only to the extent described here.

To illustrate, the reason why cmdr does not intend to support such a solution is that while the advantages are obvious, the disadvantages are equally troubling: complex definitions can be hard to write because they are nested in Tag, for example, multi-line strings are difficult here.

New Definition of Compatible flag in cmdr

So, after reviewing two or three typical command line interface definitions, we can see that they are quite different from flag before. When you design your app at the beginning, if you use flag solution for cheaper and quickest start (after all, this is a package from golang), then again. If you want to switch to an enhanced version, either one will hurt you.

flag mode

Let's see that when you use flag, your main entrance may be like this:

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

Migration to cmdr

To migrate to cmdr, you can simply replace the import "flag" statement as follows:

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

The rest of the content remains the same, that is to say, the complete entrance is now like this:

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

So, is it simple enough?

Introducing Enhancement Characteristics

So what do we expect now to introduce more cmdr proprietary features?

For example, if you want full name (complete word) as a long option and add short option definition, this can be achieved through the following sequence:

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`))
)

Similarly, other enhancements can be defined.

Available enhancement characteristics

All cmdr features are concentrated in a few small interfaces. In addition, some features are immediately available when you use cmdr without any other presentation or settings (e.g. combinations of short options, automatic help screens, multilevel commands, etc.).

All of these features, which need to specify appropriate parameters, are included in the following definitions:

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

Define short options, long options, aliases.

To sum up, you have to define a long name for an option somewhere, because that's the basis for content indexing. If long names are missing, there may be unexpected errors.

Aliases are optional.

If possible, provide short options as far as possible.

Short options are generally a letter, but using two or more letters is allowed to provide compatibility with a variety of command-line interfaces. For example, WGet and RAR both use short double-letter options. golang flag itself supports short options of any length, but no long options. The ease and compatibility of cmdr with short options are beyond the reach of almost all other third-party command-line parameter interpreters.

flag.WithShort(short string) (opt Option)

Provide short option definitions.

flag.WithLong(long string) (opt Option)

Provide long option definitions.

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

Provide alias definitions. Aliases are optional.

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

Provide descriptive line text.

OnLine provides a single line of descriptive text, usually when the parameters are listed. The multi-line description text provided by long is optional. You can provide it with empty strings. This text will give users more description information when the parameters are displayed separately.

flag.WithExamples(examples string) (opt Option)

Command-line instance samples of parameter usage can be provided.

This string can be multi-line and conforms to certain format requirements, which were described in our previous articles. The formatting requirement is to get more visual items in man/info page s, so you can decide whether to follow the rules or not.

flag.WithGroup(group string) (opt Option)

Commands or parameters can be grouped.

Groups can be sorted. Given a period prefix to the group string, the prefix is cut out for sorting. The sorting rule is A-Z0-9a-z in ASCII order. So:

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

There is order.

Since the substring before the first period is cut off, your group name can not be affected by this number.

Giving a group an empty string means using a built-in empty group, which is ranked before all other groups.

Given a cmdr.UnsortedGroup constant for a group, it is summed up in the last group. Notably, the last grouping relies on the specific value of the cmdr.UnsortedGroup constant zzzzzz. unsorted, so you still have the opportunity to define a different ordinal number to bypass this "last".

flag.WithHidden(hidden bool) (opt Option)

If hidden is true, this option will not be listed in the help screen.

flag.WithDeprecated(deprecation string) (opt Option)

Generally speaking, you need to give deprecation a version number. This means that you remind the end user that the option has been discarded since a version number.

According to the politeness rule of Deprecated, when we discard an option, we first mark it and give an alternative hint, then cancel it formally after several iterations of versions.

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

According to the logic of cmdr, when an option is explicitly hit, you can provide an immediate response action, which may allow you to perform some special operations, such as adjusting default values for the associated set of other options.

flag.WithToggleGroup(group string) (opt Option)

If you intend to define a set of options with mutually exclusive effects, like radio button group, then you can provide them with the same WithToggle Group name, which has nothing to do with WithGroup name.

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

Provides default values for options and placeholders.

The data type of the default value is quite important, because this data type is the reference for subsequent extraction of the true value of the option.

For example, the int data must provide an int value, and the Duration data must provide an exact value such as 3*time.Second.

flag.WithExternalTool(envKeyName string) (opt Option)

Provide an environment variable name, such as EDITOR. Then, if the value part of the option is not provided on the command line, the cmdr searches for the value of the environment variable, runs it as a console terminal application, and collects the results of the run (the file content of a temporary file) for replication of the option.

Like git commit -m.

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

Provides an enumeration table that constrains the values provided by the user.

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

When this option is set to enable=true, identify short integer options such as - 1973, - 211 entered by the user and use their integer values as the values of this option.

Just as head-9 is equivalent to head-n-9.

Concluding remarks

Okay. A lot of content. But it's still piled up and I'm glad to see it.

True concluding remarks

Well, v1.0.3 of cmdr is a pre-release version, and we have provided a basic implementation of flag's smoothest migration.

In recent days, we will consider completing the subcommand section and finally releasing v1.1.0. Please look forward to it.

If you think it's worthwhile, consider encouraging it.

Keywords: Linux github Java Docker REST

Added by Hepp on Tue, 23 Jul 2019 17:09:30 +0300