日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

一种命令行解析的新思路(Go 语言描述)

發(fā)布時(shí)間:2024/8/23 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一种命令行解析的新思路(Go 语言描述) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

簡介:?本文通過打破大家對命令行的固有印象,對命令行的概念解構(gòu)后重新梳理,開發(fā)出一種功能強(qiáng)大但使用極為簡單的命令行解析方法。這種方法支持任意多的子命令,支持可選和必選參數(shù),對可選參數(shù)可提供默認(rèn)值,支持配置文件,環(huán)境變量及命令行參數(shù)同時(shí)使用,配置文件,環(huán)境變量,命令行參數(shù)生效優(yōu)先級依次提高,這種設(shè)計(jì)可以更符合 12 factor的原則。

作者 | 克識
來源 | 阿里技術(shù)公眾號

一 概述

命令行解析是幾乎每個(gè)后端程序員都會用到的技術(shù),但相比業(yè)務(wù)邏輯來說,這些細(xì)枝末節(jié)顯得并不緊要,如果僅僅追求滿足簡單需求,命令行的處理會比較簡單,任何一個(gè)后端程序員都可以信手拈來。Go 標(biāo)準(zhǔn)庫提供了 flag 庫以供大家使用。

然而,當(dāng)我們稍微想讓我們的命令行功能豐富一些,問題開始變得復(fù)雜起來,比如,我們要考慮如何處理可選項(xiàng)和必選項(xiàng),對于可選項(xiàng),如何設(shè)置其默認(rèn)值,如何處理子命令,以及子命令的子命令,如何處理子命令的參數(shù)等等。

目前,Go 語言中使用最廣泛功能最強(qiáng)大的命令行解析庫是 cobra,但豐富的功能讓 cobra 相比標(biāo)準(zhǔn)庫的 flag 而言,變得異常復(fù)雜,為了減少使用的復(fù)雜度,cobra 甚至提供了代碼生成的功能,可以自動生成命令行的骨架。然而,自動生成在節(jié)省了開發(fā)時(shí)間的同時(shí),也讓代碼變得不夠直觀。

本文通過打破大家對命令行的固有印象,對命令行的概念解構(gòu)后重新梳理,開發(fā)出一種功能強(qiáng)大但使用極為簡單的命令行解析方法。這種方法支持任意多的子命令,支持可選和必選參數(shù),對可選參數(shù)可提供默認(rèn)值,支持配置文件,環(huán)境變量及命令行參數(shù)同時(shí)使用,配置文件,環(huán)境變量,命令行參數(shù)生效優(yōu)先級依次提高,這種設(shè)計(jì)可以更符合 12 factor的原則。

二 現(xiàn)有的命令行解析方法

Go 標(biāo)準(zhǔn)庫 flag提供了非常簡單的命令行解析方法,定義好命令行參數(shù)后,只需要調(diào)用 flag.Parse方法即可。

// demo.go var limit int flag.IntVar(&limit, "limit", 10, "the max number of results") flag.Parse() fmt.Println("the limit is", limit)// 執(zhí)行結(jié)果 $ go run demo.go the limit is 10 $ go run demo.go -limit 100 the limit is 100

可以看到, flag 庫使用非常簡單,定要好命令行參數(shù)后,只需要調(diào)用 flag.Parse就可以實(shí)現(xiàn)參數(shù)的解析。在定義命令行參數(shù)時(shí),可以指定默認(rèn)值以及對這個(gè)參數(shù)的使用說明。

如果要處理子命令,flag 就無能為力了,這時(shí)候可以選擇自己解析子命令,但更多的是直接使用 cobra 這個(gè)庫。

這里用 cobra 官方給出的例子,演示一下這個(gè)庫的使用方法

