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

一. 概述

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

二. 课程任务

在这里插入图片描述

三. 设计说明

模仿Cobra库官方文档中的使用示例创建简单的带子命令的命令行程序。文件结构如下:
在这里插入图片描述
首先在root.go中定义rootCmd结构体,其会在被调用时输出 “Hello World!” :

var rootCmd = &cobra.Command{
	Use:   "easyApp",
	Short: "an easy app which could print Hello World!",
	Long:  `easy easy easy easy easy easy easy easy easy easy`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("Hello world!\n")
	},
}

然后在date.go中定义dateCmd结构体,其会在被调用时输出当前的日期:

var dateCmd = &cobra.Command{
	Use:   "date",
	Short: "return current date",
	Long:  `the subcommand which is called by easyApp, return current date`,
	Run: func(cmd *cobra.Command, args []string) {
		currentTime := time.Now()
		year := currentTime.Year()
		month := currentTime.Month()
		day := currentTime.Day()
		fmt.Println("Current date: ", year, month, day)
	},
}

然后将dateCmd添加为rootCmd的子命令:

func init() {
	rootCmd.AddCommand(dateCmd)
}

main.go仿照官方文档设计如下,其仅仅调用Cobra库中的Execute方法:

package main

import (
	"github.com/github-user/easyApp/cmd"
)

func main() {
	cmd.Execute()
}

测试结果
首先使用以下命令安装整个包:

go install github.com/github-user/easyApp

分别测试不带子命令,带子命令,带参数的情况,均得到预期的输出:
在这里插入图片描述
接下来模仿 cobra 库的 command.go 重写一个 Command.go。
首先阅读cobra库中command.go的源代码,选择其中必要的方法和变量来实现简化版的 type Command struct 定义和方法。简化的Command结构体定义如下:

type Command struct {
	// Use is the one-line usage message.
	Use string

	// Short is the short description shown in the 'help' output.
	Short string

	// Long is the long message shown in the 'help <this-command>' output.
	Long string

	// commands is the list of commands supported by this program.
	commands []*Command

	// parent is a parent command for this command.
	parent *Command

	//the sub command which are used
	sub *Command

	// Run: Typically the actual work function. Most commands will only implement this.
	Run func(cmd *Command, args []string)

	// args is actual args parsed from flags.
	args []string

	pflags *flag.FlagSet
}

AddCommand方法用于给一条命令添加子命令,遍历已有的子命令看是否已存在,不存在就添加进去

func (c *Command) AddCommand(subCommand *Command) {
	for _, v := range c.commands {
		if v == subCommand {
			return
		}
	}
	c.commands = append(c.commands, subCommand)
	subCommand.parent = c
}

Execute方法首先判断一条命令是否为空,若为空则直接退出,若为根命令则调用getArgs方法解析参数,若是子命令则直接调用execute方法:

func (c *Command) Execute() error {
	if c == nil {
		return fmt.Errorf("The command is nil")
	}
	if c.parent == nil { //the root command
		getArgs(c, os.Args[1:])
	}
	c.execute()
	return nil
}

getArgs方法检测参数里是否有子命令,若有子命令则递归解析子命令的参数,否则直接把参数解析给根命令:

func getArgs(c *Command, args []string) {
	if len(args) < 1 {
		return
	}
	for _, v := range c.commands { //traverse all subcommands
		if v.Use == args[0] {
			c.args = args[:1]
			c.sub = v
			getArgs(v, args[1:])
			return
		}
	}
	c.args = args
	c.PersistentFlags().Parse(c.args)
}

execute是真正的执行函数,若没有子命令则检测参数里是否有 -h 或 --help,有的话执行PrintHelp函数;没有则直接执行Run方法;若有子命令则递归调用子命令的execute函数:

func (c *Command) execute() {
	if c.sub == nil {
		for _, v := range c.args {
			if v == "-h" || v == "--help" {
				c.PrintHelp()
				return
			}
		}
		c.Run(c, c.args)
		return
	}
	c.sub.execute()
}

Name方法返回命令的名字

func (c *Command) Name() string {
	name := c.Use
	i := strings.Index(name, " ")
	if i >= 0 {
		name = name[:i]
	}
	return name
}

功能测试
首先安装自己写的库:

go install github.com/github-user/myCobra

然后将带子命令的命令行处理程序的 import (“github.com/spf13/cobra”) 改为 import (cobra “github.com/github-user/myCobra”)
按照之前的测试方法继续测试,分别测试不带子命令,带子命令,带参数的情况,均得到预期的输出:
在这里插入图片描述
单元测试
测试代码如下:

package myCobra

import (
	"fmt"
	"os"
	"testing"
)

var rootCmd = &Command{
	Use:   "root",
	Short: "root command for testing",
	Long:  "testing testing testing testing testing testing testing",
	Run: func(cmd *Command, args []string) {
		fmt.Printf("running testing command\n")
	},
}

func TestAddCommand(t *testing.T) {
	subCmd1 := &Command{
		Use:   "sub1",
		Short: "sub command 1",
		Long:  "sub1 sub1 sub1 sub1 sub1 sub1 sub1 sub1",
		Run: func(cmd *Command, args []string) {
			fmt.Printf("sub command test 1\n")
		},
	}

	rootCmd.AddCommand(subCmd1)
	for _, v := range rootCmd.commands {
		if v == subCmd1 {
			return
		}
	}
	t.Errorf("expected: %v\n", []*Command{subCmd1})
	t.Errorf("got:      %v\n", rootCmd.commands)
}

func TestExecute(t *testing.T) {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func TestName(t *testing.T) {
	name := rootCmd.Name()
	if name != "root" {
		t.Errorf("expected: %s", "root")
		t.Errorf("got:      %s", name)
	}
}

测试结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值