golang自身带的命令行包flag,各种case,有代码洁癖的人看着就令人头大,我们一般使用其他的命令行解析包比如pflag,cobra等,cobra是个非常不错的命令行包(golang命令行解析库),docker,hugo都在使用.
cobra
基命令
首先创建一个基命令
package cmd
import (
"github.com/spf13/cobra"
)
var RootCmd = &cobra.Command{
Use: "gonne",
Run: func(cmd *cobra.Command, args []string) {
println("gonne is my ai friend")
},
}
使用命令
在main方法中调用命令,恩,就这么简单
package main
import (
"fmt"
"os"
"lastsweetop.com/cmd"
)
func main() {
if err := cmd.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
在命令行输入 gonne,就会执行基命令中Run方法
appletekiMacBook-Pro:src apple$ gonne
gonne is my ai friend
子命令
在基命令上增加子命令也相当简单,根本无需在基命令和main方法中写任何代码,只需新建一个go文件,多个子命令间也是相互独立的,多么优雅的代码,告别各种case
package cmd
import "github.com/spf13/cobra"
func init() {
RootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Gonne",
Run: func(cmd *cobra.Command, args []string) {
println("gonne version is 0.0.1")
},
}
效果如下:
appletekiMacBook-Pro:src apple$ gonne version
gonne version is 0.0.1
启动命令
我们先来个非后台运行的启动命令
func init() {
startCmd := &cobra.Command{
Use: "start",
Short: "Start Gonne",
Run: func(cmd *cobra.Command, args []string) {
startHttp()
},
}
startCmd.Flags().BoolVarP(&daemon, "deamon", "d", false, "is daemon?")
RootCmd.AddCommand(startCmd)
}
startHttp方法启动一个http的web服务
func startHttp() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello cmd!")
})
if err := http.ListenAndServe(":9090", nil); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
现在通过gonne start便可以启动一个web服务了,但是程序停留在命令行,如果ctrl+C程序也会终止了
命令行参数
如果想要后台启动,那么得让start命令知道是要后台运行的,参照docker命令行的方式就是加上-d,给一个命令添加参数的判断只需很少的代码
改造一下代码
func init() {
var daemon bool
startCmd := &cobra.Command{
Use: "start",
Short: "Start Gonne",
Run: func(cmd *cobra.Command, args []string) {
if daemon {
fmt.Println("gonne start",daemon)
}
startHttp()
},
}
startCmd.Flags().BoolVarP(&daemon, "deamon", "d", false, "is daemon?")
RootCmd.AddCommand(startCmd)
}
命令行输入
gonne start -d
1
这样就可以接收到-d参数了,这里要说明一下,第一个参数取值,第二个参数代码–deamon,第三个参数代表-d ,第四个参数代码不加-d时候的默认值,第五参数是描述
后台运行
后台运行其实这里使用的是一个巧妙的方法,就是使用系统的command命令行启动自己的命令行输入,是不是有点绕,再看看看改造后的代码
Run: func(cmd *cobra.Command, args []string) {
if daemon {
command := exec.Command("gonne", "start")
command.Start()
fmt.Printf("gonne start, [PID] %d running...\n", command.Process.Pid)
ioutil.WriteFile("gonne.lock", []byte(fmt.Sprintf("%d", command.Process.Pid)), 0666)
daemon = false
os.Exit(0)
} else {
fmt.Println("gonne start")
}
startHttp()
},
用exec的Command启动刚输入的gonne start -d,就会拦截到这条请求然后通过gonne start,但是程序就不会停留在命令行了,然后发现http服务还在,还可以访问。
还有一点就是把pid输出到gonne.lock文件,给停止的程序调用
终止后台程序
有了之前的操作后,停止就简单多了
func init() {
RootCmd.AddCommand(stopCmd)
}
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop Gonne",
Run: func(cmd *cobra.Command, args []string) {
strb, _ := ioutil.ReadFile("gonne.lock")
command := exec.Command("kill", string(strb))
command.Start()
println("gonne stop")
},
}
执行 gonne stop 即可终止之前启动的http服务
help命令
好了,关于命令的操作讲完了,再看看cobra给的福利,自动生成的help命令
这个不需要你做什么操作,只需要输入gonne help,相关信息已经帮你生产好了。
appletekiMacBook-Pro:andev apple$ gonne help
Usage:
gonne [flags]
gonne [command]
Available Commands:
help Help about any command
start Start Gonne
stop Stop Gonne
version Print the version number of Gonne
Flags:
-h, --help help for gonne
Use "gonne [command] --help" for more information about a command.
当然,子命令也有
appletekiMacBook-Pro:andev apple$ gonne start -h
Start Gonne
Usage:
gonne start [flags]
Flags:
-d, --deamon is daemon?
-h, --help help for start
自此告别各种脚本
flag
golang自带的一个解析命令行参数的方法或库,是经常用的。
结构体和默认实例
首先
type FlagSet struct {
// Usage is the function called when an error occurs while parsing flags.
// The field is a function (not a method) that may be changed to point to
// a custom error handler. What happens after Usage is called depends
// on the ErrorHandling setting; for the command line, this defaults
// to ExitOnError, which exits the program after calling Usage.
Usage func()
name string
parsed bool
actual map[string]*Flag
formal map[string]*Flag
args []string // arguments after flags
errorHandling ErrorHandling
output io.Writer // nil means stderr; use out() accessor
}
一个flag的集合
type Flag struct {
Name string // flag在命令行中的名字
Usage string // 帮助信息
Value Value // 要设置的值
DefValue string // 默认值(文本格式),用于使用信息
}
flag库中使用了CommandLine对flagset进行了初始化,默认传入参数是启动文件名
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
返回一个flagset的指针。
定义
定义 flags 有三种方式
1)flag.Xxx(),其中 Xxx 可以是 Int、String 等;返回一个相应类型的指针,如:
var ip = flag.Int("flagname", 1234, "help message for flagname")
这种模式其实是对第二种模式的一种封装,是一种最顶层的使用,其实是使用默认的CommandLine实例调用下面这种方式声明的XxxVar()函数。对应的第一个变量直接new一个,其实这个是用于存储默认值的。
2)flag.XxxVar(),将 flag 绑定到一个变量上,如:
var flagvar int
flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
这种模式其实是对自定义的一种封装,其实就是调用了var()函数。
3)还可以创建自定义 flag,只要实现 flag.Value 接口即可(要求 receiver 是指针),这时候可以通过如下方式定义该 flag:
flag.Var(&flagVal, "name", "help message for flagname")
这边才是真正的实现,其实就是将启动参数按着flag的结构体存储到flagset种的formal这个map中去,最后给parse去解析。第一个参数是一个value的接口
type Value interface {
String() string
Set(string) error
}
用于接收定义类型的结构体对象指针,最终找到对应方法的实现。
自定义这个可以举个例子加强理解
type sliceValue []string
func newSliceValue(vals []string, p *[]string) *sliceValue {
*p = vals
return (*sliceValue)(p)
}
func (s *sliceValue) Set(val string) error {
*s = sliceValue(strings.Split(val, ","))
return nil
}
func (s *sliceValue) Get() interface{} { return []string(*s) }
func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }
之后可以这么使用:
var languages []string
flag.Var(newSliceValue([]string{}, &languages), "slice", "I like programming `languages`")
这样通过 -slice “go,php” 这样的形式传递参数,languages 得到的就是 [go, php]。
flag 中对 Duration 这种非基本类型的支持,使用的就是类似这样的方式。
解析
在所有的 flag 定义完成之后,可以通过调用 flag.Parse() 进行解析。
命令行 flag 的语法有如下三种形式:
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型
实例
package main
import (
"flag"
"fmt"
)
var inputName = flag.String("name", "CHENJIAN", "Input Your Name.")
var inputAge = flag.Int("age", 27, "Input Your Age")
var inputGender = flag.String("gender", "female", "Input Your Gender")
var inputFlagvar int
func Init() {
flag.IntVar(&inputFlagvar, "flagname", 1234, "Help")
}
func main() {
Init()
flag.Parse()
// func Args() []string
// Args returns the non-flag command-line arguments.
// func NArg() int
// NArg is the number of arguments remaining after flags have been processed.
fmt.Printf("args=%s, num=%d\n", flag.Args(), flag.NArg())
for i := 0; i != flag.NArg(); i++ {
fmt.Printf("arg[%d]=%s\n", i, flag.Arg(i))
}
fmt.Println("name=", *inputName)
fmt.Println("age=", *inputAge)
fmt.Println("gender=", *inputGender)
fmt.Println("flagname=", inputFlagvar)
}
操作:
go build example_flag.go
./example_flag -h
<<'COMMENT'
Usage of ./exampleFlag:
-age int
Input Your Age (default 27)
-flagname int
Help (default 1234)
-gender string
Input Your Gender (default "female")
-name string
Input Your Name. (default "CHENJIAN")
COMMENT
./example_flag chenjian
<<'COMMENT'
args=[chenjian], num=1
arg[0]=chenjian
name= CHENJIAN
age= 27
gender= female
flagname= 1234
COMMENT
./example_flag -name balbalba -age 1111 -flagname=12333 dfdf xccccc eette
<<'COMMENT'
args=[dfdf xccccc eette], num=3
arg[0]=dfdf
arg[1]=xccccc
arg[2]=eette
name= balbalba
age= 1111
gender= female
flagname= 12333
COMMENT
kingpin
功能比flag库强大,用法差不多,相比flag库,最重要的一点就是支持不加”-“的调用。
下面实例就说明了大部分的用法
package main
import (
"os"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
app = kingpin.New("chat", "A command-line chat application.")
//bool类型参数,可以通过 --debug使该值为true
debug = app.Flag("debug", "Enable debug mode.").Bool()
//识别 ./cli register
register = app.Command("register", "Register a new user.")
// ./cli register之后的参数,可通过./cli register gggle 123456 传入name为gggle pwd为123456 参数类型为字符串
registerName = register.Arg("name", "Name for user.").Required().String()
registerPwd = register.Arg("pwd", "pwd of user.").Required().String()
//识别 ./cli post
post = app.Command("post", "Post a message to a channel.")
//可以通过 ./cli post --image file 或者 ./cli post -i file 传入文件
postImage = post.Flag("image", "Image to post.").Short('i').String()
//可以通过./cli post txt 传入字符串,有默认值"hello world"
postText = post.Arg("text", "Text to post.").Default("hello world").Strings()
)
func main() {
//从os接收参数传给kingpin处理
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
case register.FullCommand():
println("name:" + *registerName)
println("pwd:" + *registerPwd)
case post.FullCommand():
println((*postImage))
text := strings.Join(*postText, " ")
println("Post:", text)
}
if *debug == true {
println("debug")
}
}
Pflag
Docker源码中使用了Pflag,安装spf13/pflag
go get github.com/spf13/pflag
使用
基本的使用和“flag包”基本相同
新增:
添加shorthand参数
// func IntP(name, shorthand string, value int, usage string) *int
// IntP is like Int, but accepts a shorthand letter that can be used after a single dash.
var ip= flag.IntP("flagname", "f", 1234, "help message")
设置非必须选项的默认值
var ip = flag.IntP("flagname", "f", 1234, "help message")
flag.Lookup("flagname").NoOptDefVal = "4321"
结果如下图:
Parsed Arguments Resulting Value
–flagname=1357 ip=1357
–flagname ip=4321
[nothing] ip=1234
命令行语法
--flag // 布尔flags, 或者非必须选项默认值
--flag x // 只对于没有默认值的flags
--flag=x
flag定制化
例如希望使用“-”,“_”或者“.”,像–my-flag == –my_flag == –my.flag:
func wordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
from := []string{"-", "_"}
to := "."
for _, sep := range from {
name = strings.Replace(name, sep, to, -1)
}
return pflag.NormalizedName(name)
}
myFlagSet.SetNormalizeFunc(wordSepNormalizeFunc)
例如希望联合两个参数,像–old-flag-name == –new-flag-name:
func aliasNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
switch name {
case "old-flag-name":
name = "new-flag-name"
break
}
return pflag.NormalizedName(name)
}
myFlagSet.SetNormalizeFunc(aliasNormalizeFunc)
弃用flag或者它的shothand
例如希望弃用名叫badflag参数,并告知开发者使用代替参数:
// deprecate a flag by specifying its name and a usage message
flags.MarkDeprecated("badflag", "please use --good-flag instead")
从而当使用badflag时,会提示Flag –badflag has been deprecated, please use –good-flag instead
例如希望保持使用noshorthandflag,但想弃用简称n:
// deprecate a flag shorthand by specifying its flag name and a usage message
flags.MarkShorthandDeprecated("noshorthandflag", "please use --noshorthandflag only")
从而当使用n时,会提示Flag shorthand -n has been deprecated, please use –noshorthandflag only
隐藏flag
例如希望保持使用secretFlag参数,但在help文档中隐藏这个参数的说明:
// hide a flag by specifying its name
flags.MarkHidden("secretFlag")
关闭flags的排序
例如希望关闭对help文档或使用说明的flag排序:
flags.BoolP("verbose", "v", false, "verbose output")
flags.String("coolflag", "yeaah", "it's really cool flag")
flags.Int("usefulflag", 777, "sometimes it's very useful")
flags.SortFlags = false
flags.PrintDefaults()
输出:
-v, --verbose verbose output
--coolflag string it's really cool flag (default "yeaah")
--usefulflag int sometimes it's very useful (default 777)
同时使用flag包和pflag包
import (
goflag "flag"
flag "github.com/spf13/pflag"
)
var ip *int = flag.Int("flagname", 1234, "help message for flagname")
func main() {
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
flag.Parse()
}