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()
}