Go Standard library - flag

Fxe0_0發表於2024-06-04

flag包介紹

flag包作為go語言標準包之一, 其作用是簡化命令列應用程式引數解析的步驟。
在不使用用flag包的情況下, 使用者想要解析命令列引數需要使用os.Args來獲取命令列引數後, 對引數進行預期處理, 說明幫助處理, 校驗合規性等一系列繁瑣的操作, 而使用flag包封裝了這一過程, 簡化了開發流程。
包內支援Int, Bool, String, Text等型別解析, 同時也提供flag.Var()的方法用於支援自定義型別的解析。

兩種引數解析對映方法

拿String型別的引數解析作為例子(其他型別的具有同樣性質), 在flag包中具有flag.String和flag.StringVar, 兩種方法均為對命令列引數解析對映的方法. 在使用上有細微的不同.
flag.String()
flag.String定義了命令列引數名, 預設值和使用方法, 並最終返回對映生成的string型別的地址.

// String defines a string flag with specified name, default value, and usage string.
// The return value is the address of a string variable that stores the value of the flag.
func (f *FlagSet) String(name string, value string, usage string) *string {
	p := new(string)
	f.StringVar(p, name, value, usage)
	return p
}

flag.StringVar()
對比來看flag.StringVar則不具有返回值, 而是直接將接收的引數傳入用於接收結果.

// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
func StringVar(p *string, name string, value string, usage string) {
	CommandLine.Var(newStringValue(value, p), name, usage)
}

使用示例-example1

package main

import (
	"flag"
	"fmt"
)

func main() {
	// flag.String
	name := flag.String("name", "default name", "set your name")
	// flag.StringVar
	var hobby string
	flag.StringVar(&hobby, "hobby", "default hobby", "set your hobby")
	// Parse
	flag.Parse()
	// Print value
	fmt.Println(*name)
	fmt.Println(hobby)
}

run

➜ go run main.go -name=fxe00 -hobby=game
fxe00
game

為什麼要使用flag.Parse()

在example1中可以發現第15行使用了flag.Parse(), 如果去掉這行再執行時列印結果為:

➜ go run main.go -name=fxe00 -hobby=game
default name
default hobby

說明flag.Parse()的作用是解析對映, 將使用者定義的引數解析到指定的變數之中。我剛接觸的時候存在一個疑問, "flag.String和flag.StringVar難道就沒做解析嗎? 為什麼還要多一步flag.Parse()呢?", 一個比較好的解釋是:

當你使用flag.Bool, flag.Int, flag.String等函式或者它們的Var變體(如flag.BoolVar, flag.IntVar等)來定義命令列引數時,你實際上是在註冊這些引數和它們的處理邏輯到flag包內部的一個全域性的狀態中。這些函式定義了引數的名字、預設值、用途描述以及儲存引數值的變數(或變數地址)。

然而,這些註冊操作只是設定了預期的引數配置,並沒有實際去讀取和處理命令列輸入的引數值。為了使這些配置生效,即根據實際的命令列輸入來填充之前定義好的變數,你需要呼叫flag.Parse()函式。

flag.Parse()的作用包括:

1.解析命令列:它會檢查傳給程式的實際命令列引數,根據引數名稱匹配之前註冊的標誌(flags),並將對應的值解析出來。

2.填充變數:解析出的值會被用來更新之前透過flag.*Var函式指定的變數,或者如果是使用flag.*函式的話,這些值會被儲存在flag包內部的對應變數中。

3.錯誤處理:如果命令列引數不符合預期(例如,缺少必要的引數,或者引數值的格式不正確),Parse函式可能會報告錯誤並透過呼叫flag.Usage函式顯示幫助資訊後退出程式。

因此,呼叫flag.Parse()是將使用者提供的命令列引數與程式中定義的引數配置關聯起來的關鍵步驟,使得程式能夠根據這些引數調整其行為。在呼叫flag.Parse()之後,你就可以安全地訪問那些被標誌變數所持有的值了。

flag.Parse()解析

當解決了為什麼要使用flag.Parse()的疑問後, 我又想看一下flag.Parse()在解析過程中的具體實現。

CommandLine.Parse()

flag.Parse()的函式定義入手發現實際是呼叫了CommandLine.Parse(os.Args[1:])

// 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:])
}

對於os.Args[1:]來說, 一個例子就可以說明其作用為接收使用者輸入引數並返回引數列表

