【服务计算】五 程序包开发,复杂命令行支持
概述
命令行实用程序并不是都象 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”)
- 使得命令行处理程序修改代价最小,即可正常运行
实验要求
- 核心任务,就是模仿 cobra 库的 command.go 重写一个 Command.go
- 仅允许使用的第三方库 flag “github.com/spf13/pflag”
- 可以参考、甚至复制原来的代码
- 必须实现简化版的 type Command struct 定义和方法
- 不一定完全兼容 github.com/spf13/cobra
- 可支持简单带子命令的命令行程序开发
- 包必须包括以下内容:
- 生成的中文 api 文档
- 有较好的 Readme 文件,包括一个简单的使用案例
- 每个go文件必须有对应的测试文件
- 在 Gitee 或 Github 提交程序,并在 specification.md 文件中描述设计说明,单元或集成测试结果,功能测试结果。
操作系统
按照实验要求,结合自己的电脑设备与系统等条件,使用VirtualBox下Ubuntu 20.04系统完成实验。虚拟机相关设置与上一次实验相同。
环境准备
虚拟机下的实验环境与上一次实验的相同,不需要额外的配置。
开发实践
创建项目
根据go的工作空间目录结构,由于现在考虑开发一个自己的读取配置文件包,因此下文的工作目录默认在"$GOPATH/src/gitee.com/alphabstc/"下(alphabstc为我的gitee和github的用户id)。
对Cobra包的了解和使用
Cobra 是一个 Golang 包,其提供简单的接口、方便用户开发具有复杂功能的命令行程序的库。同时,Cobra 也是一个应用程序,其可以用于生成应用框架,从而开发以 Cobra 为基础的应用。
这一节的工作目录在"$GOPATH/src/gitee.com/alphabstc/myCMDApp"下。
下载Cobra包到本地
首先,在Bash下输入以下的命令
go get -u github.com/spf13/cobra
之后,可以看到如下的结果:

说明没有反馈错误和异常,下载Cobra包成功。
使用go get github.com/spf13/cobra/cobra可以编译cobra命令,使得在$GOPATH/bin目录下创建cobra可执行文件。

注意,上面的命令可能会去官网下载一些依赖的包,会导致访问超时。为此,需要在Bash下设置如下的环境变量:
export GOPROXY=https://goproxy.io
export GO111MODULE=on
上面的截图出现了错误信息,其说需要get的包有多个引用但版本不一致。为此,还需要具体指定版本信息:

这样就可以顺利下载和编译Cobra。
使用Cobra包生成一个简单的命令行程序
在自己编写的包中使用下面的导入语句,就可以使用Cobra包。
import "github.com/spf13/cobra"
通常,基于Cobra的应用有着如下的文件组织结构:

在Cobra应用程序中,通常main.go文件非常裸露。它有一个目的:初始化Cobra。
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
使用cobra init [app]命令可以初始化一个Cobra项目。使用该命令,可以创建一个以正确的结构填充程序的初始项目,并可以立即享受Cobra的所有好处。
如下图所示:

这样就初始化好了一个基于Cobra的命令行程序项目。
可以看到,其初始化好了如下框架的一个项目:

其中,main.go只有如下简单的语句。main.go的功能就是初始化项目,执行根命令。
package main
import "gitee.com/alphabstc/myCMDApp/cmd"
func main() {
cmd.Execute()
}
而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: "myCMDApp",
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)
}
}
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/.myCMDApp.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")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".myCMDApp" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".myCMDApp")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
这个包现在可以被编译运行了,这个包目前是利用cobra init创建出的一个简单命令行程序,目前其只有一个根命令root。
编译安装命令行程序myCMDApp:

之后运行,可以看到如下的结果:

其输出了描述信息。这是因为还没有给命令定义具体的动作。
修改cmd/root.go中rootCmd的代码如下:
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "myCMDApp",
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.Printf("This is my cmd app.\n")
},
}
再重新编译安装和运行该命令行程序:

可以看到其完成了上面root.go定义的输出"This is my cmd app."信息的动作。
给命令行程序添加子命令
查询当前操作系统
在cmd文件夹下中新建一个OS.go文件,定义子命令OSCmd,功能为输出当前的操作系统
OS.go代码如下:
package cmd
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
var (
OSCmd = &cobra.Command{
Use: "OS",
Short: "get current OS name",
Long: "This is a subcommand, return current OS name",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(runtime.GOOS)
},
}
)
之后修改root.go中的init函数,将OSCmd命令添加为rootCmd的子命令:
rootCmd.AddCommand(OSCmd)
重新编译安装与运行:

