支持子命令命令行程序支持包开发

支持子命令命令行程序支持包开发

概述

命令行实用程序并不是都象 cat、more、grep 是简单命令。go 项目管理程序,类似 java 项目管理 maven、Nodejs 项目管理程序 npm、git 命令行客户端、 docker 与 kubernetes 容器管理工具等等都是采用了较复杂的命令行。即一个实用程序同时支持多个子命令,每个子命令有各自独立的参数,命令之间可能存在共享的代码或逻辑,同时随着产品的发展,这些命令可能发生功能变化、添加新命令等。因此,符合 OCP 原则 的设计是至关重要的编程需求。

课程任务

  • 了解 Cobra包,使用 cobra 命令行生成一个简单的带子命令的命令行程序
  • 模仿 cobra.Command 编写一个 myCobra 库
  • 将带子命令的命令行处理程序的 import ("github.com/spf13/cobra") 改为 import (corbra "gitee.com/yourId/yourRepo")
  • 使得命令行处理程序修改代价最小,即可正常运行

任务要求

  1. 核心任务,就是模仿 cobra 库的 command.go 重写一个 Command.go
    • 仅允许使用的第三方库 flag "github.com/spf13/pflag"
    • 可以参考、甚至复制原来的代码
    • 必须实现简化版的 type Command struct 定义和方法
    • 不一定完全兼容 github.com/spf13/cobra
    • 可支持简单带子命令的命令行程序开发
  2. 包必须包括以下内容:
    • 生成的中文 api 文档
    • 有较好的 Readme 文件,包括一个简单的使用案例
    • 每个go文件必须有对应的测试文件

GitHub 地址

传送门

设计说明

获取包

输入以下的命令即可获取我实现的 mycobra 包

go get github.com/hupf3/mycobra

或者在 src 的相应目录下输入以下命令

git clone https://github.com/hupf3/mycobra.git

go install

使用包

在代码中直接进行引用:

import "github.com/hupf3/mycobra"

如果习惯用 cobra 库的也可以用以下的命令:

import cobra "github.com/hupf3/mycobra"

简单说明

此次作业 mycobra 包的设计实现,大部分是参考原作者的 cobra 包进行实现的,所以有的部分设计会与原作者重合。主要参考了原作者两个代码文件:command.go,和 cobra.go

包代码文件结构

本次包的实现主要有 5 个代码文件,如下所示:

在这里插入图片描述
  • bench_test.go:该文件是代码的基准测试,用来测试代码执行的时间

  • cobra.go:该文件是基于原作者 cobra.go 的代码文件进行修改实现的

    • hasNoOptDefVal():实现了判断没有 ‘-’ 的参数是否实现

      // hasNoOptDefVal 判断没有‘-’的参数是否存在
      func hasNoOptDefVal(name string, fs *flag.FlagSet) bool {
      	flag := fs.Lookup(name)
      	if flag == nil {
      		return false
      	}
      	return flag.NoOptDefVal != ""
      }
      
    • shortHasNoOptDefVal():实现判断有 ‘-’ 的参数是否存在

      // shortHasNoOptDefVal 判断有‘-’的参数是否存在
      func shortHasNoOptDefVal(name string, fs *flag.FlagSet) bool {
      	if len(name) == 0 {
      		return false
      	}
      
      	flag := fs.ShorthandLookup(name[:1])
      	if flag == nil {
      		return false
      	}
      	return flag.NoOptDefVal != ""
      }
      
    • stripFlags():实现了解析命令参数列表

      // stripFlags 解析命令参数列表
      func stripFlags(args []string, c *Command) []string {
      	if len(args) == 0 {
      		return args
      	}
      
      	commands := []string{}
      	flags := c.Flags()
      Loop:
      	for len(args) > 0 {
      		s := args[0]
      		args = args[1:]
      		switch {
      		case s == "--":
      			// "--" terminates the flags
      			break Loop
      		case strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags):
      			// If '--flag arg' then
      			// delete arg from args.
      			fallthrough // (do the same as below)
      		case strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2 && !shortHasNoOptDefVal(s[1:], flags):
      			// If '-f arg' then
      			// delete 'arg' from args or break the loop if len(args) <= 1.
      			if len(args) <= 1 {
      				break Loop
      			} else {
      				args = args[1:]
      				continue
      			}
      		case s != "" && !strings.HasPrefix(s, "-"):
      			commands = append(commands, s)
      		}
      	}
      
      	return commands
      }
      
    • tmpl():实现对数据执行给定的模板文本,将结果写入w

      // tmpl 对数据执行给定的模板文本,将结果写入w
      func tmpl(w io.Writer, text string, data interface{}) error {
      	t := template.New("usage")
      	t.Funcs(templateFuncs)
      	template.Must(t.Parse(text))
      	return t.Execute(w, data)
      }
      
  • cobra_test.go:是基于 cobra.go 文件的测试文件,主要用于测试该文件的函数实现是否正确

  • command.go:该代码文件主要是实现了命令和命令相关操作的函数,由于实现的代码量较大,就不在此一一详述,只挑几个重要的结构体和函数进行说明,在 API文档中有每个函数的详细说明。

    在这里插入图片描述
    • type Command struct 与命令相关的成员变量和函数的结构体

      // Command 与命令相关的成员变量和函数的结构体
      type Command struct {
      	Use string // 命令的名称
      
      	Short string // 命令短介绍
      
      	Long string // 长命令介绍
      
      	Example string // 如何使用命令的例子
      
      	usageTemplate string // 命令使用模板
      
      	flags *flag.FlagSet // 全部命令参数
      
      	lflags *flag.FlagSet // 仅针对某个命令的参数,局部的参数
      
      	gflags *flag.FlagSet // 针对所有命令的全局参数
      
      	flagErrorBuf *bytes.Buffer // 包含了 pflag 的错误信息
      
      	commands []*Command // 该命令的子命令列表
      
      	parent *Command // 该命令的父命令
      
      	usageFunc func(*Command) error // 命令的使用介绍
      
      	Run func(cmd *Command, args []string) // 执行命令的函数
      }
      
    • func (c *Command) AddCommand(cmds ...*Command):添加该命令下的子命令

      // AddCommand 添加子命令
      func (c *Command) AddCommand(cmds ...*Command) {
      	for i, x := range cmds {
      		if cmds[i] == c {
      			panic("Command can't be a child of itself")
      		}
      		cmds[i].parent = c
      		c.commands = append(c.commands, x)
      	}
      }
      
    • func (c *Command) UsageTemplate() string:返回命令的使用模板(注意:在实现此函数的过程中我发现了一个容易出错的细节,就是在’{{}}'中引用的函数的首字母一定是大写的,如果是小写的会报错)

      // UsageTemplate 返回命令的使用模板
      func (c *Command) UsageTemplate() string {
      	if c.usageTemplate != "" {
      		return c.usageTemplate
      	}
      
      	if c.HasParent() {
      		return c.parent.UsageTemplate()
      	}
      	return `
      {{.GetLong}}
      
      Usage:{{if .Runnable}}
      	{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
      	{{.CommandPath}} [command]
      
      Examples:
      	{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
      
      Available Commands:{{range .Commands}}{{if .IsAvailableCommand}}
      	{{.Name}}: {{.GetShort}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
      
      Flags:
      	{{.LocalFlags.FlagUsages}}{{end}}{{if .HasAvailableGlobalFlags}}
      
      GlobalFlags:
      	{{.GlobalFlags.FlagUsages}}{{end}} {{if .HasAvailableSubCommands}}
      
      Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
      `
      }
      
    • func (c *Command) Find(args []string) (*Command, []string, error) 找到要执行的子命令

      // innerFind 查找要执行的子命令
      func innerFind(cmd *Command, innerArgs []string) (*Command, []string, error) {
      	if innerArgs[0] != cmd.Name() {
      		return cmd, nil, errors.New("The command does not exit")
      	}
      
      	argsWOflags := stripFlags(innerArgs[1:], cmd)
      
      	if len(argsWOflags) > 0 && argsWOflags[0] == "help" {
      		return cmd, nil, errh
      	}
      
      	if len(argsWOflags) == 0 {
      		return cmd, innerArgs[1:], nil
      	}
      
      	sub := argsWOflags[0]
      
      	subCmd := cmd.findSubcmd(sub)
      	if subCmd == nil {
      		return cmd, nil, errors.New("The command does not exit")
      	}
      
      	return innerFind(subCmd, innerArgs[1:])
      }
      
      // Find 找到要执行的子命令
      func (c *Command) Find(args []string) (*Command, []string, error) {
      	commandFound, flags, err := innerFind(c, args)
      	if err == errh {
      		return commandFound, []string{}, errh
      	}
      	if err != nil {
      		return commandFound, flags, err
      	}
      	return commandFound, flags, nil
      }
      
  • command_test.go:是基于 command.go 文件的测试文件,主要用于测试该文件的函数实现是否正确

单元测试

cobra_test.go

此测试文件是基于 cobra.go 文件的测试文件,主要用于测试该文件的函数实现是否正确:

package mycobra

import (
	"reflect"
	"testing"
)

func Test_StripFlags(t *testing.T) {
	test := &Command{
		Use:     "test",
		Short:   "test",
		Long:    "test",
		Example: "test",
	}
	// 三种带参数的方法
	test.Flags().StringP("test1", "a", "", "")
	test.Flags().StringP("test2", "b", "", "")
	test.Flags().StringP("test3", "c", "", "")

	input := []string{"-a", "-b1", "-c=1", "test1", "test2", "test3"}
	r := stripFlags(input, test)
	expected := []string{"test1", "test2", "test3"}

	if !reflect.DeepEqual(r, expected) {
		t.Errorf("expected '%q' but got '%q'", expected, r)
	}
}

写好代码文件后开始进行测试,测试的结果如下:

在这里插入图片描述

通过上面的结果可以得知函数实现的正确,且通过了测试

command_test.go

此文件是基于 command.go 文件的测试文件,主要用于测试该文件的函数实现是否正确

首先在测试文件中定义两个 Command 变量方便后续的测试:

// 根命令
var test1 = &Command{
	Use:     "test1",
	Short:   "test1",
	Long:    "test1",
	Example: "test1",
}

// 子命令
var test2 = &Command{
	Use:     "test2",
	Short:   "test2",
	Long:    "test2",
	Example: "test2",
}
  • func Test_ParseFlags(t *testing.T):该函数测试了命令带参数的情况

    func Test_ParseFlags(t *testing.T) {
    	// 三种带参数的方法
    	test2.Flags().StringP("test1", "a", "", "")
    	test2.Flags().StringP("test2", "b", "", "")
    	test2.Flags().StringP("test3", "c", "", "")
    
    	args := []string{"-a1", "-b=1"}
    	test2.ParseFlags(args)
    	r1, _ := test2.Flags().GetString("test1")
    	r2, _ := test2.Flags().GetString("test2")
    	e1, e2 := "1", "1"
    
    	if r1 != e1 || r2 != e2 {
    		t.Errorf("expected '%s', '%s' but got '%s', '%s'", e1, e2, r1, r2)
    	}
    }
    

    进行测试后的结果如下:

    在这里插入图片描述
  • func Test_GlobalFlags(t *testing.T) 用来测试全局的命令参数

    // 测试全局的命令参数
    func Test_GlobalFlags(t *testing.T) {
    	test1.AddCommand(test2)
    	test2.getGflags().StringP("test", "t", "", "")
    	args := []string{"-thupf"}
    	test2.ParseFlags(args)
    	r, _ := test1.getGflags().GetString("test")
    
    	expected := "hupf"
    	if r != expected {
    		t.Errorf("expected '%s', but got '%s'", expected, r)
    	}
    }
    

    进行测试后的结果如下:

    在这里插入图片描述
  • func Test_LocalFlags(t *testing.T) 测试局部的命令参数

    // 测试局部的命令参数
    func Test_LocalFlags(t *testing.T) {
    	test1.AddCommand(test2)
    	test2.LocalFlags().StringP("test", "t", "", "")
    	args := []string{"-thupf"}
    	test2.ParseFlags(args)
    	r1, _ := test1.getGflags().GetString("test")
    	e1 := ""
    	r2, _ := test2.LocalFlags().GetString("test")
    	e2 := "hupf"
    
    	if r1 != e1 || r2 != e2 {
    		t.Errorf("expected '%s', '%s' but got '%s', '%s'", e1, e2, r1, r2)
    	}
    }
    

    进行测试后的结果如下:

    在这里插入图片描述
  • func Test_Flags(t *testing.T) 测试所有的命令参数

    // 测试所有的命令参数
    func Test_Flags(t *testing.T) {
    	test1.AddCommand(test2)
    	test2.LocalFlags().StringP("local", "l", "", "")
    	test1.getGflags().StringP("global", "g", "", "")
    	args := []string{"-ltestl", "-gtestg"}
    	test2.ParseFlags(args)
    	test1.ParseFlags(args)
    	r1, _ := test2.Flags().GetString("local")
    	e1 := "testl"
    	r2, _ := test2.Flags().GetString("global")
    	e2 := "testg"
    	if r1 != e1 || r2 != e2 {
    		t.Errorf("expected '%s', '%s' but got '%s', '%s'", e1, e2, r1, r2)
    	}
    }
    

    进行测试后的结果如下:

    在这里插入图片描述
  • func Test_CommandPath(t *testing.T) 测试命令路径的正确性

    // 测试命令路径的正确性
    func Test_CommandPath(t *testing.T) {
    	test1.AddCommand(test2)
    	r := test2.CommandPath()
    
    	expected := "test1 test2"
    	if r != expected {
    		t.Errorf("expected '%s', but got '%s'", expected, r)
    	}
    }
    

    进行测试后的结果如下:

    在这里插入图片描述

功能测试

基准测试

基准测试 bench_test 用来测试函数运行的时间是如何,该函数的实现过程就是首先定义一个命令,然后执行此命令,进行测试:

package mycobra

import (
	"testing"
)

func BenchmarkCommand_Execute(b *testing.B) {
	var test = &Command{
		Use:     "test",
		Short:   "test",
		Long:    "test test",
		Example: "test",
	}
	for i := 0; i < b.N; i++ {
		test.Execute()
	}
}

测试的结果如下所示:

在这里插入图片描述

获取使用案例

我设计了一个 pinfo 命令,来实现显示自己的个人信息(personal information),获取该使用案例的命令如下:

go get github.com/hupf3/pinfo

或者在相应的 src 目录结构下是用以下命令:

git clone https://github.com/hupf3/pinfo.git

go install

执行完毕上面的命令后,在 GOPATH 路径中的 bin 文件夹下面会多出一个 pinfo 的可执行文件,即为成功

使用案例测试

获取完上面的包后,打开命令行,输入 pinfo 测试是否成功安装 pinfo 命令

在这里插入图片描述

如果出现如上图所示的 pinfo 命令的说明即可证明成功安装 pinfo 命令

然后按照说明可以得知,该命令是获取个人信息的命令,该命令下有三个字命令,分别为:age 获取年龄, id 获取学号, name获取名字

为了实现带参数的命令,我在 name 子命令中定义了三个参数,-f 获取姓,-g 获取名,-a 获取全称

我还实现了 help 相当于是全局的参数,当命令行输入command + help 时会显示该 command 的使用用法,示例如下:

在这里插入图片描述 在这里插入图片描述

测试完 help 功能后可以进行测试 age 命令是否实现:

在这里插入图片描述

测试完 age 功能后可以进行测试 id 命令是否实现:

在这里插入图片描述

测试完 id 功能后可以进行测试 name 各个命令参数是否实现:

在这里插入图片描述

至此完成了全部的功能测试

API文档

生成网页版的 API 文档,输入以下的命令:

godoc -http=:8080

然后在浏览器中打开 http://127.0.0.1:8080 ,即可访问网页版的 go doc:

在这里插入图片描述

然后网页搜索 mycobra 即可找到我实现的程序包:

在这里插入图片描述

点开即可查看我实现的 mycobra 包中的函数和具体说明以及引用该包的方法:

在这里插入图片描述

在目录结构下执行以下命令,即可生成线下的 html 文件

go doc

godoc -url="pkg/github.com/hupf3/mycobra" > API.html

我将该文档也保存在了 github 仓库中方便检查

总结

通过本次实验提升了自己阅读程序包的能力,并且能够根据已有的程序包提取有用的信息,改善程序包,变成自己的程序包。在本次实验也注意到了一个易错点,就是双大括号引用下的函数,函数名的首字母一定要大写

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值