➜ go run main.go 1 2 3 4 5 6
[1 2 3 4 5 6]

具體還是要分析CommandLine.Parse()這個函式, 其函式定義如下

// Parse parses flag definitions from the argument list, which should not
// include the command name. Must be called after all flags in the FlagSet
// are defined and before flags are accessed by the program.
// The return value will be ErrHelp if -help or -h were set but not defined.
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()

可以看出來Parse()實際上的主要邏輯是在解析時對異常的分流處理, 而解析操作實際發生在parseOne()中
繼續跟parseOne()函式

// parseOne parses one flag. It reports whether a flag was seen.
func (f *FlagSet) parseOne() (bool, error) {
	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
		}
	}
	name := s[numMinuses:]
	if len(name) == 0 || name[0] == '-' || name[0] == '=' {
		return false, f.failf("bad flag syntax: %s", s)
	}

	// it's a flag. does it have an argument?
	f.args = f.args[1:]
	hasValue := false
	value := ""
	for i := 1; i < len(name); i++ { // equals cannot be first
		if name[i] == '=' {
			value = name[i+1:]
			hasValue = true
			name = name[0:i]
			break
		}
	}
	m := f.formal
	flag, alreadythere := m[name] // BUG
	if !alreadythere {
		if name == "help" || name == "h" { // special case for nice help message.
			f.usage()
			return false, ErrHelp
		}
		return false, f.failf("flag provided but not defined: -%s", name)
	}

	if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg
		if hasValue {
			if err := fv.Set(value); err != nil {
				return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err)
			}
		} else {
			if err := fv.Set("true"); err != nil {
				return false, f.failf("invalid boolean flag %s: %v", name, err)
			}
		}
	} else {
		// It must have a value, which might be the next argument.
		if !hasValue && len(f.args) > 0 {
			// value is the next arg
			hasValue = true
			value, f.args = f.args[0], f.args[1:]
		}
		if !hasValue {
			return false, f.failf("flag needs an argument: -%s", name)
		}
		if err := flag.Value.Set(value); err != nil {
			return false, f.failf("invalid value %q for flag -%s: %v", value, name, err)
		}
	}
	if f.actual == nil {
		f.actual = make(map[string]*Flag)
	}
	f.actual[name] = flag
	return true, nil
}

parseOne()是對引數進行解析的具體邏輯, 最終呼叫flag.Value.Set(value)將引數值設定進對應的標誌flag中, 而Value實際上是flag包中定義的介面, 介面需要實現String()和Set()方法.

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
//
// If a Value has an IsBoolFlag() bool method returning true,
// the command-line parser makes -name equivalent to -name=true
// rather than using the next command-line argument.
//
// Set is called once, in command line order, for each flag present.
// The flag package may call the String method with a zero-valued receiver,
// such as a nil pointer.
type Value interface {
	String() string
	Set(string) error
}

對於flag包中可以解析的String型別可以看到在包中也對其進行了Value的實現

// -- string Value
type stringValue string

func newStringValue(val string, p *string) *stringValue {
	*p = val
	return (*stringValue)(p)
}

func (s *stringValue) Set(val string) error {
	*s = stringValue(val)
	return nil
}

func (s *stringValue) Get() any { return string(*s) }

func (s *stringValue) String() string { return string(*s) }

自定義引數型別:flag.Var()

flag.Var()函式支援使用者自定義引數的解析, 透過前面的分析, 引數的解析實際上最終都是透過呼叫flag.Value.Set(value)來實現, 也就是隻要實現了flag.Value的介面, 就可以透過flag.Var()來解析。

示例2:自定義引數型別解析-example2

為了和前面的型別區分, 例子裡自定義一個結構體型別來實現flag.Value介面

package main

import (
	"errors"
	"flag"
	"fmt"
)

type MyVar struct {
	wantSet    string
	anotherVar string
}

func (m *MyVar) String() string {
	return fmt.Sprint(*m)
}
func (m *MyVar) Set(value string) error {
	if len(m.wantSet) > 0 {
		return errors.New("name flag already set")
	}

	*m = MyVar{
		wantSet:    value,
		anotherVar: "default",
	}
	return nil
}

func main() {
	var myVar MyVar
	flag.Var(&myVar, "var", "Set My Var")
	flag.Parse()
	fmt.Println(myVar.String())
}

run

➜ go run main.go -var=test_my_var
{test_my_var default}

相關文章