可以看到,其目前拥有了OS子命令,可以返回当前的操作系统名称。
查询当前日期
在cmd文件夹下中新建一个date.go文件,定义子命令dateCmd,功能为输出当前的日期
date.go代码如下:
package cmd
import (
"fmt"
"time"
"github.com/spf13/cobra"
)
var (
dateCmd = &cobra.Command{
Use: "date",
Short: "get current date",
Long: "This is a subcommand, return current date",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(time.Now().Date())
},
}
)
之后修改root.go中的init函数,将dateCmd命令添加为rootCmd的子命令:
rootCmd.AddCommand(dateCmd)
重新编译安装与运行:

给命令行添加选项信息
根据官网的文档,在root.go的init函数中添加如下的选项信息:
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
同时,修改rootCmd的run函数内容如下:
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("This is my cmd app.\n")
if (userLicense != "") {
fmt.Printf("Your license is %s.\n", userLicense)
}
},
重新编译安装与运行:

可以看到,成功成-l中解析了选项内容,并在run函数中将该内容输出。
查看命令行程序帮助信息
现在可以查看当前已经有多个子命令和选项的命令行程序myCMDApp的帮助信息:

可以看到,其很方便地自动根据之前添加的子命令和选项,生成了帮助信息。
模仿cobra.Command编写一个myCobra库
这一节的工作目录在"$GOPATH/src/gitee.com/alphabstc/myCobra"下。
先写测试
command.go的编写
在上述工作目录中新建一个文件command.go。对于该文件,先考虑支持不带选项的多级命令。根据实验要求,需要使得上面的myCMDApp改动最小,因此command.go对外提供了与Cobra一样的接口函数。不过根据实验要求,对数据结果Command进行了一定的简化。其主要实现了Command结构体,以及AddCommand,Execute等函数。具体代码分析详见注释:
package myCobra
import (
"fmt"
"os"
"strings"
flag "github.com/spf13/pflag"
)
type Command struct {
Use string//用法信息
Short string//短描述信息
Long string //长描述信息
Run func(cmd *Command, args []string)//命令执行时的函数
commands []*Command//子命令
parent *Command//父命令
args []string//参数数组
pflags *flag.FlagSet//选项
choose *Command //被选择执行的命令
}
func (c *Command) AddCommand(son *Command) {
for _, it := range c.commands {
if it == son {//已经存在 不添加
return
}
}
if c == son {
return//子命令不能是自己
}
c.commands = append(c.commands, son)//添加子命令
son.parent = c//设置父命令
}
func (c *Command) Execute() error {//执行
if c == nil {//不能执行空命令
return fmt.Errorf("Called Execute() on a nil Command")
}
if c.parent == nil { // 没有父命令
ParseArgs(c, os.Args[1:])//是根命令 后面的Args要被解析
}
c.execute()//执行
return nil
}
func (c *Command) execute() {
if c.choose == nil {//没有找到合适的命令
for _, it := range c.args {
if it == "--help" || it == "-h" {//为帮助信息
c.Print_help()
return
}
}
c.Run(c, c.args)//执行Run函数
return
}
c.choose.execute()//否则执行选择的子命令
}
//检索并保存每个命令的所有参数
func ParseArgs(c *Command, args []string) {
if len(args) < 1 {
return
}
for _, it := range c.commands {
if it.Use == args[0] { //有一个子命令匹配
c.args = args[:1]//获得参数信息
c.choose = it//设置为选择的子命令
ParseArgs(it, args[1:])//继续解析参数
return
}
}
c.args = args //没有子命令 所有参数都转到当前命令
c.PersistentFlags().Parse(c.args)//处理args参数
}
func (c *Command) PersistentFlags() *flag.FlagSet {
if c.pflags == nil {
c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
}
return c.pflags
}
// Name returns the command's name: the first word in the use line. 也就是c.Use第一个空格前的部分
func (c *Command) Name() string {
name := c.Use
i := strings.Index(name, " ")
if i >= 0 {
name = name[:i]
}
return name
}
//输出帮助信息
func (c *Command) Print_help() {
fmt.Printf("%s\n\n", c.Long)//输出长描述
fmt.Printf("Usage:\n\t%s [flags]\n", c.Name())//输出名称 和 [flags]
if (len(c.commands) > 0) {
fmt.Printf("\t%s [command]\n\n", c.Name())//输出名称 和 [command]
fmt.Printf("Aitailable Commands:\n")//可用命令列表
for _, it := range c.commands {
fmt.Printf("\t%-12s%s\n", it.Name(), it.Short)//命令名称和短描述
}
}
fmt.Printf("\nFlags:\n")//输出选项信息
c.PersistentFlags().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.Printf("\t-%1s, --%-21s%s%s\n\n", "h", "help", "help for ", c.Name())//help选项不存在于选项集中,但也可以使用该选项
if len(c.commands) > 0 {//提示可以获得更多更具体的帮助信息
fmt.Printf("Use \"%s [command] --help\" for more information about a command.\n", c.Name())
}
fmt.Println()
}
上面的实现仅使用了第三方库pflag,而且实现了简化版的type Command struct 定义和方法,可支持简单带子命令的命令行程序开发。
单元测试
参考之前Cobra包的单元测试,为command.go编写单元测试代码如下。其测试了执行子命令和执行没有子命令的命令功能是否正常:
package myCobra
import (
"bytes"
"strings"
"testing"
)
func emptyRun(*Command, []string) {}
func executeCommand(root *Command, args ...string) (output string, err error) {
_, output, err = executeCommandC(root, args...)
return output, err
}
func executeCommandC(root *Command, args ...string) (c *Command, output string, err error) {
buf := new(bytes.Buffer)
err = root.Execute()
return c, buf.String(), err
}
func TestChildCommand(t *testing.T) {
var child1CmdArgs []string
rootCmd := &Command{Use: "root", Run: emptyRun}
child1Cmd := &Command{
Use: "child1",
Run: func(_ *Command, args []string) { child1CmdArgs = args },
}
child2Cmd := &Command{Use: "child2", Run: emptyRun}
rootCmd.AddCommand(child1Cmd)
rootCmd.AddCommand(child2Cmd)
output, err := executeCommand(rootCmd, "child1", "one", "two")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
got := strings.Join(child1CmdArgs, " ")
expected := ""
if got != expected {
t.Errorf("child1CmdArgs expected: %q, got: %q", expected, got)
}
}
func TestCallCommandWithoutSubcommands(t *testing.T) {
rootCmd := &Command{Use: "root", Run: emptyRun}
_, err := executeCommand(rootCmd)
if err != nil {
t.Errorf("Calling command without subcommands should not have error: %v", err)
}
}
单元测试结果:

说明通过了单元测试。
功能测试
下面通过使用myCobra包,来进行功能测试。为了避免对之前包myCMDApp的修改,将之前myCMDApp包的代码文件复制了一份到包myCMDApp2包中。并将导入的包从第三方库Cobra修改为gitee.com/alphabstc/myCobra。
进行编译和运行:

测试子命令:

测试选项:

可以看到使用子命令和选项功能都正常。
再测试帮助功能:
](https://img-blog.csdnimg.cn/20201027145642269.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NUY3ljbG9uZQ==,size_16,color_FFFFFF,t_70#pic_center)
可见正确输出了帮助信息。

测试不存在的子命令

可以看到提示了命令不存在的信息。
查看子命令帮助信息:

看到可以正确显示子命令的帮助信息。
以上功能测试说明实现的myCobra包功能基本正确。
生成API文档
先使用命令go get golang.org/x/tools/cmd/godoc来安装godoc。该命令会访问官网下载godoc,有可能访问超时。为此,需要在Bash下设置如下的环境变量:
export GOPROXY=https://goproxy.io
export GO111MODULE=on
这样就可以顺利安装godoc:

然后在bash下运行命令go build golang.org/x/tools/cmd/godoc

再运行godoc就可以在浏览器通过http://localhost:6060/来访问godoc了。

之后,还可以将文档导出出来:
godoc -url "http://localhost:6060/pkg/gitee.com/alphabstc/myCobra" > api.html

项目链接
https://gitee.com/alphabstc/service-computing-my-cobra
593

被折叠的 条评论
为什么被折叠?