package mainimport ("fmt""strings""github.com/spf13/cobra" )func main() {var echoTimes intvar cmdPrint = &cobra.Command{Use: "print [string to print]",Short: "Print anything to the screen",Long: `print is for printing anything back to the screen. For many years people have printed back to the screen.`,Args: cobra.MinimumNArgs(1),Run: func(cmd *cobra.Command, args []string) {fmt.Println("Print: " + strings.Join(args, " "))},}var cmdEcho = &cobra.Command{Use: "echo [string to echo]",Short: "Echo anything to the screen",Long: `echo is for echoing anything back. Echo works a lot like print, except it has a child command.`,Args: cobra.MinimumNArgs(1),Run: func(cmd *cobra.Command, args []string) {fmt.Println("Echo: " + strings.Join(args, " "))},}var cmdTimes = &cobra.Command{Use: "times [string to echo]",Short: "Echo anything to the screen more times",Long: `echo things multiple times back to the user by providing a count and a string.`,Args: cobra.MinimumNArgs(1),Run: func(cmd *cobra.Command, args []string) {for i := 0; i < echoTimes; i++ {fmt.Println("Echo: " + strings.Join(args, " "))}},}cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")var rootCmd = &cobra.Command{Use: "app"}rootCmd.AddCommand(cmdPrint, cmdEcho)cmdEcho.AddCommand(cmdTimes)rootCmd.Execute() }

可以看到子命令的加入讓代碼變得稍微復(fù)雜,但邏輯仍然是清晰的,并且子命令和跟命令遵循相同的定義模板,子命令還可以定義自己子命令。

$ go run cobra.go echo times hello --times 3 Echo: hello Echo: hello Echo: hello

cobra 功能強(qiáng)大,邏輯清晰,因此得到大家廣泛的認(rèn)可,然而,這里卻有兩個(gè)問題讓我無法滿意,雖然問題不大,但時(shí)時(shí)縈懷于心,讓人郁郁。

1 參數(shù)定義跟命令邏輯分離

從上面 --times的定義可以看到,參數(shù)的定義跟命令邏輯的定義(即這里的 Run)是分離的,當(dāng)我們有大量子命令的時(shí)候,我們更傾向把命令的定義放到不同的文件甚至目錄,這就會出現(xiàn)命令的定義是分散的,而所有命令的參數(shù)定義卻集中在一起的情況。

當(dāng)然,這個(gè)問題用 cobra 也很好解決,只要把參數(shù)定義從 main函數(shù)移動到 init函數(shù),并將 init 函數(shù)分散到跟子命令的定義一起即可。比如子命令 times 定義在 times.go文件中,同時(shí)在文件中定義 init函數(shù),函數(shù)中定義了 times 的參數(shù)。然而,這樣導(dǎo)致當(dāng)參數(shù)比較多時(shí)需要定義大量的全局變量,這對于追求代碼清晰簡潔無副作用的人來說如芒刺背。

為什么不能像 flag庫一樣,把參數(shù)定義放到命令函數(shù)的里面呢?這樣代碼更緊湊,邏輯更直觀。

// 為什么我不能寫成下面這樣呢? func times(){cobra.IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")cobra.Parse() }

相信大家稍加思考就會明白,times函數(shù)只有解析完命令行參數(shù)才能調(diào)用,這就要求命令行參數(shù)要事先定義好,如果把參數(shù)定義放到 times,這就意味著只有調(diào)用 times函數(shù)時(shí)才會解析相關(guān)參數(shù),這就跟讓手機(jī)根據(jù)外殼顏色變換主題一樣無理取鬧,可是,真的是這樣嗎?

2 子命令與父命令的順序定義不夠靈活

在開發(fā)有子命令甚至多級子命令的工具時(shí),我們經(jīng)常面臨到底是選擇 cmd {resource} {action}還是 cmd {action} {resource}的問題,也就是 resource 和 action 誰是子命令誰是參數(shù)的問題,比如 Kubernetes 的設(shè)計(jì),就是 action 作為子命令:kubectl get pods ... kubectl get deploy ...,而對于 action 因不同 resource 而差別很大時(shí),則往往選擇 resource 作為子命令, 比如阿里云的命令行工具: aliyun ecs ... aliyun ram ...

在實(shí)際開發(fā)過程中,一開始我們可能無法確定action 和 resource 哪個(gè)作為子命令會更好,在有多級子命令的情況下這個(gè)選擇可能會更困難。

在不使用任何庫的時(shí)候,開發(fā)者可能會選擇在父命令中初始化相關(guān)資源,在子命令中執(zhí)行代碼邏輯,這樣父命令和子命令相互調(diào)換變得非常困難。 這其實(shí)是一種錯(cuò)誤的邏輯,調(diào)用子命令并不意味著一定要調(diào)用父命令,對于命令行工具來說,命令執(zhí)行完進(jìn)程就會退出,父命令初始化后的資源,并不會在子命令中重復(fù)使用。

cobra 的設(shè)計(jì)可以讓大家規(guī)避這個(gè)錯(cuò)誤邏輯,其子命令需要提供一個(gè) Run 函數(shù),在這個(gè)函數(shù),應(yīng)該實(shí)現(xiàn)初始化資源,執(zhí)行業(yè)務(wù)邏輯,銷毀資源的整個(gè)生命周期。然而,cobra 仍然需要定義父命令,即必須定義 echo 命令,才能定義 echo times 這個(gè)子命令。實(shí)際上,在很多場景下,父命令是沒有執(zhí)行邏輯的,特別是以 resource 作為父命令的場景,父命令的唯一作用就是打印這個(gè)命令的用法。

cobra 讓子命令和父命令的定義非常簡單,但父子調(diào)換仍然需要修改其間的鏈接關(guān)系,是否有方法讓這個(gè)過程更簡單一點(diǎn)呢?

三 重新認(rèn)識命令行

關(guān)于命令行的術(shù)語有很多,比如參數(shù)(argument),標(biāo)識(flag)和選項(xiàng)(option)等,cobra 的設(shè)計(jì)是基于以下概念的定義
Commands represent actions, Args are things and Flags are modifiers for those actions.

另外,又基于這些定義延伸出更多的概念,比如 persistent flags代表適用于所有子命令的 flag,local flags 代表只用于當(dāng)前子命令的 flag, required flags代表必選 flag 等等。

這些定義是 cobra 的核心設(shè)計(jì)來源,要想解決我上面提到的兩個(gè)問題,我們需要重新審視這些定義。為此,我們從頭開始一步步分析何為一個(gè)命令行。

1 命令行只是一個(gè)可被 shell 解析執(zhí)行的字符串

$ cmd arg1 arg2 arg3

命令行及其參數(shù),本質(zhì)上就是一個(gè)字符串而已。字符串的含義是由 shell來解釋的,對于 shell來說,一個(gè)命令行由命令和參數(shù)組成,命令和參數(shù)以及參數(shù)和參數(shù)之間是由空白符分割。

還有別的嗎? 沒了,沒有什么父命令、子命令,也沒有什么持久參數(shù)、本地參數(shù),一個(gè)參數(shù)是雙橫線(--) 、單橫線(-)還是其他字符開頭,都沒有關(guān)系,這只是字符串而已,這些字符串由 shell 傳遞給你要執(zhí)行的程序,并放到 os.Args (Go 語言)這個(gè)數(shù)組里。

2 參數(shù)、標(biāo)識與選項(xiàng)

從上面的描述可知,參數(shù)(argument)是對命令行后面那一串空白符分隔的字符串的稱呼,而一個(gè)參數(shù),在命令行中又可以賦予不同的含義。

以橫線或雙橫線開頭的參數(shù)看起來有些特殊,結(jié)合代碼來看,這種類型的參數(shù)有其獨(dú)特的作用,就是將某個(gè)值跟代碼中的某個(gè)變量關(guān)聯(lián)起來,這種類型的參數(shù),我們叫做標(biāo)識(flag)。回想一下,os.Args 這個(gè)數(shù)組里的參數(shù)有很多,這些參數(shù)跟命令中的變量是沒有直接關(guān)系的,而 flag 提供的本質(zhì)上是一個(gè)鍵值對,我們的代碼中,通過把鍵跟某個(gè)變量關(guān)聯(lián)起來,從而實(shí)現(xiàn)了對這個(gè)變量賦值的功能。

flag.IntVar(&limit, "limit", 10, "the max number of results")// 變量綁定,當(dāng)在命令行中指定 -limit 100 的時(shí)候,這意味著我們是把 100 這個(gè)值,賦予變量 limit

標(biāo)識(flag)賦予了我們通過命令行直接給代碼中某個(gè)變量賦值的能力。那么一個(gè)新的問題是,如果我沒有給這個(gè)變量賦值呢,程序還能繼續(xù)運(yùn)行下去嗎?如果不能繼續(xù)運(yùn)行,則這個(gè)參數(shù)(flag 只是一種特殊的參數(shù))就是必選的,否則就是可選的。還有一種可能,命令行定義了多個(gè)變量,任意一個(gè)變量有值,程序都可以執(zhí)行下去,也即是說只要這多個(gè)標(biāo)識中隨便指定一個(gè),程序就可以執(zhí)行,那么這些標(biāo)識或參數(shù)從這個(gè)角度講又可以叫做選項(xiàng)(option)。

經(jīng)過上面的分析,我們發(fā)現(xiàn)參數(shù)、標(biāo)識、選項(xiàng)的概念彼此交織,既有區(qū)別又有相近的含義。標(biāo)識是以橫線開頭的參數(shù),標(biāo)識名后面的參數(shù)(如果有的話),是標(biāo)識的值。這些參數(shù)可能是必選或可選,或多個(gè)選項(xiàng)中的一個(gè),因此這些參數(shù)又可以稱為選項(xiàng)。

3 子命令

經(jīng)過上面的分析,我們可以很簡單的得出結(jié)論,子命令只是一種特殊的參數(shù),這種參數(shù)外觀上跟其他參數(shù)沒有任何區(qū)別(不像標(biāo)識用橫線開頭),但是這個(gè)參數(shù)會引發(fā)特殊的動作或函數(shù)(任意動作都可以封裝為一個(gè)函數(shù))。

對比標(biāo)識和子命令我們會意外的發(fā)現(xiàn)其中的關(guān)聯(lián):標(biāo)識關(guān)聯(lián)變量而子命令關(guān)聯(lián)函數(shù)!他們具有相同的目的,標(biāo)識后面的參數(shù),是變量的值,那么子命令后面的所有參數(shù),就是這個(gè)函數(shù)的參數(shù)(并非指語言層面的函數(shù)參數(shù))。

更有趣的問題是,為什么標(biāo)識需要以橫線開頭?如果沒有橫線,是否能達(dá)成關(guān)聯(lián)變量的目的?這顯然可以的,因?yàn)樽用罹蜎]有橫線,對變量的關(guān)聯(lián)和對函數(shù)的關(guān)聯(lián)并沒有什么區(qū)別。本質(zhì)上,這個(gè)關(guān)聯(lián)是通過標(biāo)識或子命令的名字實(shí)現(xiàn)的,那橫線起到什么作用呢?

