服务计算第五次作业
支持子命令命令行程序支持包开发
一. 概述
命令行实用程序并不是都象 cat、more、grep 是简单命令。go项目管理程序,类似 java 项目管理 maven、Nodejs 项目管理程序npm 、git 命令行客户端、 docker 与 kubernetes 容器管理工具等等都是采用了较复杂的命令行。即一个实用程序同时支持多个子命令,每个子命令有各自独立的参数,命令之间可能存在共享的代码或逻辑,同时随着产品的发展,这些命令可能发生功能变化、添加新命令等。因此,符合 OCP 原则 的设计是至关重要的编程需求。
二. 课程任务
任务一
了解Cobra包,使用 cobra 命令行生成一个简单的带子命令的命令行程序
-
cobra简介:
cobra是一个构建命令行应用接口的工具 ,同时是一个用于生成应用以及命令文件的工具,cobra已经被许多go工程使用,如Github,以及Hugo等;
-
cobra 应用过程:
a). cobra包的安装:
因为golang并不自带cobra包,所以我们需要自行安装cobra包,以前使用的安装方式:
go get -v github.com/spf13/cobra/cobra
在golang.org官网无法在中国访问的情况下已经不能使用,作为替代,我们使用如下的三条指令来代替上方的安装指令:
git clone https://github.com/golang/text git clone https://github.com/golang/sys go install github.com/spf13/cobra/cobra
安装成功之后,你将在$GOPATH\bin下找到cobra.exe的可执行程序;
b). 创建cobra应用:
我们在$GOPATH/src的目录下创建一个cobra应用,叫做demo;
先进入到$GOPATH/src目录下,再使用以下指令来创建一个新的cobra目录:
cobra init demo --pkg-name=demo
命令解释:
cobra创建一个新的cobra应用的基本语法如下:
cobra init [name] [flags]
根据我们上面的命令来分析,
demo
为新创建的cobra应用的名字,--pkg-name=demo
为创建demo应用时的命令行参数;需要指出的一点是:--pkg-name=demo
这一个命令行参数必须给出,不然,cobra不会创建demo应用。还有需要注意的是--pkg-name
后面的参数值必须和name相同,否则在创建出来的main.go中,它将访问不到demo/cmd中的文件。正确运行了cobra的init指令之后,我们将在$GOPATH/src目录下新建一个名叫demo的cobra应用,它的目录结构如下:
demo\ cmd\ root.go LICENSE main.go
为了了解我们的cobra到底做了什么,我们打开root.go以及main.go文件,查看它们的实现;
-
root.go:
package cmd import ( "fmt" "github.com/spf13/cobra" "os" // homedir "github.com/mitchellh/go-homedir" // "github.com/spf13/viper" ) var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "demo", Short: "A brief description of your application", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } }
因为原文件代码量较大,我们只分析root.go中当前涉及的一部分;
root.go实际上定义了根命令,当我们使用
go run main.go
指令时,我们将调用根命令;而所有的命令的具体定义都被保存在cobra/command.go中的Command结构体中。上方用于初始化命令的几个变量的含义如下:- Use:为使用命令时所用的命令名称,如使用根命令时在命令行输入demo即可;
- Short:在使用help命令查看时输出的描述信息;
- long:在使用 ”help “命令查看命令使用方法时输出的描述信息;
-
main.go
package main import "demo/cmd" func main() { cmd.Execute() }
相比于root.go的代码量,main.go的代码量极少,只是调用了cmd.Execute函数便结束了。实际上main.go调用的Execute函数即为root.go中的Execute函数,那么main.go函数实际上是调用了根命令rootCmd的Execute函数;
那么,我们来看看这个程序的输出:
先使用go install demo编译安装demo,再使用demo运行这个程序;
可以看到,我们的程序输出了long中的描述字段;
c). 为根命令增加动作:
在根命令的初始化过程中,cobra默认注释掉了命令的Run参数,而Run参数是为根命令运行时服务的。因为在命令结构体的Execute函数中,Execute函数实际上执行的就是命令的Run参数中所定义的函数,而在Run参数没有定义的时候,Execute函数将默认打印出命令的long参数中的命令描述。Execute函数的具体定义可以见cobra/command.go。那么我们给根命令初始化Run参数,使它按照我们定义的函数运行;
package cmd import ( "fmt" "github.com/spf13/cobra" "os" homedir "github.com/mitchellh/go-homedir" "github.com/spf13/viper" ) var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "demo", Short: "A brief description of your application", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, // Uncomment the following line if your bare application // has an action associated with it: Run: func(cmd *cobra.Command, args []string) { fmt.Println("this is a test to run") }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } }
可以看到,我们在根命令的初始化中加上了Run参数,使它输出"this is a test for run"的一段字符串。我们重新编译安装demo之后,再运行一次demo以检测我们的函数是否正确。
可以看到,命令执行的结果和预期一致;
d). 为应用增加子命令:
现在,我们为demo应用增加一个名为test的子命令;方法很简单,在demo的目录下运行
cobra add test
即可,增加子命令文件之后的文件目录树
demo\ cmd\ test.go root.go LICENSE main.go
同样的,我们打开test.go,看看它是怎么实现的:
package cmd import ( "fmt" "github.com/spf13/cobra" ) // testCmd represents the test command var testCmd = &cobra.Command{ Use: "test", Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("test called") }, } func init() { rootCmd.AddCommand(testCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // testCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // testCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") }
可以看到,在test.go中,我们和root.go中一样,利用cobra.command结构体定义了一个命令,在test.go中我们定义的命令名为testCmd,并给出了Run参数。在Run参数中,我们输出了字符串“test called”,那么我们调用test子命令时应当会输出字符串“test called”。而在这个.go文件的初始化函数init函数中,我们将代表test子命令的testCmd加入到代表根命令的rootCmd中,成为它的子命令。AddCommand函数的定义同样可以在cobra/command.go中找到;那么我们重新编译安装demo,然后运行test子命令,得到以下输出
可以看到,我们使用子命令的时候执行的只有子命令的Run函数,而没有使用根命令的Run函数,因此不会输出“this is a test for run”;
e). 为应用增加参数:
命令行参数是执行一个命令所必不可少的,或者说极为重要的部分。因此,为应用增加参数十分重要。我们从test.go的init注解中便可以了解到,如果要为一个指令增加命令行参数,那么就调用命令的Flags或是PersistentFlags方法即可为命令增加对应的命令行参数,这个用法和flag库的用法十分相似;那么我们就为test增加一个-v的整数类型的命令行参数:
package cmd import ( "fmt" "github.com/spf13/cobra" ) // testCmd represents the test command var testCmd = &cobra.Command{ Use: "test", Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("test called") }, } func init() { rootCmd.AddCommand(testCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // testCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: testCmd.Flags().IntP("version", "v", 1, "Help message for version") }
我们在将新的demo重新编译安装之前,先利用-h命令输出test子命令的相应信息:
可以看到test子命令除了有Flags参数列表之外,还有Global Flags参数列表。位于Global Flags参数列表的命令行参数实际上是继承自父命令的用PersistentFlags定义的命令行参数,我们可以重新打开root.go进行查看;
func init() { cobra.OnInitialize(initConfig) // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.demo.yaml)") // Cobra also supports local flags, which will only run // when this action is called directly. rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") }
可以看到,我们是在根命令的init函数中定义了config命令行参数,而config参数被根命令的子命令test继承了,但是test子命令并没有继承根命令的-t参数。所以,这就是PersistentFlags方法定义的命令行参数和Flags定义的命令行参数的不同,PersistentFlags定义的命令行参数可以被子命令继承与使用,但是Flags定义的命令行参数则不行。
在重新编译安装之后,我们在使用-h命令获得test子命令的相关信息:
可以看到,在加上了新的命令参数之后,test子命令得到了一个新的命令行参数,-v;
我们运行一下这个命令,为了让我们输入的参数在输出中有所反应,我们更改一下test子命令的Run函数,让它输出我们传入的参数:
Run: func(cmd *cobra.Command, args []string) { fmt.Println("test called") num, err := cmd.Flags().GetInt("version") if err == nil{ fmt.Printf("the num user input is %d\n", num); } },
在这里我们使用cmd.Flags().GetInt(“version”)来获得命令行参数的值;
最终结果为:
完美;
-
任务二
模仿 cobra.Command 编写一个 myCobra 库
要求:
- 将带子命令的命令行处理程序的
import ("github.com/spf13/cobra")
改为import (corbra "gitee.com/yourId/yourRepo")
; - 使得命令行处理程序修改代价最小,即可正常运行;
- 仅允许使用的第三方库
flag "github.com/spf13/pflag"
; - 可以参考、甚至复制原来的代码;
- 必须实现简化版的
type Command struct
定义和方法; - 不一定完全兼容
github.com/spf13/cobra
; - 可支持简单带子命令的命令行程序开发;
阶段一:让最简单的根命令跑起来;
为了让我们的实现能够一步步进行,我们将command函数的实现分步骤进行。并将原来的cmd目录下的子命令的test.go文件删去,只保留根命令的root.go文件,并将root.go中cobra包改为我们自行定义的cobra包,更改后的root.go文件如下:
package cmd
import (
"fmt"
corbra "user/cobra"
"os"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &corbra.Command{
Use: "demo",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
Run: func(cmd *corbra.Command, args []string) {
fmt.Println("this is a test to run")
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
在这里我们主要进行了以下改动:
- 删除现在不必要的vipper等包,并将原来的cobra包改为我们自定义的corbra包:
"user/cobra"
; - 删除了root.go中现在不需要的init与initConfig函数;
- 将run与Execute函数中的cobra改为corbra,使用我们的corbra;
为了让根命令能够跑起来,我们的command.go文件的内容如下:
package cobra
import (
"fmt"
)
// 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
// args is a string list store all arg from command;
args []string
// Run: Typically the actual work function. Most commands will only implement this.
Run func(cmd *Command, args []string)
}
// Execute .
func (c *Command) Execute() (err error) {
fmt.Println("Execute run")
err = c.execute()
return nil
}
func (c *Command) execute()(err error){
c.Run(c, c.args)
return nil
}
- Command结构体:在corbra中的Command结构体目前只包含了cobra的Command结构体的一小部分最基础的内容,后面将视情况加入其它部分。
- Execute函数:作为对外接口的函数,这里的Execute函数只需要调用内部的execute函数即可,我们利用fmt输出这个Execute被调用的信息;
- execute函数:实际调用Command结构体中的run函数,完成用户定义的函数,在这里应当会调用rootCmd的Run函数;
我们完成以上修改之后,对demo程序重新进行编译安装,并调用根命令,输出以下结果:
可以看到,根命令被正确调用;
阶段二:为根命令增加子命令;
现在来到阶段二,在实现了根命令使用的情况下,我们希望能够给与根命令一个子命令,那么我们需要对command.go函数进行修改,得到以下内容:
package cobra
import (
"os"
"fmt"
"strings"
)
// 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
// args is a string list store all arg from command;
args []string
// Run: Typically the actual work function. Most commands will only implement this.
Run func(cmd *Command, args []string)
}
// Execute .
func (c *Command) Execute() (err error) {
fmt.Printf("Execute run %s\n", c.Name())
if c.parent == nil{
c.args = os.Args[1:]
} else {
c.args = c.parent.args[1:]
}
tcommands := stripFlags(c.args, c)
if len(tcommands) == 0{
err = c.execute()
return err
}
//fmt.Printf("something wrong with %v\n", tcommands)
for _, v := range c.commands{
if tcommands[0] == v.Name(){
err = v.Execute()
}
}
return err
}
func (c *Command) execute()(err error){
c.Run(c, c.args)
return nil
}
// 从命令行参数中读取下一个子命令并返回该子命令的名称;
func stripFlags(args []string, c *Command) []string {
if len(args) == 0 {
return args
}
commands := []string{}
for _, v := range args {
if v != "" && !strings.HasPrefix(v, "-"){
commands = append(commands, v)
break
}
}
return commands
}
// AddCommand 用于先命令c加入子命令;
// AddCommand is used to add a subcommand to command c
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)
}
}
// Name 函数用于返回命令的名称;
// Name returns the command's name: the first word in the use line.
func (c *Command) Name() string {
name := c.Use
i := strings.Index(name, " ")
if i >= 0 {
name = name[:i]
}
return name
}
- 增加AddCommand函数:使得子命令可以加入到父命令中;具体方式为,将命令加入父命令的子命令列表commands中,并将子命令的父命令指针parent指向父命令;
- 增加Name函数:使得我们可以得到命令的名称,命令的名称即为我们初始化命令时的
Use:XXX
一列为命令分配的名字; - 增加stripFlags函数:用于从命令行参数中取出下一个子命令,当没有时返回空值;
- 修改Execute函数:我们从根命令开始,利用os.Args得到命令行参数,并将os.Args[1:]赋予根命令的args成员变量,当为子命令时,就用父命令的args[1:]为子命令的args赋值。然后使用stripFlags函数获取下一条子命令,并执行子命令的Execute函数;如果没有下一条子命令说明到达命令末尾,我们调用当前命令的execute函数;
这样我们就实现了将子命令加入到父命令方法,并且实现了多级子命令,即根命令的子命令可以有自己的子命令。我们进行测试,为根命令root添加一个子命令test,并为test添加子命令testson。我们对demo程序进行重新编译安装,然后运行各个命令进行测试;
-
运行demo:
-
运行demo test:
-
运行demo test testson
可以看到,我们的命令只运行的子命令的run函数,而没有运行父命令的run函数,说明多级子命令实现成功;但是由于Execute函数的限制,我们的命令行参数格式拥有以下限制;
格式要求:
由于我们的Execute函数对命令格式的要求,我们的命令行只能接收以下形式对子命令的调用:
根命令[子命令[子命令的子命令 ...]]
即我们要调用test子命令的时候,必须使用demo test,而不是test;调用testson的时候,必须使用demo test testson,以给出使用子命令的完整路径。
而且,我们的命令不允许在父命令和子命令之间存在其他命令行参数,只允许给与最后一个子命令命令行参数,即不允许如下情况出现:
demo -h test
但是可以使用:
demo test -h
;
阶段三:为命令增加参数;
那么,就到了最重要的部分了,给命令增加参数,为了给命令增加参数,我们需要对command.go作出修改,修改后的内容如下:
package cobra
import (
...
flag "github.com/spf13/pflag"
)
// Command .
type Command struct {
...
// flags is full set of flags.
flags *flag.FlagSet
...
}
...
func (c *Command) execute()(err error){
c.ParseFlags(c.args)
c.Run(c, c.args)
return nil
}
// 从命令行参数中读取所有子命令并返回所有子命令的字符串列表;
func stripFlags(args []string, c *Command) []string {
if len(args) == 0 {
return args
}
commands := []string{}
for _, v := range args {
if v != "" && !strings.HasPrefix(v, "-"){
commands = append(commands, v)
break
} else {
break
}
}
return commands
}
...
// Flags 用于初始化命令的flags参数
// Flags returns the complete FlagSet that applies
// to this command (local and persistent declared here and by all parents).
func (c *Command) Flags() *flag.FlagSet {
if c.flags == nil {
c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
}
return c.flags
}
// ParseFlags 为命令行参数进行解析;
// ParseFlags parses persistent flag tree and local flags.
func (c *Command) ParseFlags(args []string) error {
err := c.Flags().Parse(args)
return err
}
修改的内容如下:
- 新调用了pflag包,我们使用pflag包中的FlagSet变量及其相关函数来帮助我们进行命令行参数的解析;
- command结构体增加了
flags *flag.FlagSet
用于保存各个命令行参数; - 增加了Flags函数用于初始化command结构体的flags变量,以及访问flags变量;
- 增加了ParseFlags函数,在其中使用了pflag包中的FlagSet变量的类方法
Parse([]string)
来解析命令行参数,使用的数据则来自命令的args变量; - 改动了execute函数,新增对ParseFlags的调用,使得我们在运行命令的run函数之前就已经完成了对命令行参数的解析,之后Run中的用户可以使用
cmd.Flags().GetXXX()
函数访问解析后的命令行参数; - 改动了stripFlags函数,使它在分析到第一个非命令字符串的时候便停止分析;
测试:
经过改动之后,我们就可以为我们的命令增加对应的命令行参数了,为了进行我们的测试,我们为test子命令增加了一个-v
的命令行参数,它可以接收整数值;改动后的test.go文件如下:
package cmd
import (
"fmt"
corbra "user/cobra"
//"github.com/spf13/cobra"
)
// testCmd represents the test command
var testCmd = &corbra.Command{
Use: "test",
Short: "A brief description of your command",
...
Run: func(cmd *corbra.Command, args []string) {
fmt.Println("test called")
v, _:= cmd.Flags().GetInt("version")
fmt.Printf("test called by args int %d\n", v)
},
}
func init() {
rootCmd.AddCommand(testCmd)
...
testCmd.Flags().IntP("version", "v", 1, "Help message for version")
}
我们在init中使用pflag中的FlagSet的IntP方法为test命令增加了一个命令行参数,并在test的run函数中对这个命令行参数进行了访问;结果如下:
我们使用以上三种形式对命令行参数-v
进行访问,都输出了正确结果,而在默认情况下,我们输出了-v
的默认值1;那么,我们为函数添加命令行参数成功;
阶段四:为命令增加默认的-h参数;
为了实现cobra中的一般功能,我们还必须为命令增加默认的-h参数,以及父命令的PersistFlag所定义的命令行参数可以被子命令访问的功能;为了实现这两个功能,我们对command.go进行如下更改:
package cobra
import (
"os"
"fmt"
"strings"
flag "github.com/spf13/pflag"
)
// Command .
type Command struct {
...
// helpFunc is
helpFunc func(c *Command, a []string)
...
}
// Execute .
func (c *Command) Execute() (err error) {
//fmt.Printf("Execute run %s\n", c.Name())
c.InitDefaultHelpFlag()
if c.parent == nil{
c.args = os.Args[1:]
} else {
c.args = c.parent.args[1:]
}
tcommands := stripFlags(c.args, c)
if len(tcommands) == 0{
err = c.execute()
return err
}
//fmt.Printf("something wrong with %v\n", tcommands)
for _, v := range c.commands{
if tcommands[0] == v.Name(){
err = v.Execute()
}
}
return err
}
func (c *Command) execute()(err error){
c.ParseFlags(c.args)
if v, _ := c.Flags().GetBool("help"); v == true {
c.helpFunc(c, c.args)
return nil
}
c.Run(c, c.args)
return nil
}
...
// InitDefaultHelpFlag: 为命令c添加help命令行参数;
// InitDefaultHelpFlag adds default help flag to c.
// It is called automatically by executing the c or by calling help and usage.
// If c already has help flag, it will do nothing.
func (c *Command) InitDefaultHelpFlag() {
// c.mergePersistentFlags()
if c.Flags().Lookup("help") == nil {
usage := "help for "
if c.Name() == "" {
usage += "this command"
} else {
usage += c.Name()
}
c.Flags().BoolP("help", "h", false, usage)
}
c.helpFunc = printHelp;
}
// printHelp: 用于打印帮助信息;
func printHelp(c *Command, a []string) {
fmt.Printf("%s\n\n", c.Long)
fmt.Printf("Usage:\n")
fmt.Printf("\t%s [flags]\n", c.Name())
if (len(c.commands) > 0) {
fmt.Printf("\t%s [command]\n\n", c.Name())
fmt.Printf("Available Commands:\n")
for _, v := range c.commands {
fmt.Printf("\t%-10s%s\n", v.Name(), v.Short)
}
}
fmt.Printf("\nFlags:\n")
c.Flags().VisitAll(func (flag *flag.Flag) {
fmt.Printf("\t-%1s, --%-6s %-12s%s (default \"%s\")\n", flag.Shorthand, flag.Name, flag.Value.Type(), flag.Usage, flag.DefValue)
})
fmt.Println()
if len(c.commands) > 0 {
fmt.Printf("Use \"%s [command] --help\" for more information about a command.\n", c.Name())
}
fmt.Println()
}
改动如下:
- command结构体中增加了一个helpFunc函数,用于定义帮助函数;
- Execute函数增加了在刚开始的时候增加了一个对InitDefaultHelpFlag()函数的调用,为当前命令进行添加一个
-h
命令行参数,并为当前命令指定helpFunc函数;; - execute函数中增加对
-h
命令参数的判断,如果发现-h
参数立即调用HelpFunc,打印帮助信息,并结束命令; - 增加InitDefaultHelpFlag函数,用于为当前命令添加
-h
参数,并指定helpFunc; - 增加printHelp函数,用于打印帮助信息;
输出示例:
以上就是本次作业的全部实现,本次作业所实现的command.go的接口与cobra中的command.go的接口相同,因此,我们在利用自己写的command.go替代cobra的时候,可以不用对函数进行太多的改动,只需要改动引用的库即可;
源码地址: https://gitee.com/wangyuwen2020/sever_count/tree/master/cobra