关于 Go 语言优秀命令行库 Cobra 的探索

二叶草 2020年4月10日15:37:25函数代码关于 Go 语言优秀命令行库 Cobra 的探索已关闭评论阅读模式

Cobra 是 Golang 生态系统中最着名的项目之一。它简单,高效,并得到 Go 社区的大力支持。让我们来深入探索一下。

设计

Cobra 中的 Command 是一个具有名称,使用描述和运行逻辑函数的结构体:

cmd := &cobra.Command{
    Run:   runGreet,
    Use:   `greet`,
    Short: "Greet",
    Long:  "This command will print Hello World",
}

设计非常类似于原生的 go 标准库命令,如 go env,go fmt等

比如,go fmt 命令结构:

var CmdFmt = &base.Command{
    Run:       runFmt,
    UsageLine: "go fmt [-n] [-x] [packages]",
    Short:     "gofmt (reformat) package sources",
    Long: `
Fmt runs the command 'gofmt -l -w' on the packages named
by the import paths. It prints the names of the files that are modified.
For more about gofmt, see 'go doc cmd/gofmt'.
For more about specifying packages, see 'go help packages'.
The -n flag prints commands that would be executed.
The -x flag prints commands as they are executed.
To run gofmt with specific options, run gofmt itself.
See also: go fix, go vet.
    `,
}

如果你熟悉了 Cobra,很容易理解内部命令是如何工作的,反之亦然。我们可能会想,当 Go 已经定义了命令接口后,为什么还要要使用外部库?

Go 标准库定义的接口:

type Command struct {
// Run 运行命令.
// 参数在命令之后
    Run func(cmd *Command, args []string)

    // UsageLine 是一行描述信息.
    // 其中第一个词语应是命令.
UsageLine string

    // Short 是 'go help' 输出的简单描述.
Short string

    // Long 是 'go help <this-command>' 输出的详细描述.
Long string

    // Flag 一组特定于此命令的标志码.
Flag flag.FlagSet

    // CustomFlags 表示了命令将执行自定义的标志解析
CustomFlags bool

    // Commands 列举可用的命令和 help 主题.
    // 顺序和 'go help' 输出一致.
    // 注意:通常最好避免使用子命令.
Commands []*Command
}

此接口是仅适用于标准库的内部包的一部分。在 2014 年 6 月的 Go 1.4 版本中,Russ Cox 提出了 限制使用内部包和命令的建议[1]。基于此内部包构建命令会带来错误:

package main

import (
"cmd/go/internal/base"
)

func main() {
    cmd := &base.Command{
        Run: func(cmd *base.Command, args []string) {
println(`Hello`)
        },
        Short: `Hello`,
    }
    cmd.Run(cmd, []string{})
}
main.go:4:2: use of internal package cmd/go/internal/base not allowed

然而,正如 Cobra 创建者 Steve Francia[2] 所解释的那样:这个内部界面设计 催生了了 Cobra[3](Steve Franci 在 Google 工作并曾直接参与了 Go 项目。)。

该项目也建立在来自同一作者的 pflag 项目[4] 之上,提供符合 POSIX 标准。因此,程序包支持短标记和长标记,如-e替代--example ,或者多个选项,如-abc 和-a,-b 和-c 都是是有效选项。这旨在改进 Go 库中的 flag 包,该库仅支持标志-xxx。

特性

Cobra 有一些值得了解的简便方法:

  • Cobra 提供了两种方法来运行我们的逻辑:Run func(cmd *Command, args []string) 和 RunE func(cmd *Command, args []string) error ,后者可以返回一个错误,我们将能够从 Execute() 方法的返回中捕获。

  • Command 结构 提供了一个 Aliases(别名) 属性,允许我们将命令迁移到一个新名称,而不需要在alias属性中通过映射旧名称来破坏现有的行为。这种兼容性策略甚至可以通过使用 Deprecated 属性来增强,该属性允许您将一个命令标记为Deprecated(即将弃用,不推荐使用),并在删除它之前提供一个简短的说明。

  • 由于每个命令都可以嵌入其他命令,因此 Cobra 本身支持嵌套命令,并允许我们像下边这样编写:

go run main.go foo bar

在这里, foo 是命令,bar 是嵌套命令:

package main

import (
"github.com/spf13/cobra"
)

func main() {
    cmd := newCommand() // 构建一般命令
    cmd.AddCommand(newNestedCommand()) // 加入嵌套命令

    rootCmd := &cobra.Command{}
    rootCmd.AddCommand(cmd)

if err := rootCmd.Execute(); err != nil {
println(err.Error())
    }
}

func newCommand() *cobra.Command {
    cmd := &cobra.Command{
        Run:  func (cmd *cobra.Command, args []string) {
println(`Foo`)
        },
        Use:   `foo`,
        Short: "Command foo",
        Long:  "This is a command",
    }

return cmd
}