是跟變量關(guān)聯(lián)還是函數(shù)關(guān)聯(lián),仍然是由參數(shù)的名字決定的,這是在代碼中預(yù)先定義的,沒有橫線一樣可以區(qū)別標(biāo)識和子命令,一樣可以完成變量或參數(shù)的關(guān)聯(lián)。

比如:

// 不帶有橫線的參數(shù)也可以實(shí)現(xiàn)關(guān)聯(lián)變量或函數(shù) for _, arg := range os.Args{switch arg{case "limit": // 設(shè)置 limit 變量case "scan": // 調(diào)用 scan 函數(shù)} }

由此可見,標(biāo)識在核心功能實(shí)現(xiàn)上,并沒有特殊的作用,橫線的作用主要是用來增強(qiáng)可讀性。然而需要注意的是,雖然本質(zhì)上我們可以不需要標(biāo)識,但一旦有了標(biāo)識,我們就可以利用其特性實(shí)現(xiàn)額外的功用,比如 netstat -lnt這里的 -lnt就是 -l -n -t的語法糖。

4 命令行的構(gòu)成

經(jīng)過上面的分析,我們可以把命令行的參數(shù)賦予不同的概念

  • 標(biāo)識(flag):以橫線或雙橫線開頭的參數(shù),標(biāo)識又由標(biāo)識名和標(biāo)識參數(shù)組成

    • --flagname flagarg
  • 非標(biāo)識參數(shù)
  • 子命令(subcommand),子命令也會有子命令,標(biāo)識和非標(biāo)識參數(shù)
