聊一聊Go編寫的命令行工具類

命令行工具

原生flag包

Go原生在flag包提供了一個命令行工具類,它可以讓我們執行類似命令行的賦參操作,經常被運用於工具類,特別是數據處理過程,可以方便我們進行參數可視化註解。

flag包提供了多個常用類型的賦值方法,如String, Int, Bool, Float64, Duration等。

  • 通過flag.XXXType()函數可以對參數名稱,默認值,描述進行定義
  • flag.parse()用於指定程序開始解析傳入的命令行變量
  • 參數-h可以調用flag內置的usage()方法, 把參數描述打印出來

下面列舉一個簡單的例子。

用法示例:

func main() {

	//聲明接收變量, 注意返回的是個指針類型
	wordParam := flag.String("word", "defaultValue", "Desc param 1 for something.")
	numParam := flag.Int("number", 0, "Desc param 2 for integer number.")

	//an existing var declared elsewhere in the program
	ok := AllRight()

	flag.BoolVar(&ok, "ok", false, "Desc value for boolean.")

	flag.Parse()

	log.Printf("Flag for wordParam: %s", *wordParam)
	log.Printf("Flag for wordParam: %d", *numParam)
	log.Printf("Flag for inner bool value : %v", ok)
}

func AllRight() bool {
	return true
}

我們通過command line 傳參給可執行程序,輸出:

$ ./tool.exe -word=hello -number=6
2020/03/18 10:59:49 Flag for wordParam: hello
2020/03/18 10:59:49 Flag for wordParam: 6
2020/03/18 10:59:49 Flag for inner bool value : false

如果使用-h/--help命令可以調出參數提示,輸出:

$ ./tool.exe -h
Usage of E:\Code\GoWork\src\HelloGo\basic\flag\tool.exe:
  -number int
        Desc param 2 for integer number.
  -ok
        Desc value for boolean.
  -word string
        Desc param 1 for something. (default "defaultValue")

flag分級用法

在實際應用環境,調用目標可能有多個,有時我們需要多個命令,多個參數聯合起來,用於調用不同的方法,類似於參數調用子命令的參數,
如:./cmd foo -a="a" -b=1或者./cmd bar -c="c" -d=2

Subcommands

原生flag包提供了一個子命令構造方式,NewFlagSet用於返回子命令的flag,示例如下:

我們定義一個解析子命令方法:

func SubCommands() {
	//param1: 命令參數, param2: 錯誤退出碼
	fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
	//聲明子命令fc, fc2
	fc := fooCmd.String("fc", "default of fc", "foo sub value1")
	fc2 := fooCmd.String("fc2", "default of fc2", "foo sub value2")

	barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
	//聲明子命令bc
	bc := barCmd.String("bc", "default of bc", "bar sub b")

	if len(os.Args) < 2 {
		fmt.Println("expected 'foo' or 'bar' subcommands")
		fooCmd.Usage()
		barCmd.Usage()
		os.Exit(1)
	}

	//參數邏輯調用, 使用分支語句
	switch os.Args[1] {
	case "foo":
		fooCmd.Parse(os.Args[2:])
		log.Printf("fc of foo: %s", *fc)
		log.Printf("fc2 of foo: %s", *fc2)
	case "bar":
		barCmd.Parse(os.Args[2:])
		log.Printf("bc of bar: %s", *bc)
	default:
		fmt.Println("expected 'foo' or 'bar' subcommands")
		os.Exit(1)
	}

}

執行命令行提示:

$ ./tool.exe
expected 'foo' or 'bar' subcommands
Usage of foo:
  -fc string
        foo sub value1 (default "default of fc")
  -fc2 string
        foo sub value2 (default "default of fc2")
Usage of bar:
  -bc string
        bar sub b (default "default of bc")

命令調用示例:

$ ./tool.exe foo -fc="fc value" -fc2="fc22222"
2020/03/18 12:54:16 fc of foo: fc value
2020/03/18 12:54:16 fc2 of foo: fc22222

問題延申:

上述方式藉助Go的內置flag實現了命令傳參,但是有個問題,如上面提到,“在實際應用環境,調用目標可能有多個,有時我們需要多個命令”,對每個調用鏈都起一個switch分支去寫可能十分喫力,我們自己能不能對調用鏈執行封裝,嘗試實現類似於flag的功能呢?


自定義分支調用

最近在預覽前輩的代碼塊中,發現一個巧妙的實現方式,在Go中,函數是可以作爲參數傳入的,我們可以利用這一特性建立一個方法鏈數組,利用全局函數變量根據入參進行調用,類似於面嚮對象語言裏面的多態,本質上是相同類型的不同表現。

程序實例:

  • 先定義一個cmd結構體,封裝了指令/執行方法/提示
/*
	自定義方法調用
 */
//封裝命令結構, 指令/執行方法/提示
type cmd struct {
	Name    string
	Process func(args ...string)
	Usage   func() string
}

//全局指令數組
var all []*cmd
  • 根據cmd的規範建立多個實現類,如下面的例子,有wind, sun兩個實現方式

wind實現:

//可變參數作爲可選入參
func Process(args ...string)  {
	if len(args) < 2 {
		logrus.Error(Usage())
		return
	}

	//Do something
	logrus.Infof("Wind fly from %s, level %s.", args[0], args[1])
}

//參數提示
func Usage() string {
	return "Hint: wind <N/S/W/E> <level>"
}

sun實現:

func Process(args ...string)  {
	if len(args) < 2 {
		logrus.Error(Usage())
		return
	}

	//Do something
	logrus.Infof("Sun %s about %s miles.", args[0], args[1])

}

//參數提示
func Usage() string {
	return "Hint: sun <rise/down> <range>"
}

至此, 不同指令的表現就實現完了,有點類似於面嚮對象語言裏面的多態,本質上是相同類型的不同表現。
後面我們來看下在main方法怎麼調用

func init()  {
	all = append(all, &cmd{"sun", sun.Process, sun.Usage})
	all = append(all, &cmd{"wind", wind.Process, wind.Usage})
}

//全局用法
func usage() string {
	sb := new(strings.Builder)
	sb.WriteString(fmt.Sprintf("Usage: %s <command> [args...]\n", os.Args[0]))
	for _, c := range all {
		sb.WriteString("\n命令: ")
		sb.WriteString(c.Name)
		sb.WriteString(", 用法: ")
		sb.WriteString(c.Usage())
		sb.WriteString("\n")
	}
	return sb.String()
}

func main()  {
	if len(os.Args) > 1 && os.Args[1] != "--help" && os.Args[1] != "-h"{
		for _, c := range all {
			//匹配全局命令
			if c.Name == os.Args[1] {
				c.Process(os.Args[2:]...)
				return
			}
		}
		fmt.Fprintf(os.Stderr, "No match func for %s.\n", os.Args[1])
	}
	fmt.Fprintln(os.Stderr, usage())
}

我們利用入參的長度以及內置命令區分調用鏈,對符合格式的參數與全局指令切片進行匹配。
下面是 輸出示例:

$ ./tool.exe -h
Usage: E:\Code\GoWork\src\HelloGo\basic\flag\tool.exe <command> [args...]

命令: sun, 用法: Hint: sun <rise/down> <range>

命令: wind, 用法: Hint: wind <N/S/W/E> <level>

$ ./tool.exe a b
No match func for a.
Usage: E:\Code\GoWork\src\HelloGo\basic\flag\tool.exe <command> [args...]

命令: sun, 用法: Hint: sun <rise/down> <range>

命令: wind, 用法: Hint: wind <N/S/W/E> <level>

可以看到當參數不合法時會遍歷打印各個子命令的用法給予提示。

$ ./tool.exe sun
time="2020-03-18T15:23:23+08:00" level=error msg="Hint: sun <rise/down> <range>"

常規調用:

$ ./tool.exe wind N 7
time="2020-03-18T15:22:40+08:00" level=info msg="Wind fly from N, level 7."
$ ./tool.exe sun rise 30
time="2020-03-18T15:24:28+08:00" level=info msg="Sun rise about 30 miles."

小結

以上是go命令行工具的簡單用法,內置的flag幫我們封裝好了一下基礎函數,我們也可以利用字符串處理自定義實現工具類,兼容多種場景,簡單的邏輯判斷也能達到flag的那種用法。

參考鏈接:

Git project
https://github.com/pixeldin/HelloGo/tree/master/basic/flag
Go by Example: Command-Line Subcommands
https://gobyexample.com/command-line-subcommands
Command line flag syntax
https://golang.org/pkg/flag/#hdr-Command_line_flag_syntax

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章