cmdr 02 - 復刻一個 wget

cmdr 02 - Covered for wgetnginx

基於 cmdr v0.2.11

Getting Start 以後,咱們來介紹如何用 cmdr 復刻一個 wget 的命令行界面,並具體介紹 CommandFlag 的各個細節以及 cmdr 可以作到哪些別人作不到的事。git

此外,咱們也聲明一下,Getting Start ('另外一個go命令行參數處理器 - cmdr') 的內容有了一些輕微的變化,由於這兩週來,咱們已經不停地增長了不少特性來完善 cmdr 的能力,期間有一些不恰當的策略、衍生的命名、採用的算法都有所調整,雖然盡力避免變化,但它是不可免的。咱們是指望給你的編程界面愈來愈完美,讓整個編寫的流程流暢化,天然化。github

wget 的參數

wget 自己是一個 GNU 應用程序。它的命令行參數有長有短,短參數可能有兩個字符,此外參數被分爲若干個分組。請看一部分截取:算法

這將是咱們復刻的基準。編程

cmdr 都能作到些什麼 - First

咱們曾經作過多個應用,不一樣的開發語言,不一樣的目標,有的是練練手,有的是眼前有個事情有點煩、很差處理、一怒之下就幹,有的是有特定的目的例如一個RESTful服務,等等。segmentfault

因此,要想知足那麼多的狀況下命令行參數的組織和設定都能被很好地表示,不誇張地說,迄今數十年來,咱們沒有找到一個命令參數解釋器可以完成這個任務。把時間限定在最近幾年,把開發語言限定在 Golang,C++,Python 等幾種以內,依然沒有誰真的能這麼稱呼本身。現有的命令行參數解釋器都有這樣那樣的不如意:數組

  • 短參數不能重複,哪怕是在多級命令結構下也必須全局惟一;
  • 不能分組;
  • 分組後順序隨機或者字母序,開發者沒法干預,沒法按照本身的意願提供最好的順序;
  • 短參數須要兩個字母、或者三個字母的縮略語,更能表達參數原意時,基本上大多數現有的命令行參數解釋器都廢了;
  • 想要長參數顯示爲「--progress=TYPE」的式樣,其中的 TYPE 還能夠被複用;
  • 想要 git -m 的效果,結果費盡了力,終於實現了一個,然而受制於既有命令行解釋器的結構,實現的坑坑窪窪的,本身都難以滿意;
  • 想要和配置文件掛鉤,沒錯掛鉤了,然而須要寫不少代碼來安排;
  • 想要 /etc/program 加載配置文件,結果累了;想要 /etc/nginx/sites.avaliable 那樣的效果,本身 watch 了,卻合併不了新的配置到已經加載和構建好的配置中,也沒法有效地通知應用的業務層按需取用新的配置條目;
  • 還有不少

遇到這些狀況時,多數時候只能忍了,畢竟沒有太多精力專門去搞參數問題,還有大把的業務須要去完成的對吧。架構

cmdr 選擇和實現 wget-demo 也是爲了展現本身大致上可以解決命令行參數處理的多數問題。不過和其它命令行參數的策略不一樣地在於:別人一般會對參數值的類型作不少文章,例如支持 string/int/slice/map 的多種式樣,或者提供 validator,或者採用 Golang 結構 Tag 方式來掛鉤參數類型處理器等等。可是 cmdr 在參數類型方面只能說有且夠,總體的重心並不在這些方面。app

cmdr 具備一個精悍短小的關鍵處理器 InternalExecFor(),它負責處理組合短參數的各類狀況。函數

例如:對於 -1acg -t3 來講,cmdr 可以正確地識別到 -1 -c -c -g -t=3 的參數集合。

進一步地,對於 -4nva 來講,cmdr 可以正確識別到 -4 - nv -a 的參數集合。

此外,-mmsg -m msg -m=msg -m'what msg' -m"msg" '-mmsg' "-mWhat msg" 都是對的。在這裏,cmdr處理了多數變形形態,有的形態則沒必要處理,由於 Shell 會負責處理其中一部分引號問題。

cmdr 也關注短參數的字母重複問題,在不一樣層級的子命令之間,你能夠同時使用 -a 這樣的短參數,固然,-a 仍然不能在子命令內重複,也不能和子命令的上層命令的參數相沖突。長參數以及別名都有一樣的處理邏輯。

wget-demo 的實現細節

按照上一小節 cmdr 都能作到些什麼 - First 提到的 cmdr 的專一點的說法,wget-demo 已經能夠被很好地實現出來了。實際上,wget-demo 的代碼很是簡單(並不短),這也是 cmdr 想要給予開發者的方便。

這裏 查閱 wget-demo 的目錄。

這裏 查閱 wget-demo 的單一代碼文件。

main()

首先看 main:

func main() {
    logrus.SetLevel(logrus.DebugLevel)
    logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})

    // To disable internal commands and flags, uncomment the following codes
    cmdr.EnableVersionCommands = false
    cmdr.EnableVerboseCommands = false
    cmdr.EnableHelpCommands = false
    cmdr.EnableGenerateCommands = false
    cmdr.EnableCmdrCommands = false

    if err := cmdr.Exec(rootCmd); err != nil {
        logrus.Errorf("Error: %v", err)
    }
}

line 2,3能夠被忽略,那是便於 cmdr 開發階段的內容。發佈後的 cmdr 也依賴於 logrus,但實際上這是由於 cmdr 的 examples 的緣由,而 cmdr 自身是不作此依賴的,因此你仍是能夠本身選擇 logger。logger 問題之後或許會被 cmdr 慎重考慮,完全去除對任何 logger 的依賴。

line 5-10 是爲了 wget-demo 專用的。由於 wget 沒有命令和子命令,只有參數,所以 cmdr 內置的幾個命令(組)被禁用了。

真正的代碼,只有 line 12-14。無需解釋。

rootCmd

因此你須要作的只是編排 rootCmd 結構。

var (
    rootCmd = &cmdr.RootCommand{
        Command: cmdr.Command{
            BaseOpt: cmdr.BaseOpt{
                Name: "wget",
                Flags: append(
                    startupFlags,
                    append(loggerFlags,
                        downloadFlags...)...,
                ),
            },
            SubCommands: []*cmdr.Command{},
        },

        AppName:    "wget-demo",
        Version:    wgetVersion,
        VersionInt: 0x011400,
        Header: `GNU Wget 1.20, a non-interactive network retriever.

Usage: wget [OPTION]... [URL]...

Mandatory arguments to long options are mandatory for short options too.`,
    }
)

rootCmd 包含一個 Command 嵌入結構。而後 rootCmd 包含 AppName, Version, Header 等等頂級宣告。看看 RootCommand 的定義:

type(
    // RootCommand holds some application information
    RootCommand struct {
        Command

        AppName    string
        Version    string
        VersionInt uint32

        Copyright string
        Author    string
        Header    string // using `Header` for header and ignore built with `Copyright` and `Author`, and no usage lines too.

        ow   *bufio.Writer
        oerr *bufio.Writer
    }
)

你能夠編寫本身的 CopyrightAuthor 字段,由 cmdr 爲你構造 app 的 header 部分。你也能夠單純指定 Header 字段讓 cmdr 原樣輸出。

爲了復刻的更像一點,wget-demo 定製了 Header 字段。

此外,wget 的分組的參數選項,咱們選擇實現了前三組,所以你能看到 line 6-9 使用了一個 append 嵌套組合這三組參數集定義。

Command

rootCmd 包含一個 Command 嵌入結構,其定義爲:

type(
    // BaseOpt is base of `Command`, `Flag`
    BaseOpt struct {
        Name string
        // single char. example for flag: "a" -> "-a"
        // Short rune.
        Short string
        // word string. example for flag: "addr" -> "--addr"
        Full string
        // more synonyms
        Aliases []string
        // group name
        Group string
        // to-do: Toggle Group
        ToggleGroup string

        owner  *Command
        strHit string

        Flags []*Flag

        Description             string
        LongDescription         string
        Examples                string
        Hidden                  bool
        DefaultValuePlaceholder string

        // Deprecated is a version string just like '0.5.9', that means this command/flag was/will be deprecated since `v0.5.9`.
        Deprecated string

        // Action is callback for the last recognized command/sub-command.
        // return: ErrShouldBeStopException will break the following flow and exit right now
        // cmd 是 flag 被識別時已經獲得的子命令
        Action func(cmd *Command, args []string) (err error)
    }
    
    // Command holds the structure of commands and subcommands
    Command struct {
        BaseOpt
        SubCommands []*Command
        // return: ErrShouldBeStopException will break the following flow and exit right now
        PreAction func(cmd *Command, args []string) (err error)
        // PostAction will be run after Action() invoked.
        PostAction func(cmd *Command, args []string)
        // be shown at tail of command usages line. Such as for TailPlaceHolder="<host-fqdn> <ipv4/6>":
        // austr dns add <host-fqdn> <ipv4/6> [Options] [Parent/Global Options]
        TailPlaceHolder string

        root            *RootCommand
        allCmds         map[string]map[string]*Command // key1: Commnad.Group, key2: Command.Full
        allFlags        map[string]map[string]*Flag    // key1: Command.Flags[#].Group, key2: Command.Flags[#].Full
        plainCmds       map[string]*Command
        plainShortFlags map[string]*Flag
        plainLongFlags  map[string]*Flag
    }
)
Name

Name 暫時沒有什麼用處,目前你老是能夠忽略它。未來,它可能被更好地用在文檔輸出方面。

Short, Full, Aliases

Short, Full, Aliases 無需再特別說明了,只是再強調一次,在上級命令的全部子命令中,它們不能重複。在多級子命令結構的不一樣層級中,沒有這個限制,你能夠比較寬泛地定義本身的命令和子命令集合。

PreAction, Action, PostAction

當命令被識別出來時,PreAction 被當即執行,此時,cmd.GetHitStr() 能夠得到被命中的命令行參數中的命令字符串。你能夠在這裏創建 PreAction 邏輯,當特定條件不知足時,你的邏輯能夠返回 cmdr.ErrShouldBeStopException 來通知當即退出。

ActionPostAction 的用法應該很明確,這裏就不展開了。你對命令的實現邏輯一般應該老是利用 Action 字段來完成。

Command 的函數

Command 也包含一些相似於 GetHitStr() 的函數:

  • PrintHelp(justFlags bool):輸出幫助屏。
  • PrintVersion():輸出版本信息屏。
  • GetRoot() 直接訪問到 rootCmd;若是想逐級回溯,經過 Owner 字段就能夠了。
  • IsRoot() 幫助你測試是否到達了頂級命令。
  • HasParent() 幫助你測試是否還有 Owner/Parent。
  • ...
Group

Group 字段被用於命令分組。相同的字符串會被組織爲一個命令組,顯示的效果像這樣:

若是你不指定Group,那麼它們會被自動歸屬於一個名爲 cmdr.UnsortedGroup 的特殊組中,圖示中的 ms, s, t 都是這樣的未指定分組,它們不會有組標題輸出,並且老是被做爲第一個被輸出的分組。

若是你想要歸屬到 「Misc」 分組,那麼你能夠指定 Group 字段爲 cmdr.SysMgmtGroup,其特殊之處在於老是被最後輸出(v0.2.11及前可能存在不一樣的表現,下一版本會予以確認,但想要最後輸出也很容易,稍後描述)。

對於分組誰先誰後,實際上有一個方案:指定你的Group字符串時使用兩段結構「a.b」。a被用於排序,你可使用字母和數字,例如:「001」,「011」,「091」等等。又或者:「A01」,「B01"等等。b被用做分組名並被用於顯示。

