CLI命令行应用

发布时间 2023-04-20 09:43:13作者: 夏目&贵志

前言

针对golang这门高级语言,主要想了解它的语言特性还有服务器建站还有微服务搭建方面的用途,以下都可以算是使用记录。

一、命令行应用的标准库实现

很多语言都有针对命令行参数的功能包,比如python的argparse和golang的flag两个标准库,对于不少c/cpp程序员来说,最直显的就是main函数的argc和argv两个运行参数了,使用效果就是平时输入命令的各种参数:

# 删库跑路专用
rm -rf /*

如上面的rm命令,rf就是它的执行参数,负责告知rm,把根目录下所有文件、文件夹和子文件夹都给我干掉的意思并且是强行删除不问询。高级语言针对针对命令行参数已经封装好只需要调用函数传入参数即可,不像c/cpp还需要针对输入参数来写逻辑处理,同样的场景有不同的使用,相对而言高级语言拿来就用不用造轮子是为了方便实现,c/cpp很多都是自定义的造轮子所以更像原有功能不满意所以自己造一个来用,算是有深入了解的高手的语言吧。

1.1 实现一个简单带命令参数的应用

golang标准库flag的使用比较简单,如下:

func main(){
    var name string
    flag.StringVar(&name, "name", "作者", "帮助信息")
    flag.StringVar(&name, "n", "author", "帮助信息")

    var age int
    flag.IntVar(&age, "age", 18, "年龄")
    flag.IntVar(&age, "a", 80, "年龄")
    flag.Parse()

    fmt.Printf("author: %s, age: %d\n", name, age)
}

输出如下:

# golang可以直接run也可以进行编译再运行,这里编译了可执行文件再执行
jam:~$ go build .
jam:~$ ./gotour -h
Usage of ./gotour:
  -a int
        age (default 80)
  -age int
        年龄 (default 18)
  -n string
        帮助信息 (default "aaa")
  -name string
        帮助信息 (default "bbb")
jam:~$ ./gotour
author: bbb, age: 80
jam:~$ ./gotour -n jack
author: jack, age: 80
jam:~$ ./gotour --name=jam
author: jam, age: 80
jam:~$ ./gotour -age 29
author: bbb, age: 29
jam:~$ 

简单的应用就是使用flag的StringVar或者IntVar或BoolVal等api来设置具体类型的参数,都是四个参数,第一个是存储信息的变量,第二个是参数名比如上面的rf,第三个则是这个参数的默认值,如果没有输入该参数,变量就存储默认值,最后一个参数则是提示之类的信息,比如很多linux命令都有help参数,这个就是默认help参数的针对对应参数的解释提示。在上面特定参数设置好以后,进行Parse()调用,就算整体设置完毕。

另一种实现

除了上面四参数的各种Var库函数,还有下面这种:

func main() {
	flag.Parse()
	name := flag.String("name", "jam", "作者")
	age := flag.Int("a", 18, "作者年龄")

	fmt.Printf("the author: %s, age: %d\n", *name, *age)
}

输出如下:

jam:$ go build .
jam:$ ./gotour 
the author: jam, age: 18
jam:$ ./gotour -h
Usage of ./gotour:
  -a int
        作者年龄 (default 18)
  -name string
        作者 (default "jam")
jam:$ ./gotour --name Peter
the author: Peter, age: 18
jam:$ ./gotour -a 88
the author: jam, age: 88

要注意的是,针对同一个命令比如上面注明作者这个命令,是哪个调用在前,就使用哪个默认值,另外就是Parse的调用可以在参数设置完以后也可以一开始就调用。还有就是注意别设置重复的命令参数,冲突起来大概就只能谁先谁后了。

1.2 实现有子命令的应用

当一个命令应用具备越来越多的功能时,就会变得臃肿,需要几个主命令参数,其下子命令负责更细致的划分,比如用的较多的linux安装命令:

# centos
yum list installed

# ubuntu
apt list --installed

上面两个命令的执行可以得到各自系统的对应已安装包列表(不过安装方式要对应yum或者apt就是了),上面list参数就是列出列表的意思,后面installed参数则是指明列表内容,它就是对应list的命令集下的子命令了。使用flag标准库实现一下:

func main() {
	flag.Parse()
	args := flag.Args()

	if len(args) <= 0 {
		return
	}

	switch args[0] {
	case "cat":
		showCmd := flag.NewFlagSet("cat", flag.ExitOnError)
		showCmd.StringVar(&something, "s", "abaaba", "简单输出")
		showCmd.StringVar(&something, "sentence", "小aba", "输出")
		_ = showCmd.Parse(args[1:])
	case "want":
		wantCmd := flag.NewFlagSet("want", flag.ExitOnError)
		wantCmd.StringVar(&something, "book", "计算机科学", "书名")
		wantCmd.StringVar(&something, "b", "计算机网络", "书名")
		_ = wantCmd.Parse(args[1:])
	}

	fmt.Printf("the something:%s\n", something)
}

输出如下:

jam:~$ ./gotour 
jam:~$ ./gotour -h
Usage of ./gotour:
jam:~$ ./gotour cat
the something:小aba
jam:~$ ./gotour want -b 火影忍者
the something:火影忍者
jam:~$ 

二 命令行应用的cobra库实现

因为要用到外部库cobra, 所以先安装一下吧,我这里的IDE是goland和配置好了的vscode环境,但都需要在对应位置输入下面命令:

go get -u github.com/spf13/cobra

关于库的使用,都是了解个用法,重要还是例子:

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "版本号",
	Long:  "输出版本号",
	Run: func(cmd *cobra.Command, args []string) {
		version := "1.0.0"
		if len(args) > 1 {
			version = args[1]
		}
		fmt.Println("gocmd version :", version)
	},
}

func main() {
	err := versionCmd.Execute()
	if err != nil {
		log.Fatal("命令执行失败:%v", err)
	}
}

输出如下:

jam:~$  go build
jam:~$  ./cmdpra -h
输出版本号

Usage:
  version [flags]

Flags:
  -h, --help   help for version
jam:~$  ./cmdpra version 2.0.0
gocmd version : 2.0.0

对照flag标准库,cobra的使用也需要四部分信息:命令参数, 短描述, 长描述和命令处理(和flag稍微有点不同), cobra内置了Command数据结构来存储对应信息,命令的调用也就是一个Command对象调用Execute即可,如上是单个命令的实现,
要实现多个命令统一子命令就需要使用cobra的AddCommand接口了:

var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "something",
	Run: func(cmd *cobra.Command, args []string) {
		s := "nothing"
		if len(args) > 1 {
			s = args[1]
		}
		fmt.Println("cat ", s)
	},
}

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "版本号",
	Long:  "输出版本号",
	Run: func(cmd *cobra.Command, args []string) {
		version := "1.0.0"
		if len(args) > 1 {
			version = args[1]
		}
		fmt.Println("gocmd version :", version)
	},
}

func main() {
	rootCmd := &cobra.Command{}
	rootCmd.AddCommand(catCmd)
	rootCmd.AddCommand(versionCmd)
	err := rootCmd.Execute()
	if err != nil {
		log.Fatal("命令执行失败:%v", err)
	}
}

不过一般来说,项目会根据情况进行解偶,所以这些个子命令的设置一般会设定在cmd包里,再来内部调用,到时候main包就只需要进行一个Execute即可,上面输出如下:

jam:~$ ./cmdpra 
Usage:
   [command]

Available Commands:
  cat         something
  help        Help about any command
  version     版本号

Flags:
  -h, --help   help for this command

Use " [command] --help" for more information about a command.
jam:~$ ./cmdpra cat abaaba
cat  nothing

除此以外,添加子命令的方式也可以类似标准库的StringVar,如下:

var str string

var catCmd = &cobra.Command{
	Use:   "cat",
	Short: "something",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("gocmd version: %s\n", str)
	},
}

func main() {
	rootCmd := &cobra.Command{}
	rootCmd.AddCommand(catCmd)
	catCmd.Flags().StringVarP(&str, "version", "v", "v1.0.0", "版本号")
	err := rootCmd.Execute()
	if err != nil {
		log.Fatal("命令执行失败:%v", err)
	}
}

flag标准库的StringVar需要四个参数,两次调用就可以有v和version两个子命令参数的实现,cobra.StringVarP一个接口五个参数就实现,算是可以打字少一点。对应输出就是下面了:

jam:~$ ./cmdpra cat --version=v2.0.0
gocmd version: v2.0.0
jam:~$ ./cmdpra cat
gocmd version: v1.0.0
jam:~$ ./cmdpra
Usage:
   [command]

Available Commands:
  cat         something
  help        Help about any command

Flags:
  -h, --help   help for this command

Use " [command] --help" for more information about a command.
jam:~$