func newNestedCommand() *cobra.Command {
    cmd := &cobra.Command{
        Run:  func (cmd *cobra.Command, args []string) {
println(`Bar`)
        },
        Use:   `bar`,
        Short: "Command bar",
        Long:  "This is a nested command",
    }

return cmd
}

可以使用嵌套命令是 决定构建 Cobra[5]的主要动机之一

轻量

这个库的代码主要包含一个文件,而且很好理解,它不会影响你程序的性能。接下来,我们做一个压力测试(benchmark):

package main

import (
"github.com/spf13/cobra"
"math/rand"
"os"
"strconv"
"testing"
)

func BenchmarkCmd(b *testing.B) {
for i := 0; i < b.N; i++ {
        root := &cobra.Command{
            Run: func(cmd *cobra.Command, args []string) {
println(`main`)
            },
            Use:   `test`,
            Short: "test",
        }

        max := 100
for c := 0; c < max; c++ {
            cmd := &cobra.Command{
                Run: func(cmd *cobra.Command, args []string) {
                    _ = c
                },
                Use:   `test-`+strconv.Itoa(c),
                Short: `test `+strconv.Itoa(c),
            }
            root.AddCommand(cmd)
        }

        r := rand.Intn(max)
        os.Args = []string{"go", "test-"+strconv.Itoa(r)}
        _ = root.Execute()
    }
}

Cobra 运行 50 条命令只有 49.0μs 负载:

name   time/op
Cmd-8  49.0µs ± 1%

name   alloc/op
Cmd-8  78.3kB ± 0%

name   allocs/op
Cmd-8  646 ± 0%

由于 Cobra 被设计运行在 CLI 模式下, 性能并不重要, 但是可以看出这个库有多么轻量.

可替代性

即使 Cobra 倾向于成为 Go 社区的标准包 - 浏览最近使用 Cobra 的项目[6]证实了这一点 - 了解 Go 生态系统中有关 CLI 接口的内容总是好的。

让我们回顾两个可以替代 Cobra 的项目:

  • cli[7],一个用于构建命令行应用程序的包。这个包和 Cobra 一样流行,与嵌套命令,bash 补全,hook(钩子),alias(别名)等非常相似。但是,与 Cobra 不同,这个包使用 Go 库中的原生flag包。

urfave/cli 例子:

package main

import (
"log"
"os"
"github.com/urfave/cli"
)

func main() {
    app := cli.NewApp()
    app.Commands = []cli.Command{
        {
            Action:  func(c *cli.Context) error {
println("Hello world")
return nil
            },
            Name:   `greet`,
            Usage:  "This command will print Hello World",
        },
    }

    err := app.Run(os.Args)
if err != nil {
        log.Fatal(err)
    }
}
  • subcommands[8]:虽然托管在 Google 的 Github 帐户中,但该项目并非官方 Google 产品。该库也很简单

google/subcommands 示例

package main

import (
"context"
"flag"
"github.com/google/subcommands"
)

type GreetCommand struct {}
func (g *GreetCommand) Name() string     { return "greet" }
func (g *GreetCommand) Synopsis() string { return "Greet the world." }
func (g *GreetCommand) Usage() string { return `Print Hello World.` }
func (g *GreetCommand) SetFlags(*flag.FlagSet) {}
func (p *GreetCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
println(`Hello World`)

return subcommands.ExitSuccess
}

func main() {
    subcommands.Register(&GreetCommand{}, "foo")

    flag.Parse()
    subcommands.Execute(context.Background())
}

如我们之前看到的 Cobra 或 CLI,该库基于一个接口而不是一个结构体,因此并使代码稍显冗长。

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
二叶草
Go语言中的常量 函数代码

Go语言中的常量

1 概述 常量,一经定义不可更改的量。功能角度看,当出现不需要被更改的数据时,应该使用常量进行存储,例如圆周率。从语法的角度看,使用常量可以保证数据,在整个运行期间内,不会被更改。例如当前处理器的架构...
Go语言的接口 函数代码

Go语言的接口

Go语言-接口 在Go语言中,一个接口类型总是代表着某一种类型(即所有实现它的类型)的行为。一个接口类型的声明通常会包含关键字type、类型名称、关键字interface以及由花括号包裹的若干方法声明...
Go语言支持的正则语法 函数代码

Go语言支持的正则语法

1 字符 语法 说明 . 任意字符,在单行模式(s标志)下,也可以匹配换行 字符类 否定字符类 d Perl 字符类 D 否定 Perl 字符类 ASCII 字符类 否定 ASCII 字符类 pN U...
Go语言的包管理 函数代码

Go语言的包管理

1 概述 Go 语言的源码复用建立在包(package)基础之上。包通过 package, import, GOPATH 操作完成。 2 main包 Go 语言的入口 main() 函数所在的包(pa...