ToggleGroup

ToggleGroup暫未實現,由於其功能能夠暫時使用 PreAction 來代替。

since 0.2.13,ToggleGroup 已被移出 BaseOpt 結構,移入 Flag 中。

since 0.2.15 (待發布),ToggleGroup 已被實現。

Description,LongDescription

DescriptionLongDescription,是命令的描述性文字。你必須提供 Description 字段,在上面的圖示中,它被顯示在命令的後半段。若是你提供了 LongDescription ,它將會在命令的 --help 屏中被顯示,另外,在 man page 或者文檔輸出中,LongDescription 也會被輸出以便更細緻地進行描述。

Examples

Examples 是命令的用例。實際上咱們限定了用例的格式:

Examples:`
$ {{.AppName}} start
                    make program running as a daemon background.
$ {{.AppName}} start --foreground
                    make program running in current tty foreground.
$ {{.AppName}} run
                    make program running in current tty foreground.
$ {{.AppName}} stop
                    stop daemonized program.
$ {{.AppName}} reload
                    send signal to trigger program reload its configurations.
$ {{.AppName}} status
                    display the daemonized program running status.
$ {{.AppName}} install [--systemd]
                    install program as a systemd service.
$ {{.AppName}} uninstall
                    remove the installed systemd service.
`,

你必須按上述格式來提供 Examples 的具體內容。第一行以 $ {{.AppName}} 開頭,而後是你的命令,若是是多級下的子命令,請注意補全,例如 $ {{.AppName}} ms tags list。而後第二行爲上一行命令的功能性描述,不建議描述太冗長,也不建議描述被切分到多行。如是重複。

這樣作的緣由是爲了在 man page 和文檔輸出時 cmdr 可以重組 examples 部分的格式令其更視覺化。

這是一個 man page 的部分截圖,咱們能夠令其更視覺化,幫助最終使用者。

Hidden

若是你不想命令被顯示在幫助屏、man page、文檔中,使用 Hidden 字段來隱藏它。

Deprecated

若是你計劃在下一某個版本廢棄某個命令,可使用 Deprecated 字段來標識它,你應該提供一個語義化的版本號到 Deprecated 中,至少在 Markdown 的文檔輸出中,它會被顯示爲刪除線樣式。

在 Terminal 中,deprecated 的命令顯示爲暗色。

DefaultValuePlaceholder, DefaultValue
適用於 Flag,不適用於 Command

DefaultValuePlaceholder 字段提供一個字符串 X,X 被鏈接在長參數以後用於顯示目的,例如:--config=FILE。這是爲了讓參數的用法更具備表義性,也是爲了強調參數爲帶值的。

注意爲了提醒 cmdr 你須要一個帶值參數,你必須明確設定 DefaultValue 字段爲一個特定數據類型的值。你可使用 string, int, string slice, int slice, duration 做爲默認值。

若是是不帶值的參數,它們老是具備 bool 類型的隱含值。若是你不指定 DefaultValue,那麼 cmdr 認爲你須要的是一個 bool 類型的不帶值參數。

若是你在提供命令行參數是使用逗號分隔的字符串,並且爲 DefaultValue 設定了 string slice, int slice 的話,那麼 cmdr 會識別到並切分字符串轉義爲 Slice。稍後你在 Action 中可使用 cmdr.GetStringSlice() 等方式直接抽取到數組。

DefaultValue 字段決定了 該參數的值的存儲方式。但你能夠自由地抽取該參數值到不一樣的數據類型,你能夠經過 Get() 抽出該參數值的內部存儲,而後自行轉義爲想要的類型。

since 0.2.13,DefaultValuePlaceholder 已被移出 BaseOpt 結構,移入 Flag 中。
Flags
since 0.2.13,Flags 已被移出 BaseOpt 結構,移入 Command 中。

