一. 概述
命令行实用程序并不是都象 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)
}
}
测试结果: