Golang開發命令列工具之flag包的使用

語言: CN / TW / HK
    • 3、flag包命令列引數的定義
    • 4、flag包命令列引數解析
    • 6、flag定義短引數和長引數

1、命令列工具概述

日常命令列操作,相對應的眾多命令列工具是提高生產力的必備工具,我在之前的文章 我的生產力工具推薦-終端01篇 中有推薦過一些我常用的基於 terminal 終端的命令列 cli 工具

滑鼠能夠讓使用者更容易上手,降低使用者學習成本。 而對於開發者,鍵盤操作模式能顯著提升生產力,還有在一些專業工具中, 大量使用快捷鍵代替繁瑣的滑鼠操作,能夠使開發人員更加專注於工作,提高效率,因為鍵盤操作模式更容易產生肌肉記憶

舉個栗子。我司業務研發,前些年在我們的強力推動下(被迫)轉向使用了 git 作為版本控制,開始使用的是圖形化“小烏龜”工具。後續出現幾次問題解決起來較麻煩後,推薦其使用原生的 git 命令列。如今,使用 git 命令列操作版本控制可謂 “一頓操作猛如虎......”

命令列(鍵盤)操作在很大程度上可以提高工作效率,與之相對應的是滑鼠(觸屏等)操作,這兩種模式是目前的主流人機互動方式

設計一款命令列工具的開發語言可以選擇原始的 shell 、甚至是更原始的語言 C ,更為容易上手且功能更多的有 nodepythongolang

本文是基於 golang 開發命令列工具的開篇,主要是基於 golang 原生內建的、輕量的 flag 包實現,用 golang 設計命令列工具而不用 shellpython 的原因這裡就不做論述了

2、flag包介紹

flag 包用來解析命令列引數

相比簡單的使用 os.Args 來獲取命令列引數, flag 可以實現按照更為通用的命令列用法,例如 mysql -u root -p 123456 。其中 mysql 是命令列的名稱即這個命令, -u-p 分別是這個命令的兩個引數:使用者名稱和密碼,後面接著的是對應的引數值,有了引數的宣告之後,兩個引數可以互換位置,引數值也可以選填或按照預設(預設)值進行指定

flag 的詳細用法可參考 flag包文件

flag 包支援的命令列引數的型別有 boolintint64uintuint64float float64stringduration

即布林值、整型、浮點型、字串、時間段型別

3、flag包命令列引數的定義

定義 flag 命令列引數,用來接收命令列輸入的引數值,一般有以下兩種方法

  • flag.TypeVar():先定義引數(實際上是指標),再定義 flag.TypeVar 將命令列引數儲存(繫結)到前面引數的值的指標(地址)
var name string
var age int
var height float64
var graduated bool
// &name 就是接收使用者命令列中輸入的-n後面的引數值
// 返回值是一個用來儲存name引數的值的指標/地址
// 定義string型別命令列引數name,括號中依次是變數名、flag引數名、預設值、引數說明
flag.StringVar(&name, "n", "", "name引數,預設為空")
// 定義整型命令列引數age
flag.IntVar(&age,"a", 0, "age引數,預設為0")
// 定義浮點型命令列引數height
flag.Float64Var(&height,"h", 0, "height引數,預設為0")
// 定義布林型命令列引數graduated
flag.BoolVar(&graduated,"g", false, "graduated引數,預設為false")
  • flag.Type():用短變數宣告的方式定義引數型別及變數名
// 定義string型別命令列引數name,括號中依次是flag引數名、預設值、引數說明
namePtr := flag.String("n", "", "name引數,預設為空")
// 定義整型命令列引數age
age := flag.Int("a", 0, "age引數,預設為0")
// 定義浮點型命令列引數height
height := flag.Float64("h", 0, "height引數,預設為0")
// 定義布林型命令列引數graduated
graduated:= flag.Bool("g", false, "graduated引數,預設為false")

4、flag包命令列引數解析

固定用法,定義好引數後,通過呼叫 flag.Parse() 來對命令列引數進行解析寫入註冊的 flag 裡,進而解析獲取引數值,通過檢視原始碼中也是呼叫的 os.Args

原始碼路徑 go/src/flag/flag.go

// Parse parses the command-line flags from os.Args[1:]. Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
	// Ignore errors; CommandLine is set for ExitOnError.
	CommandLine.Parse(os.Args[1:])
}

進而檢視 Parse 方法的原始碼

func (f *FlagSet) Parse(arguments []string) error {
	f.parsed = true
	f.args = arguments
	for {
		seen, err := f.parseOne()
		if seen {
			continue
		}
		if err == nil {
			break
		}
		switch f.errorHandling {
		case ContinueOnError:
			return err
		case ExitOnError:
			if err == ErrHelp {
				os.Exit(0)
			}
			os.Exit(2)
		case PanicOnError:
			panic(err)
		}
	}
	return nil
}

真正解析引數的是 parseOne 方法(這裡省略原始碼),結論是

  • 當遇到單獨的一個 "-" 或不是 "-" 開始時,會停止解析
  • 遇到連續的兩個 "-" 時,解析停止
  • 在終止符"-"之後停止