$ command --flag flagarg subcommand subcmdarg --subcmdfag subcmdflagarg

四 啟發(fā)式命令行解析

我們來重新審視一下第一個(gè)需求,即我們期望任何一個(gè)子命令的實(shí)現(xiàn),都跟使用標(biāo)準(zhǔn)庫的 flag 一樣簡單。這也就意味著,只有在執(zhí)行這個(gè)函數(shù)的時(shí)候,才開始解析其命令行參數(shù)。如果我們能把子命令和其他參數(shù)區(qū)分開來,那么就可以先執(zhí)行子命令對應(yīng)的函數(shù),后解析這個(gè)子命令的參數(shù)。

flag 之所以在 main中調(diào)用 Parse, 是因?yàn)?shell 已經(jīng)知道字符串的第一個(gè)項(xiàng)是命令本身,后面所有項(xiàng)都是參數(shù),同樣的,如果我們能識別出子命令來,那么也可以讓以下代碼變?yōu)榭赡?#xff1a;

func command(){// 定義 flags// 調(diào)用 Parse 函數(shù) }

問題的關(guān)鍵是如何將子命令跟其他參數(shù)區(qū)分開來,其中標(biāo)識名以橫線或雙橫線開頭,可以顯而易見的區(qū)別開來,其他則需要區(qū)分子命令、子命令參數(shù)以及標(biāo)識參數(shù)。仔細(xì)思考可以發(fā)現(xiàn),我們雖然期望參數(shù)無需預(yù)先定義,但子命令是可以預(yù)先定義的,通過把非標(biāo)識名的參數(shù),跟預(yù)先定義的子命令比對,則可以識別出子命令來。

為了演示如何識別出子命令,我們以上面 cobra 的代碼為例,假設(shè) cobra.go 代碼編譯為程序 app,那么其命令行可以執(zhí)行

$ app echo times hello --times 3

按 cobra 的概念, times 是 echo 的子命令,而 echo 又是 app 的子命令。我們則把 echo times整體作為 app 的子命令。

1 簡單解析流程

  • 定義echo子命令關(guān)聯(lián)到函數(shù)echo, echo times子命令關(guān)聯(lián)到函數(shù) echoTimes
  • 解析字符串 echo times hello --times 3
  • 解析第一個(gè)參數(shù),通過 echo匹配到我們預(yù)定義的 echo子命令,同時(shí)發(fā)現(xiàn)這也是 echo times命令的前綴部分,此時(shí),只有知道后一個(gè)參數(shù)是什么,我們才能確定用戶調(diào)用的是 echo還是 echo times
  • 解析第二個(gè)參數(shù),通過 times我們匹配到 echo times子命令,并且其不再是任何子命令的前綴。此時(shí)確定子命令為 echo times,其他所有參數(shù)皆為這個(gè)子命令的參數(shù)。
  • 如果解析第二個(gè)參數(shù)為 hello,那么其只能匹配到 echo這個(gè)子命令,那么會調(diào)用 echo函數(shù)而不是 echoTimes函數(shù)。
  • 2 啟發(fā)式探測流程

    上面的解析比較簡單,但現(xiàn)實(shí)情況下,我們往往期望允許標(biāo)識可以出現(xiàn)在命令行的任意位置,比如,我們期望新加一個(gè)控制打印顏色的選項(xiàng) --color red,從邏輯上講,顏色選項(xiàng)更多的是對 echo的描述,而非對 times的描述,因此我們期望可以支持如下的命令行:

    $ app echo --color red times hello --times 3

    此時(shí),我們期望調(diào)用的子命令仍然是 echo times,然而中間的參數(shù)讓情況變得復(fù)雜起來,因?yàn)檫@里的參數(shù) red可能是 --color的標(biāo)識參數(shù)(red),可能是子命令的一部分,也可能是子命令的參數(shù)。更有甚者,用戶還可能把參數(shù)錯(cuò)誤的寫為 --color times

    所謂啟發(fā)式的探測,是指當(dāng)解析到 red參數(shù)時(shí),我們并不知道 red到底是子命令(或者子命令的前綴部分),還是子命令的參數(shù),因此我們可以將其假定為子命令的前綴進(jìn)行匹配,如果匹配不到,則將其當(dāng)做子命令參數(shù)處理。

  • 解析到 red時(shí),用 echo red搜索預(yù)定義的子命令,若搜索不到,則將 red視為參數(shù)
  • 解析 times時(shí),用 echo times搜索預(yù)定義的子命令,此時(shí)可搜索到 echo times子命令
  • 可以看到 red不需區(qū)分是 --color的標(biāo)識參數(shù),還是子命令的非標(biāo)識參數(shù),只要其匹配不到任何子命令,則可以確認(rèn),其一定是子命令的參數(shù)。

    3 子命令任意書寫順序

    子命令本質(zhì)上就是一個(gè)字符串,我們上面的啟發(fā)式解析已經(jīng)實(shí)現(xiàn)將任意子命令字符串識別出來,前提是預(yù)先對這個(gè)字符串進(jìn)行定義。也就是將這個(gè)字符串關(guān)聯(lián)到某個(gè)函數(shù)。這樣的設(shè)計(jì)使得父命令、子命令只是邏輯上的概念,而跟具體的代碼實(shí)現(xiàn)毫無關(guān)聯(lián),我們需要做的就是調(diào)整映射而已。

    維護(hù)映射關(guān)系

    # 關(guān)聯(lián)到 echoTimes 函數(shù) "echo times" => echoTimes# 調(diào)整子命令只是改一下這個(gè)映射而已 "times echo" => echoTimes

    五 Cortana: 基于啟發(fā)式命令行解析的實(shí)現(xiàn)

    為了實(shí)現(xiàn)上述思路,我開發(fā)了 Cortana這個(gè)項(xiàng)目。Cortana 引入 Btree 建立子命令與函數(shù)之間的映射關(guān)系,得益于其前綴搜索的能力,用戶輸入任意子命令前綴,程序都會自動列出所有可用的子命令。啟發(fā)式命令行解析機(jī)制,可以在解析具體的標(biāo)識或子命令參數(shù)前,先解析出子命令,從而搜索到子命令所映射的函數(shù),在映射的函數(shù)中,去真正的解析子命令的參數(shù),實(shí)現(xiàn)變量的綁定。另外,Cortana 充分利用了 Go 語言 Struct Tag 的特性,簡化了變量綁定的流程。

    我們用 cortana 重新實(shí)現(xiàn) cobra 代碼的功能

    package mainimport ("fmt""strings""github.com/shafreeck/cortana" )func print() {cortana.Title("Print anything to the screen")cortana.Description(`print is for printing anything back to the screen. For many years people have printed back to the screen.`)args := struct {Texts []string `cortana:"texts"`}{}cortana.Parse(&args)fmt.Println(strings.Join(args.Texts, " ")) }func echo() {cortana.Title("Echo anything to the screen")cortana.Description(`echo is for echoing anything back. Echo works a lot like print, except it has a child command.`)args := struct {Texts []string `cortana:"texts"`}{}cortana.Parse(&args)fmt.Println(strings.Join(args.Texts, " ")) }func echoTimes() {cortana.Title("Echo anything to the screen more times")cortana.Description(`echo things multiple times back to the user by providinga count and a string.`)args := struct {Times int `cortana:"--times, -t, 1, times to echo the input"`Texts []string `cortana:"texts"`}{}cortana.Parse(&args)for i := 0; i < args.Times; i++ {fmt.Println(strings.Join(args.Texts, " "))} }func main() {cortana.AddCommand("print", print, "print anything to the screen")cortana.AddCommand("echo", echo, "echo anything to the screen")cortana.AddCommand("echo times", echoTimes, "echo anything to the screen more times")cortana.Launch() }

    命令用法跟 cobra 完全一樣,只是自動生成的幫助信息有一些區(qū)別

    # 不加任何子命令,輸出自動生成的幫助信息 $ ./app Available commands:print print anything to the screen echo echo anything to the screen echo times echo anything to the screen more times# 默認(rèn)啟用 -h, --help 選項(xiàng),開發(fā)者無需做任何事情 $ ./app print -h Print anything to the screenprint is for printing anything back to the screen. For many years people have printed back to the screen.Usage: print [texts...]-h, --help help for the command# echo 任意內(nèi)容 $ ./app echo hello worldhello world# echo 任意次數(shù) $ ./app echo times hello world --times 3hello worldhello worldhello world# --times 參數(shù)可以在任意位置 $ ./app echo --times 3 times hello worldhello worldhello worldhello world

    1 選項(xiàng)與默認(rèn)值

    args := struct {Times int `cortana:"--times, -t, 1, times to echo the input"`Texts []string `cortana:"texts"` }{}

    可以看到, echo times 命令有一個(gè) --times 標(biāo)識,另外,則是要回顯的內(nèi)容,內(nèi)容本質(zhì)上也是命令行參數(shù),并且可能因?yàn)閮?nèi)容中有空格,而被分割為多個(gè)參數(shù)。

    我們上面提到,標(biāo)識本質(zhì)上是將某個(gè)值綁定到某個(gè)變量,標(biāo)識的名字,比如這里的 --times,跟變量 args.Times 關(guān)聯(lián),那么對于非標(biāo)識的其他參數(shù)呢,這些參數(shù)是沒有名字的,因此我們統(tǒng)一綁定到一個(gè) Slice,也就是 args.Texts

    Cortana 定義了屬于自己的 Struct Tag,分別用來指定其長標(biāo)識名、短標(biāo)識名,默認(rèn)值和這個(gè)選項(xiàng)的描述信息。其格式為: cortana:"long, short, default, description"

    • 長標(biāo)識名(long): --flagname, 任意標(biāo)識都支持長標(biāo)識名的格式,如果不寫,則默認(rèn)用字段名
    • 短標(biāo)識名(short): -f,可以省略
    • 默認(rèn)值(default):可以為任意跟字段類型匹配的值,如果省略,則默認(rèn)為空值,如果為單個(gè)橫線 "-",則標(biāo)識用戶必須提供一個(gè)值
    • 描述(description):這個(gè)選項(xiàng)的描述信息,用于生成幫助信息,描述中可以包含任意可打印字符(包括逗號和空格)

    為了便于記憶,cortana這個(gè) Tag 名字也可以寫為 lsdd,即上述四部分的英文首字母。

    2 子命令與別名

    AddCommond 可以添加任意子命令,其本質(zhì)上是建立子命令與其處理函數(shù)的映射關(guān)系。

    cortana.AddCommand("echo", echo, "echo anything to the screen")

    在這個(gè)例子里,print命令和 echo命令是相同的,我們其實(shí)可以通過別名的方式將兩者關(guān)聯(lián)

    // 定義 print 為 echo 命令的別名 cortana.Alias("print", "echo")

    執(zhí)行 print 命令實(shí)際上執(zhí)行的是 echo

    $ ./app print -h Echo anything to the screenecho is for echoing anything back. Echo works a lot like print, except it has a child command.Available commands:echo times echo anything to the screen more timesUsage: echo [texts...]-h, --help help for the command

    別名的機(jī)制非常靈活,可以為任意命令和參數(shù)設(shè)置別名,比如我們期望實(shí)現(xiàn) three這個(gè)子命令,打印任意字符串 3 次。可以直接通過別名的方式實(shí)現(xiàn):

    cortana.Alias("three", "echo times --times 3") # three 是 echo times --times 3 的別名 $ ./app three hello worldhello worldhello worldhello world

    3 help 標(biāo)識和命令

    Cortana 自動為任意命令生成幫助信息,這個(gè)行為也可以通過 cortana.DisableHelpFlag禁用,也可以通過 cortana.HelpFlag來設(shè)定自己喜歡的標(biāo)識名。

    cortana.Use(cortana.HelpFlag("--usage", "-u")) # 自定義 --usage 來打印幫助信息 $ ./app echo --usage Echo anything to the screenecho is for echoing anything back. Echo works a lot like print, except it has a child command.Available commands:echo times echo anything to the screen more timesUsage: echo [texts...]-u, --usage help for the command

    Cortana 默認(rèn)并沒有提供 help子命令,但利用別名的機(jī)制,我們自己很容易實(shí)現(xiàn) help命令。

    cortana.Alias("help", "--help") // 通過別名,實(shí)現(xiàn) help 命令,用于打印任意子命令的幫助信息 $ ./app help echo times Echo anything to the screen more timesecho things multiple times back to the user by providinga count and a string.Usage: echo times [options] [texts...]-t, --times <times> times to echo the input. (default=1)-h, --help help for the command

    4 配置文件與環(huán)境變量

    除了通過命令行參數(shù)實(shí)現(xiàn)變量的綁定外,Cortana 還支持用戶自定義綁定配置文件和環(huán)境變量,Cortana 并不負(fù)責(zé)配置文件或環(huán)境變量的解析,用戶可以借助第三方庫來實(shí)現(xiàn)這個(gè)需求。Cortana 在這里的主要作用是根據(jù)優(yōu)先級合并不同來源的值。其遵循的優(yōu)先級順序如下:

    默認(rèn)值 < 配置文件 < 環(huán)境變量 < 參數(shù)

    Cortana 設(shè)計(jì)為便于用戶使用任意格式的配置,用戶只需要實(shí)現(xiàn) Unmarshaler 接口即可,比如,使用 JSON 作為配置文件:

    cortana.AddConfig("app.json", cortana.UnmarshalFunc(json.Unmarshal))

    Cortana 將配置文件或環(huán)境變量的解析完全交給第三方庫,用戶可以自由定義如何將配置文件綁定到變量,比如使用 jsonTag。

    5 沒有子命令?

    Cortana 的設(shè)計(jì)將命令查找和參數(shù)解析解耦,因此兩者可以分別獨(dú)立使用,比如在沒有子命令的場景下,直接在main函數(shù)中實(shí)現(xiàn)參數(shù)解析:

    func main(){args := struct {Version bool `cortana:"--version, -v, , print the command version"`}{}cortana.Parse(&args)if args.Version {fmt.Println("v0.1.1")return}// ... } $ ./app --version v0.1.1

    六 總結(jié)

    命令行解析是一個(gè)大家都會用到,但并不是特別重要的功能,除非是專注于命令行使用的工具,一般程序我們都不需要過多關(guān)注命令行的解析,所以對于對這篇文章的主題感興趣,并能讀到文章最后的讀者,我表示由衷的感謝。

    flag庫簡單易用,cobra 功能豐富,這兩個(gè)庫已經(jīng)幾乎可以滿足我們所有的需求。然而,我在編寫命令行程序的過程中,總感到現(xiàn)有的庫美中不足,flag庫只解決標(biāo)識解析的問題,cobra庫雖然支持子命令和參數(shù)的解析,但把子命令和參數(shù)的解析耦合在一起,導(dǎo)致參數(shù)定義跟函數(shù)分離。Cortana的核心訴求是將命令查找和參數(shù)解析解耦,我通過重新回歸命令行參數(shù)的本質(zhì),發(fā)明了啟發(fā)式解析的方法,最終實(shí)現(xiàn)了上述目標(biāo)。這種解耦使得 Cortana即具備 cobra一樣的豐富功能,又有像 flag一樣的使用體驗(yàn)。這種通過精巧設(shè)計(jì)而用非常簡單的機(jī)制實(shí)現(xiàn)強(qiáng)大功能體驗(yàn)讓我感到非常舒適,希望通過這篇文章,可以跟大家分享我的快樂。

    原文鏈接
    本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。?

    總結(jié)

    以上是生活随笔為你收集整理的一种命令行解析的新思路(Go 语言描述)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。