命令的參數集被定義於此。

SubCommands

對於命令來講,多級命令可以構成一個結構化的層次,不只便於用戶索引和記憶,也有利於業務邏輯的構建和編寫。

嵌套多級的子命令可能會很冗長,所以實際編碼過程當中,你能夠考慮拆分並獨立定義子命令,並在父命令中組合它們。

TailPlaceHolder

對於命令來講,在 Usage 行的顯示也須要被 meaningful。若是你有這樣的須要,那麼 TailPlaceHolder 字段能夠在 Usage 行的正常輸出以外額外嵌入一段文字。

對於 TailPlaceHolder="<host-fqdn> <ipv4/6>" 來講,顯示的效果是這樣的:

應該不須要更多解釋了,這個用文字表達我須要首先給出一堆術語釋義才行,就不騙字數了。

Flag

參數,選項,都是 Flag 的同義語。cmdr 在代碼實現時選用了 Flag 這個單詞而已。

除了在 Command 中已經描述過的 術語二者都有的字段以外,這一小節描述其它部分,尤爲是 Flag 特有的部分:

ToggleGroup

參考 Command 中有關小節的描述。雖未實現,但這個字段能夠乾點什麼,未來吧

DefaultValuePlaceholder

參考 Command 中有關小節的描述。自 cmdr v0.2.13 起,通過代碼 review,這個字段正式移入 Flag 中,由於這纔是正確的邏輯歸屬點。

DefaultValue

參考 Command 中有關小節的描述。嗯,它原本就設計在 Flag 中,難怪之前寫 demo 時感受怪怪的,DefaultValuePlaceholder 寫在一處,DefaultValue 又寫在另外一處。從此就是一家人了。

ValidArgs

還沒有實現。暫時也沒考慮。原來的意圖是提供枚舉文字量。但是你們都是寫代碼的,不如就 1,2,3 將就了吧先。

Required

未用。實際上 cmdr 沒有校驗的概念,也沒有必須存在這種概念。

由於咱們以爲,你不該該要求用戶必定要提供一個什麼。

好比 consul 集羣在哪裏呀?consul 集羣固然是在 consul.ops.local 那兒啊,要否則大家家雲設施架構師設計的不同,那麼它就在 registrar.prod.ashiley.org.local 啊。換句話說,你老是應該給參數一個默認值,甚至給它 nil 或者 」「 也能夠,你的業務邏輯應該處理一下這些臨界場景。

儘管咱們設計了 cmdr 以幫助你創建完善的 Command Line UI,但讓用戶隨時隨地能省缺就省缺纔是正確的。

ExternalTool

這個字段的用途,首先是實現 git commit -m 效果。

爲了達到效果,你必須在 ExternalTool 中填寫 」EDITOR「 字符串,又或者使用 cmdr.ExternalToolEditor 常量。

本質上,cmdrExternalTool 視爲環境變量名,試圖探查環境變量是否是存在,並取得該值做爲執行文件X,而後採用一個臨時文件T做爲執行文件X的輸入參數並就地執行它們,待用戶操做完畢並關閉執行文件X以後,臨時文件T的內容被當作文本並被做爲選項值填入。

因此,git commit -m 就是這麼幹的,cmdr 複製了這個流程。若是你須要相似的邏輯,那麼就能夠藉助於 ExternalTool 字段。

組織

依據上面各小節的對 RootCommand,Command,Flag的闡述,接下來就是具體的數據集的定義了。

咱們已經提到過嵌套結構的煩惱並作出了建議,至於更好的數據集定義方案,繼續改善吧,歡迎給我建議。

小結

那麼如今,你已經能夠構建出你的 Command Line UI 了。wget-demo 已經實現了三組參數集,不但可以被正確識別,顯示的效果也還不錯:

若是但願對命令行參數的解釋和操做有更多便利,歡迎 Issue 到:

https://github.com/hedzr/cmdr

REF

相關文章
相關標籤/搜索