CLI開發框架
命令行工具開發,主要是介紹開發用到的包,集成了一個框架,只要學會了基本每個人都能開發安全工具了。
該文章先學flags包,是比較經典的一個包,相比后面要學習的集成框架這個比較自由比較細化點,自定義可能高一些,后續會學到一個Cobra框架,這個很多安全工具都在使用,先學會flags包入門再去理解Cobra框架就比較好學。
flags包
- 支持段選項長選項
意思是使用的時候可以用簡寫也可以用完整的參數寫,比如一個工具叫a.exe
,用的時候可以a.exe -h
也可以a.exe -help
- 支持短選項組合寫,比如:
a.exe -p
a.exe -s
兩種可以寫在一起:a.exe -ps
- 一個參數選項可以給多個值(代碼中很容易了解到這一點)
- flags包默認有-h選項,解析完后–help和-h會返回你的所有參數解釋,不用自己寫help
安裝
go get github.com/jessevdk/go-flags
個人感覺學習這個框架只需要理解兩步:
- 指定結構體選項
- 解析結構體
基礎的選項:隨便寫幾個展示即可
- short:短選項名字
- long:長選項名字
- description:顯示你當前選項的一個解釋,-help或者-h的時候顯示
- tips:
// 參數選項type Option struct {//所有結構體首字母一定要大寫,否則解析不了,但是short或者long的名字就隨便你起V string `short:"v" long:"verbose" description:"顯示詳細信息"`Vlist []string `short:"V" long:"verbose-list" description:"顯示詳細信息列表"`I int `short:"i" long:"input" description:"int類型測試"`//這里可以給了一個默認值default:"xxx",不給的話在不使用該參數的時候就不用調用那個函數P func(string) `short:"p" long:"print" description:"打印"` // default:"myPrint"IntMap map[string]int `short:"m" long:"intmap" description:"intmap"`}
函數這里是要給一個函數傳遞進去使用,具體如下:
是在你實例化你的選項結構體后,將你寫好的函數傳遞進去,或者你寫一個匿名函數也行。
// 打印功能func myPrint(str string) {fmt.Println("打印-p參數值:", str)}
func main() {var opt Option //定義一個選項opt.P = myPrint //把打印函數賦值給P
}
使用的時候就可以用該參數了,傳遞的參數值就是給到函數變量使用的:
參數傳遞格式
- -V傳遞:
-V 123 -V 456 -V 789
這樣才能傳遞進map里面,不可以使用逗號或者空格隔開-V 123 456
/-V 123,456
都是不行的 - map的傳遞:
-m key1:123 -m key2:456
,不建議使用等號=
,至少我在windows測試的時候等號不能作為鍵值對分割,只能使用:
分割,同時多個map鍵值對也是多個-m才能傳遞添加進去。
剩下幾個類型自己實踐即可,這里就夠用了。
選項設置
選項設置都是直接在結構體選項反引號中直接添加即可,他會解析的。
-
required:在結構體選項中可設置,true的時候,該選項必選,否則報錯,一定要給這個選項一個值
比如:
V ?string ?short:"v" long:"verbose" description:"顯示詳細信息" required:true
-
default:表示你這個選項參數有一個默認值,就算你不加他也會以默認值形式作用本次運行
比如:上面結構體代碼注釋其實有提到
P ?func(string) ?short:"p" long:"print" description:"打印" default:"myPrint"
這里就是可以給一個default,表示這個選項的默認值是多少,可以看到我這里給的是一個函數名,這個函數名我也在代碼這種實現了嗎,所以運行的時候肯定是可以找到我這個函數然后執行
分組
這個功能比較好用,至少對我來說以后開發構思中肯定需要用到這個,以及后面講的子命令都是在一些比較完整的工具開發中用的比較多。
先看運行的效果圖
這里和之前的不一樣了,之前的沒有分組的時候就是直接把一些參數打印出來,這里會有參數分組,對比之下會更加直觀一點。
分組的結構體之間的聯系是比較緊密的
- 先創建好組名(組名也要用結構體)
結構體內的字段都是你接下來要創建的結構體選項,所以在這里可以先提前想好選項的結構體名字
比如下面的就是Host
和Scan
就是我們接下來要創建的分組的選項參數結構體
group
就是組名,到時候你在help中就能看到分組的組名
namespace
是空間名,在help中顯示為Host.xxx選項
,就是類似這樣,主要是告訴你哪個組下的選項,重要是group
是組名即可,顯示的時候比較明顯。
// basic 分組type Basic struct {HostOption Host `group:"host" namespace:"Host"`ScanOption Scan `group:"scan" namespace:"Scan"`}
- 正常創建你在分組的時候想好的那幾個結構體字段,我們給的名字是:
Host
和ScanOption
,那么接下來就是寫這兩個結構體了
注意:我在Scan
中又開了一個組ScanType
,所以還要寫一個ScanType
的結構體
這是help效果圖,應該很容易理解是怎么分組的了,也知道那個namespace
是什么了,注意區分Host
和host
,host是分組的組名,Host才是那個namespace。
type Host struct {HostName string `short:"N" long:"hostname" description:"主機名"`HostMac string `short:"M" long:"hostmac" description:"主機mac"`}// 在掃描類型中繼續分組type ScanType struct {HttpType string `short:"T" long:"http" description:"http掃描"`DataBase string `short:"D" long:"database" description:"數據庫掃描"`Other string `short:"O" long:"other" description:"其他掃描"`}type Scan struct {BasicScan ScanType `group:"scantype" namespace:"scantype"`ScanPort int `short:"P" long:"scanport" description:"掃描端口"`ScanIP string `short:"I" long:"scanip" description:"掃描ip"`}
子命令
簡單的來說:go version
這個 version
就是子命令,不用帶-
,直接用的就是子命令,-version
這種帶-
的就是選項而不是子命令哈,注意一個符號的區別。
同時記住一點:子命令在flags包中也自動實現了-h /h命令,所以不用編寫幫助信息。
(但是你想要實現不添加-h /h 就實現比如 xx.exe finger 也能給出幫助信息的話就要在接口中實現了,后續會在finger中講明白)
version
- 先實現
version
子命令
繼承了flags.Command的Execute接口就成功實現了子命令了,只剩下注冊到主要的結構體中,也就是之前學到的需要一個結構體注冊選項
(tips:version
的結構體為空,不是拿來切割或者分組,只是一個顯示版本號,后面講finger
的時候他才是作為這個分組命令來弄)
//給一個空的,因為version一般都不需要什么其他參數來輔助就可以看到版本了
type ChildCommand struct {}// 繼承了flags.Command即可自動調用func (c *ChildCommand) Execute(args []string) error {fmt.Println("version: 1.0.0")return nil}
- 注冊子命令到主選項結構體中,這里才是給到flags解析的結構體,注意這里給的鍵不再是
short
/long
,而是command
type VOption struct {Version ChildCommand `command:"version" description:"Version顯示版本信息"`}
運行效果沒問題(源碼稍后放)
finger(測試)
用finger
來加深理解
一般指紋識別的都是xx.exe finger -u xxx -p xxx
所以我認為用這個例子非常好
- 依舊是先做好一個子命令,但是這里要給子命令上兩個選項,用來指紋識別的ip和端口
同時要記得實現接口Execute
// 模擬一下指紋掃描中常見的一個子命令type Finger struct {U string `short:"u" long:"url" description:"url"`P string `short:"p" long:"port" description:"port"`}func (c *Finger) Execute(args []string) error {if c.U == "" || c.P == "" {// 如果沒有提供參數,顯示幫助信息,盡量做的完美一點parser := flags.NewParser(c, flags.Default)parser.WriteHelp(os.Stdout)return nil}return nil}
- 在主結構體中注冊這個子命令
我就直接在之前的VOption結構體注冊了
type VOption struct {Version ChildCommand `command:"version" description:"Version顯示版本信息"`// 指紋掃描Finger Finger `command:"finger" description:"Finger指紋掃描"`}
注冊完成就可以用了
- 加幫助參數
- 這個沒有加參數 -h 或者 /h等等
以上就是flags包的一些基礎常用的內容了,拿到參數之后就是往后丟給你自己寫的功能函數即可。
所有測試源碼
test1 基礎使用測試、test2分組測試 和 test3子命令 自己看著用就行。
package mainimport ("fmt""log""os""github.com/jessevdk/go-flags")// 參數選項type Option struct {//所有結構體首字母一定要大寫,否則解析不了,但是short或者long的名字就隨便你起V string `short:"v" long:"verbose" description:"顯示詳細信息"`Vlist []string `short:"V" long:"verbose-list" description:"顯示詳細信息列表"`I int `short:"i" long:"input" description:"int類型測試"`//這里可以給了一個默認值default:"xxx",不給的話在不使用該參數的時候就不用調用那個函數P func(string) `short:"p" long:"print" description:"打印"` // default:"myPrint"IntMap map[string]int `short:"m" long:"intmap" description:"intmap"`}// 打印功能func myPrint(str string) {fmt.Println("打印-p參數值:", str)}// basic 分組type Basic struct {HostOption Host `group:"host" namespace:"Host"`ScanOption Scan `group:"scan" namespace:"Scan"`}type Host struct {HostName string `short:"N" long:"hostname" description:"主機名"`HostMac string `short:"M" long:"hostmac" description:"主機mac"`}// 在掃描類型中繼續分組type ScanType struct {HttpType string `short:"T" long:"http" description:"http掃描"`DataBase string `short:"D" long:"database" description:"數據庫掃描"`Other string `short:"O" long:"other" description:"其他掃描"`}type Scan struct {BasicScan ScanType `group:"scantype" namespace:"scantype"`ScanPort int `short:"P" long:"scanport" description:"掃描端口"`ScanIP string `short:"I" long:"scanip" description:"掃描ip"`}type ChildCommand struct {}// 繼承了flags.Command即可自動調用func (c *ChildCommand) Execute(args []string) error {fmt.Println("version: 1.0.0")return nil}// 模擬一下指紋掃描中常見的一個子命令type Finger struct {U string `short:"u" long:"url" description:"url"`P string `short:"p" long:"port" description:"port"`}func (c *Finger) Execute(args []string) error {if c.U == "" || c.P == "" {// 如果沒有提供參數,顯示幫助信息parser := flags.NewParser(c, flags.Default)parser.WriteHelp(os.Stdout)return nil}return nil}type VOption struct {Version ChildCommand `command:"version" description:"Version顯示版本信息"`// 指紋掃描Finger Finger `command:"finger" description:"Finger指紋掃描"`}func test3() {parser := flags.NewParser(&VOption{}, flags.Default)_, err := parser.Parse()if err != nil {if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {return // 幫助信息已打印,直接退出}//如果不是打印的幫助信息報錯的話就直接log就行log.Println("parses failed: ", err)return}}func test1() {var opt Option //定義一個選項opt.P = myPrint //把打印函數賦值給Pparser := flags.NewParser(&opt, flags.Default)_, err := parser.Parse()if err != nil {if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {return // 幫助信息已打印,直接退出}//如果不是打印的幫助信息報錯的話就直接log就行log.Println("parses failed: ", err)return}fmt.Println("--------------------------")fmt.Println("打印-v參數值:", opt.V)fmt.Println("打印-V 列表所有的參數值:", opt.Vlist)fmt.Println("打印-i參數值:", opt.I)fmt.Println("打印--intmap參數值:", opt.IntMap)}func test2() {var BasicOption Basicparser := flags.NewParser(&BasicOption, flags.Default)_, err := parser.Parse()if err != nil {if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {return // 幫助信息已打印,直接退出}//如果不是打印的幫助信息報錯的話就直接log就行log.Println("parses failed: ", err)return}fmt.Println("--------------------------")}func main() {// test1()// test2()test3()}