解析引數時,對於引數的指定方式一般有"-"、"--"、以及是否空格等方式,組合下來有如下幾種方式

-flag xxx 空格和一個 - 符號
--flag xxx 空格和兩個 - 符號
-flag=xxx 等號和一個 - 符號
--flag=xxx 等號和兩個 - 符號

其中, -flag xxx 方式最為常用,如果引數是布林型,只能用等號方式指定

5、flag包命令列幫助

flag 包預設會根據定義的命令列引數,在使用時如果不輸入引數就列印對應的幫助資訊

這樣的幫助資訊我們可以對其進行覆蓋去改變預設的 Usage

package main

import (
    "flag"
    "fmt"
)

func main()  {
    var host string
    var port int
    var verbor bool
    var help bool
    // 繫結命令列引數與變數關係
    flag.StringVar(&host, "H", "127.0.0.1", "ssh host")
    flag.IntVar(&port, "P", 22, "ssh port")
    flag.BoolVar(&verbor, "v", false, "detail log")
    flag.BoolVar(&help, "h", false, "help")
    // 自定義-h
    flag.Usage = func() {
        fmt.Println(`
Usage: flag [-H addr] [-p port] [-v]

Options: 
    `)
        flag.PrintDefaults()
    }
    // 解析命令列引數
    flag.Parse()
    if help {
        flag.Usage()
    } else {
        fmt.Println(host, port, verbor)
    }
}
/*
➜  go run flag_args.go -h

Usage: flag [-H addr] [-p port] [-v]

Options:

  -H string
        ssh host (default "127.0.0.1")
  -P int
        ssh port (default 22)
  -h    help
  -v    detail log
 */

6、flag定義短引數和長引數

簡單來說,短引數和長引數,就是例如我們在使用某些命令時,檢視命令版本可以輸入 -V ,也可以輸入 --version 。這種情況下, flag 並沒有預設支援,但是可以通過可以兩個選項共享同一個變數來實現,即通過給某個相同的變數設定不同的選項,引數在初始化的時候其順序是不固定的,因此還需要保證其擁有相同的預設值

package main

import (
  "fmt"
  "flag"
)

var logLevel string

func init() {
  const (
    defaultLogLevel = "DEBUG"
    usage = "set log level"
  )
  flag.StringVar(&logLevel, "log_level", defaultLogLevel, usage)
  flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)")
}

func main() {
  flag.Parse()
  fmt.Println("log level:", logLevel)
}

通過 const 宣告公共的常量,並在預設值以及幫助資訊中去使用,這樣就可以實現了

7、示例

實現計算字串或目錄下遞迴計算檔案 md5 的命令,類似 linuxmd5sum 命令

其中利用 bufio 分批次讀取檔案,防止檔案過大時造成資源佔用高

package main

import (
	"bufio"
	"crypto/md5"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
)

func md5reader(reader *bufio.Reader) string {  //
	hasher := md5.New()  // 定義MD5 hash計算器
	bytes := make([]byte, 1024*1024*10)  // 分批次讀取檔案

	for {
		n, err := reader.Read(bytes)
		if err != nil {
			if err != io.EOF {
				return ""
			}
			break
		} else {
			hasher.Write(bytes[:n])
		}
	}
	return fmt.Sprintf("%x", hasher.Sum(nil))
}

func md5file(path string) (string, error) {
	file, err := os.Open(path)
	if err != nil {
		return "", err
	} else {
		defer file.Close()
		return md5reader(bufio.NewReader(file)), nil
	}
}

func md5str(txt string) (string, error) {
	return md5reader(bufio.NewReader(strings.NewReader(txt))), nil
	//return fmt.Sprintf("%x", md5.Sum([]byte(txt)))
}

func main()  {
	txt := flag.String("s", "", "md5 txt")
	path := flag.String("f", "", "file path")
	help := flag.Bool("h", false, "help")
	flag.Usage = func() {
		fmt.Println(`
Usage: md5 [-s 123abc] [-f path]
Options:
		`)
		flag.PrintDefaults()
	}
	flag.Parse()
	if *help || *txt == "" && *path == "" {
		flag.Usage()
	} else {
		var md5 string
		var err error
		if *path != "" {
			md5, err = md5file(*path)
		} else {
			md5, err = md5str(*txt)
		}
		if err != nil {
			fmt.Println(err)
		} else {
			fmt.Println(md5)
		}
	}
}

編譯生成二進位制檔案

➜  go build -o md5go -x md5_bufio.go
➜  ll md5go 
-rwxr-xr-x  1 ssgeek  staff   1.9M Oct 2 00:54 md5go

測試使用

➜  ./md5go -h             

Usage: md5 [-s 123abc] [-f path]
Options:
                
  -f string
        file path
  -h    help
  -s string
        md5 txt
➜  ./md5go -s 123456
e10adc3949ba59abbe56e057f20f883e
➜  ./md5go -f md5_bufio.go
8607a07cbb98cec0e9abe14b0db0bee6

Just here,see you ~