上篇文章 介紹了 flag 中如何擴展一個新的類型支持。本篇介紹如何使用 flag
實現子命令,總的來講,這篇纔是這個系列的核心,前兩篇只是鋪墊。git
前兩篇文章連接以下:github
Go 命令行解析 flag 包之快速上手
Go 命令行解析 flag 包之擴展新類型golang
但願看完本篇文章,若是再閱讀 go 命令的實現源碼,至少在總體結構上不會迷失方向了。數組
正式介紹子命令的實現以前,先了解下 flag 包中的一個類型,FlagSet
,它表示了一個命令。bash
從命令的組成要素上看,一個命令由命令名、選項 Flag
與參數三部分組成。相似以下:app
$ cmd --flag1 --flag2 -f=flag3 arg1 arg2 arg3
複製代碼
FlagSet
的定義也正符合了這一點,以下:框架
type FlagSet struct {
// 打印命令的幫助信息
Usage func() // 命令名稱 name string parsed bool // 實際傳入的 Flag actual map[string]*Flag // 會被使用的 Flag,經過 Flag.Var() 加入到了 formal 中 formal map[string]*Flag // 參數,Parse 解析命令行傳入的 []string, // 第一個不知足 Flag 規則的(如不是 - 或 -- 開頭), // 從這個位置開始,後面都是 args []string // arguments after flags // 發生錯誤時的處理方式,有三個選項,分別是 // ContinueOnError 繼續 // ExitOnError 退出 // PanicOnError panic errorHandling ErrorHandling output io.Writer // nil means stderr; use out() accessor } 複製代碼
包含字段有命令名 name
,選項 Flag 有 formal
和 actual
,參數 args
。函數
若是有人說,FlagSet
是命令行實現的核心,仍是比較認同的。之因此前面一直沒有提到它,主要是 flag 包爲了簡化命令行的處理流程,在 FlagSet
上作了進一步的封裝,簡單的使用能夠直接無視它的存在。oop
flag 中定義了一個全局的 FlagSet
類型變量,CommandLine
,用它表示整個命令行。能夠說,CommandLine
是 FlagSet
的一個特例,它的使用模式較爲固定,因此在它之上能提供了一套默認的函數。fetch
前面已經用過的一些,好比下面這些函數。
func BoolVar(p *bool, name string, value bool, usage string) {
CommandLine.Var(newBoolValue(value, p), name, usage)
}
func Bool(name string, value bool, usage string) *bool {
return CommandLine.Bool(name, value, usage)
}
func Parse() {
// Ignore errors; CommandLine is set for ExitOnError.
CommandLine.Parse(os.Args[1:])
}
複製代碼
更多的,這裏不一一列舉了。
接下來,咱們來脫掉這層外衣,梳理下命令行的整個處理流程吧。
CommandLine
的整個使用流程主要由三部分組成,分別是獲取命令名稱、定義命令中的實際選項和解析選項。
命令名稱在 CommandLine
建立的時候就已經指定了,以下:
CommandLine = NewFlagSet(os.Args[0], ExitOnError)
複製代碼
名稱由 os.Args[0]
指定,即命令行的第一個參數。除了命令名稱,同時指定的還有出錯時的處理方式,ExitOnError
。
接着是定義命令中實際會用到的 Flag
。
核心的代碼是 FlagSet.Var()
,以下所示:
func (f *FlagSet) Var(value Value, name string, usage string) {
// Remember the default value as a string; it won't change.
flag := &Flag{name, usage, value, value.String()}
// ...
// 省略部分代碼
// ...
if f.formal == nil {
f.formal = make(map[string]*Flag)
}
f.formal[name] = flag
}
複製代碼
以前使用過的 flag.BoolVar
和 flag.Bool
都是經過 CommandLine.Var()
,即 FlagSet.Var()
, 將 Flag
保存到 FlagSet.formal
中,以便於以後在解析的時候能將值成功設置到定義的變量中。
最後一步是從命令行中解析出選項 Flag
。因爲 CommandLine
表示的是整個命令行,因此它的選項和參數必定是從 os.Args[1:]
中解析。
flag.Parse
的代碼以下:
func Parse() {
// Ignore errors; CommandLine is set for ExitOnError.
CommandLine.Parse(os.Args[1:])
}
複製代碼
如今的重點是要了解 flag 中選項和參數的解析規則,如 gvg -v list
,按什麼規則肯定 -v
是一個 Flag,而 list
是參數的呢?
若是繼續向下追 Parse
的源碼,在 FlagSet.parseOne
中將發現 Flag
的解析規則。
func (f *FlagSet) ParseOne() if len(f.args) == 0 {
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {
return false, nil
}
numMinuses := 1
if s[1] == '-' {
numMinuses++
if len(s) == 2 { // "--" terminates the flags
f.args = f.args[1:]
return false, nil
}
}
// ...
}
複製代碼
三種狀況下會終止解析 Flag
,分別是當命令行參數所有解析結束,即 len(f.args) == 0
,或長度小於 2,但第一位字符不是 -
,或者參數長度等於 2,且第二個字符是 -
。以後的內容會繼續看成命令行參數處理。
若是沒有子命令,命令的解析工做到此就基本完成了,再日後就是業務代碼的開發了。那若是 CommandLine
還有子命令呢?
子命令和 CommandLine
不管是形式仍是邏輯上,基本沒什麼差別。形式上,子命令一樣包含選項和參數,邏輯上,子命令的選項和參數的解析規則與 CommandLine
相同。
一個包含子命令的命令行,形式以下:
$ cmd --flag1 --flag2 subcmd --subflag1 --subflag2 arg1 arg2
複製代碼
從上面能夠看出,若是 CommandLine
包含了子命令,能夠理解爲自己也就沒了參數,由於 CommandLine
的第一個參數便是子命令的名稱,而以後的參數要解析爲子命令的選項參數了。
如今,子命令的實現就變得很是簡單了,建立一個新的 FlagSet
,將 CommandLine
中的參數按前面介紹的流程從新處理一下。
第一步,獲取 CommandLine.Arg(0)
,檢查是否存在相應的子命令。
func main() {
flag.Parse()
if h {
flag.Usage()
return
}
cmdName := flag.Arg(0)
switch cmdName {
case "list":
_ = list.Exec(cmdName, flag.Args()[1:])
case "install":
_ = install.Exec(cmdName, flag.Args()[1:])
}
}
複製代碼
子命令的實現定義在另一個包中,以 list
命令爲例。 代碼以下:
var flagSet *flag.FlagSet
var origin string
func init() {
flagSet = flag.NewFlagSet("list", flag.ExitOnError)
val := newStringEnumValue("installed", &origin, []string{"installed", "local", "remote"})
flagSet.Var(
val, "origin",
"the origin of version information, such as installed, local, remote",
)
}
複製代碼
上面的代碼中,定義了 list
子命令的 FlagSet
,並在 Init
方法爲其增長了一個選項 Flag
,origin
。
Run
函數是真正執行業務邏輯的代碼。
func Run(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}
fmt.Println("list --oriign", origin)
return nil
}
複製代碼
最後的 Exec
函數組合 Init
和 Run
函數,已提供給 main
調用。
func Run(name string, args []string) error {
Init(name)
if err := Run(args); err != nil {
return err
}
return nil
}
複製代碼
命令行的解析完成,若是子命令還有子命令,處理的邏輯依然相同。接下來的工做,就能夠開始在 Run
函數中編寫業務代碼了。
如今,閱讀下 Go 命令的實現代碼吧。
因爲大佬們寫的代碼是基於 flag 包實現純手工打造,沒用任何的框架,在可讀性上會有點差。
源碼位於 go/src/cmd/go/cmd/main.go 下,經過 base.Go
變量初始化了 Go 支持的全部命令,以下:
base.Go.Commands = []*base.Command{
bug.CmdBug,
work.CmdBuild,
clean.CmdClean,
doc.CmdDoc,
envcmd.CmdEnv,
fix.CmdFix,
fmtcmd.CmdFmt,
generate.CmdGenerate,
modget.CmdGet,
work.CmdInstall,
list.CmdList,
modcmd.CmdMod,
run.CmdRun,
test.CmdTest,
tool.CmdTool,
version.CmdVersion,
vet.CmdVet,
help.HelpBuildmode,
help.HelpC,
help.HelpCache,
help.HelpEnvironment,
help.HelpFileType,
modload.HelpGoMod,
help.HelpGopath,
get.HelpGopathGet,
modfetch.HelpGoproxy,
help.HelpImportPath,
modload.HelpModules,
modget.HelpModuleGet,
modfetch.HelpModuleAuth,
modfetch.HelpModulePrivate,
help.HelpPackages,
test.HelpTestflag,
test.HelpTestfunc,
}
複製代碼
不管是 go
命令,仍是它的子命令,都是 *base.Command
類型。能夠看一下 *base.Command
的定義。
type Command struct {
Run func(cmd *Command, args []string) UsageLine string Short string Long string Flag flag.FlagSet CustomFlags bool Commands []*Command } 複製代碼
主要的字段有三個,分別是 Run
,主要負責業務邏輯的處理,FlagSet
,負責命令行的解析,以及 []*Command
, 所支持的子命令。
再來看看 main
函數中的核心邏輯。以下:
BigCmdLoop:
for bigCmd := base.Go; ; {
for _, cmd := range bigCmd.Commands {
// ...
// 主要邏輯代碼
// ...
}
// 打印幫助信息
helpArg := ""
if i := strings.LastIndex(cfg.CmdName, " "); i >= 0 {
helpArg = " " + cfg.CmdName[:i]
}
fmt.Fprintf(os.Stderr, "go %s: unknown command\nRun 'go help%s' for usage.\n", cfg.CmdName, helpArg)
base.SetExitStatus(2)
base.Exit()
}
複製代碼
從最頂層的 base.Go
開始,遍歷 Go 的全部子命令,若是沒有相應的命令,則打印幫助信息。
省略的那段主要邏輯代碼以下:
for _, cmd := range bigCmd.Commands {
// 若是找不到命令,繼續下次循環
if cmd.Name() != args[0] {
continue
}
// 檢查是否存在子命令
if len(cmd.Commands) > 0 {
// 將 bigCmd 設置爲當前的命令
// 好比 go tool compile,cmd 即爲 compile
bigCmd = cmd
args = args[1:]
// 若是沒有命令參數,則說明不符合命令規則,打印幫助信息。
if len(args) == 0 {
help.PrintUsage(os.Stderr, bigCmd)
base.SetExitStatus(2)
base.Exit()
}
// 若是命令名稱是 help,打印這個命令的幫助信息
if args[0] == "help" {
// Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
help.Help(os.Stdout, append(strings.Split(cfg.CmdName, " "), args[1:]...))
return
}
// 繼續處理子命令
cfg.CmdName += " " + args[0]
continue BigCmdLoop
}
if !cmd.Runnable() {
continue
}
cmd.Flag.Usage = func() { cmd.Usage() }
if cmd.CustomFlags {
// 解析參數和選項 Flag
// 自定義處理規則
args = args[1:]
} else {
// 經過 FlagSet 提供的方法處理
base.SetFromGOFLAGS(cmd.Flag)
cmd.Flag.Parse(args[1:])
args = cmd.Flag.Args()
}
// 執行業務邏輯
cmd.Run(cmd, args)
base.Exit()
return
}
複製代碼
主要是幾個部分,分別是查找命令,檢查是否存在子命令,選項和參數的解析,以及最後是命令的執行。
經過 cmd.Name() != args[0]
判斷是否查找到了命令,若是找到則繼續向下執行。
經過 len(cmd.Commands)
檢查是否存在子命令,存在將 bigCmd
覆蓋,並檢查是否符合命令行是否符合規範,好比檢查 len(args[1:])
若是爲 0,則說明傳入的命令行沒有提供子命令。若是一切就緒,經過 continue
進行下一次循環,執行子命令的處理。
接着是命令選項和參數的解析。能夠自定義處理規則,也能夠直接使用 FlagSet.Parse
處理。
最後,調用 cmd.Run
執行邏輯處理。
本文介紹了 Go 中如何經過 flag 實現子命令,從 FlagSet
這個結構體講起,經過 flag 包中默認提供的 CommandLine
梳理了 FlagSet
的處理邏輯。在基礎上,實現了子命令的相關功能。
本文最後,分析了 Go 源碼中 go
如何使用 flag 實現。由於是純粹使用 flag 包裸寫,讀起來稍微有點難度。本文只算是一個引子,至少幫助你們在大的方向不至於迷路,裏面更多的細節還須要本身挖掘。