<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 語(yǔ)言實(shí)戰(zhàn):命令行(3)CLI 框架

          共 26875字,需瀏覽 54分鐘

           ·

          2021-02-28 20:37

          經(jīng)過(guò)前面兩期的介紹,相信大家已經(jīng)可以寫(xiě)簡(jiǎn)單的命令行程序,并且能夠使用命令行參數(shù)。

          即使遇到一些困難,建立直觀認(rèn)識(shí)和了解關(guān)鍵詞之后,在網(wǎng)絡(luò)上搜索答案也變得相對(duì)容易。

          接下來(lái)介紹 CLI 框架。

          命令行程序的前兩期:

          命令行框架

          對(duì)于簡(jiǎn)單的功能,單個(gè) go 文件,幾個(gè)函數(shù),完全是足夠的。沒(méi)有必要為了像那么回事,硬要分很多個(gè)包,每個(gè)文件就兩行代碼。為了框架而框架,屬于過(guò)早優(yōu)化。

          但反過(guò)來(lái)說(shuō),隨著往項(xiàng)目里不斷添加特性,代碼越來(lái)越多,如何更好地組織代碼,達(dá)到解耦和復(fù)用,就成了必須要考慮的問(wèn)題。

          我們當(dāng)然可以把自己的思考,體現(xiàn)在項(xiàng)目的代碼組織上,乃至從中抽取一套框架。但一個(gè)深思熟慮,適應(yīng)各種場(chǎng)景變化的框架,還是有門(mén)檻、需要技術(shù)和經(jīng)驗(yàn)積累的。

          更便捷的做法,是引入社區(qū)熱門(mén)的框架,利用里面提供的腳手架減少重復(fù)勞動(dòng),并從中學(xué)習(xí)它的設(shè)計(jì)。

          對(duì)于 CLI 程序而言,我知道的最流行的框架有兩個(gè),分別是:

          • urfave/cli:https://github.com/urfave/cli
          • cobra:https://github.com/spf13/cobra

          cobra 的功能會(huì)更強(qiáng)大完善。它的作者 Steve Francia(spf13)是 Google 里面 go 語(yǔ)言的 product lead,同時(shí)也是 gohugo、viper 等知名項(xiàng)目的作者。

          但強(qiáng)大的同時(shí),也意味著框架更大更復(fù)雜,在實(shí)現(xiàn)一些小規(guī)模的工具時(shí),反而會(huì)覺(jué)得殺雞牛刀。所以這里只介紹 cli 這個(gè)框架,有興趣的朋友可以自行了解 cobra ,原理大同小異。

          urfave/cli 框架

          cli 目前已經(jīng)開(kāi)發(fā)到了 v2.0+。推薦使用最新的穩(wěn)定版本。

          這里使用 go module 模式,那么引入 cli 包只需要在代碼開(kāi)頭

          import "github.com/urfave/cli/v2"

          如果還不熟悉 go module,或者不知道最后面的 v2 代表什么,請(qǐng)看這篇文章:《golang 1.13 - module VS package》。

          簡(jiǎn)單說(shuō),go module 使用語(yǔ)義化版本(semver),認(rèn)為主版本號(hào)變更是『不兼容變更(breaking changes)』,需要體現(xiàn)在導(dǎo)入路徑上。v0.x (不穩(wěn)定版本,可以不兼容)和 v1.x (默認(rèn))不需要標(biāo),v2.0 及以上的版本,都需要把主版本號(hào)標(biāo)在 module 路徑的最后。

          但是注意,這個(gè) v2 既不對(duì)應(yīng)實(shí)際的文件目錄,也不影響包名。在這里,包名仍然是 cli

          根據(jù)作者提供的例子,實(shí)現(xiàn)一個(gè)最小的 CLI 程序看看:

          // 為了編譯后不用改名,module name 直接就叫 boom
          package main

          import (
              "fmt"
              "log"
              "os"

              "github.com/urfave/cli/v2"
          )

          func main() {
              app := &cli.App{
                  Name: "boom",
                  Usage: "make an explosive entrance",
                  Action: func(c *cli.Context) error {
                      fmt.Println("boom! I say!")
                      return nil
                  },
              }

              err := app.Run(os.Args)
              if err != nil {
                  log.Fatal(err)
              }
          }

          這段代碼實(shí)現(xiàn)了一個(gè)叫 boom 的程序,執(zhí)行的時(shí)候會(huì)輸出 "boom! I say!":

          >go build
          >boom
          boom! I say!

          另外,框架已經(jīng)自動(dòng)生成了默認(rèn)的幫助信息。在調(diào)用 help 子命令,或者發(fā)生錯(cuò)誤時(shí),會(huì)輸出:

          >boom help
          NAME:
             boom - make an explosive entrance

          USAGE:
             boom.exe [global options] command [command options] [arguments...]

          COMMANDS:
             help, h  Shows a list of commands or help for one command

          GLOBAL OPTIONS:
             --help, -h  show help (default: false)

          這段代碼做的事情很簡(jiǎn)單。初始化一個(gè) cli.App ,設(shè)置三個(gè)字段:

          • 名字,就是 "boom"。
          • 用途,也是一個(gè)字符串,會(huì)在 help 信息用到。
          • 動(dòng)作,也就是執(zhí)行程序時(shí)具體執(zhí)行什么內(nèi)容。這里是輸出一個(gè)字符串。

          運(yùn)行部分,將命令行參數(shù) os.Args 作為參數(shù)傳遞給 cli.AppRun() 方法,框架就會(huì)接管參數(shù)的解析和后續(xù)的命令執(zhí)行。

          如果是跟著教程一路過(guò)來(lái),那么很可能這里是第一次引入第三方包。IDE 可以會(huì)同時(shí)提示好幾個(gè)關(guān)于 "github.com/urfave/cli/v2" 的錯(cuò)誤,例如:"github.com/urfave/cli/v2 is not in your go.mod file" 。

          可以根據(jù) IDE 的提示修復(fù),或者執(zhí)行 go mod tidy ,或者直接等 go build 時(shí)自動(dòng)解決依賴。無(wú)論選擇哪一種,最終都會(huì)往 go.mod 里添加一行 require github.com/urfave/cli/v2

          重構(gòu)

          當(dāng)然,實(shí)現(xiàn)這么簡(jiǎn)單的功能,除了幫忙生成幫助信息,框架也沒(méi)什么用武之地。

          接下來(lái)我們用框架把 gosrot 改造一下,在基本不改變功能的前提下,把 cli 包用上。

          因?yàn)橛辛?cli 包處理參數(shù),我們就不用 flag 包了。(其實(shí) cli 里面用到了 flag 包。)

          func main() {
              app := &cli.App{
                  Name:   "gosort",
                  Usage:  "a simple command line sort tool",
                  Action: sortCmd,
                  Flags: []cli.Flag{
                      &cli.BoolFlag{
                          Name:        "lex",
                          Aliases:     []string{"l"},
                          Usage:       "sort lexically",
                          Destination: &lex,
                      },
                      // unique 同為 BoolFlag,省略,請(qǐng)自行補(bǔ)完
                      // ...
                      &cli.StringFlag{
                          Name:        "from",
                          Aliases:     []string{"f"},
                          // `FILE` 是占位符,在幫助信息中會(huì)輸出 -f FILE    input from FILE
                          // 用戶能更容易理解 FILE 的用途
                          Usage:       "input from `FILE`",
                          Destination: &from,
                      },
                      // 省略剩余的 StringFlag...
                  },
              }

              err := app.Run(os.Args)
              if err != nil {
                  log.Fatal(err)
              }
          }

          cliFlagflag 包類似,有兩種設(shè)置方法。既可以設(shè)置以后通過(guò) cli.Context 的方法讀取值:ctx.Bool("lex")string 等其它類型以此類推)。也可以直接把變量地址設(shè)置到 Destination 字段,解析后直接訪問(wèn)對(duì)應(yīng)的變量。

          這里為減少函數(shù)傳參,用了后者,把參數(shù)值存儲(chǔ)到全局(包級(jí))變量。

          程序入口改為 cli.App 之后,原來(lái)的 main() 函數(shù)就改為 sortCmd ,作為 appAction 字段。

          // 增加 Context 參數(shù) 和返回 error,以滿足 cli.ActionFunc (Action 字段的類型)簽名
          func sortCmd(ctx *cli.Context) error {
              // 不再需要設(shè)置 flag 包
              var strs []string
              if from != "" {
                  if !isFile(from) {
                      return fmt.Errorf("%s is not a file", from)
                  }

                  buf, err := ioutil.ReadFile(from)
                  if err != nil {
                      return fmt.Errorf("read %s fail, caused by\n\t%w", from, err)
                  }

                  // 篇幅關(guān)系,省略... 參考之前兩期的內(nèi)容
              }
              // 省略...

              if output == "" {
                  fmt.Println(res)
              } else {
                  err := ioutil.WriteFile(output, []byte(res), 0666)
                  if err != nil {
                      return fmt.Errorf("write result to %s fail, caused by\n\t%w", output, err)
                  }
              }
              return nil
          }

          由于程序被封裝成了 cli.App ,程序的執(zhí)行交給框架處理, sortCmd 內(nèi)部不再自行調(diào)用 os.Exit(1) 退出,而是通過(guò)返回 error 類型,將錯(cuò)誤信息傳遞給上層處理。

          這里主要使用 fmt.Errorf() 格式化錯(cuò)誤信息然后返回。從 1.13 開(kāi)始,fmt.Errorf() 提供了一個(gè)新的格式化動(dòng)詞 %w ,允許將底層的錯(cuò)誤信息,包裝在新的錯(cuò)誤信息里面,形成錯(cuò)誤信息鏈。后續(xù)可以通過(guò) errors 包的三個(gè)函數(shù) Is() , As()Unwrap() ,對(duì)錯(cuò)誤信息進(jìn)行進(jìn)一步分析處理。

          接下來(lái)編譯執(zhí)行

          >go build
          # 不同參數(shù)的含義參考上一期內(nèi)容
          >gosort -h
          NAME:
             gosort - a simple command line sort tool

          USAGE:
             gosort [global options] command [command options] [arguments...]

          COMMANDS:
             help, h  Shows a list of commands or help for one command

          GLOBAL OPTIONS:
             --lex, -l                 sort lexically (default: false)
             --unique, -u              remove duplicates (default: false)
             --from FILE, -f FILE      input from FILE
             --output FILE, -o FILE    output to FILE
             --insep value, -i value   input seperator
             --outSep value, -s value  output seperator (default: ",")
             --help, -h                show help (default: false)
          >gosort -u -i=, -s=- 111,111,555,678,333,567,678
          111-333-555-567-678

          如果完全照著教程的思路重構(gòu),到這一步,你可能會(huì)發(fā)現(xiàn),代碼可以編譯和運(yùn)行,卻沒(méi)有輸出。這是因?yàn)橛幸粋€(gè)地方很容易忘記修改。 請(qǐng)嘗試自行找到問(wèn)題所在,并解決。

          另起爐灶

          框架除了解析參數(shù),自動(dòng)生成規(guī)范的幫助信息,還有一個(gè)主要的作用,是子命令(subcommand)的組織和管理。

          gosort 主要圍繞一個(gè)目的(提交號(hào)的排序去重)展開(kāi),各項(xiàng)功能是組合而不是并列的關(guān)系,更適合作為參數(shù),而不是拆分成多個(gè)子命令。而且之前的開(kāi)發(fā)容易形成思維定勢(shì),下面我們另舉一例,不在 gosort 基礎(chǔ)上修改。

          為了容易理解,接下來(lái)用大家比較熟悉的 git 做例子。篇幅關(guān)系,只展示項(xiàng)目可能的結(jié)構(gòu),不(可能)涉及具體的代碼實(shí)現(xiàn)。

          首先,我們看一下 git 有哪些命令:

          >git help
          usage: git [--version] [--help] [-C <path>] [-c name=value]
                     [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
                     [-p | --paginate | --no-pager] [--no-replace-objects] [--bare]
                     [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
                     <command> [<args>]

          These are common Git commands used in various situations:

          start a working area (see also: git help tutorial)
             clone      Clone a repository into a new directory
             init       Create an empty Git repository or reinitialize an existing one

          work on the current change (see also: git help everyday)
             add        Add file contents to the index
             mv         Move or rename a file, a directory, or a symlink
          // 篇幅關(guān)系,省略余下內(nèi)容,你可以自己嘗試執(zhí)行 git help 查看

          總的來(lái)說(shuō),就是有一系列的全局選項(xiàng)(global options,跟在 git 后面,command 之前),一系列子命令(subcommand),每個(gè)命令下面還有一些專屬的參數(shù)。

          這樣的工具,有幾個(gè)特點(diǎn):

          • 功能強(qiáng)大,子功能很多,無(wú)法用一個(gè)命令 + 若干參數(shù)完成,一般實(shí)現(xiàn)為多個(gè)子命令。
          • 既有影響多數(shù)子命令的全局選項(xiàng),也有某些子命令專屬的選項(xiàng)。
          • 子命令之間,既相互獨(dú)立,又共享一部分底層實(shí)現(xiàn)。

          為了更好地組織程序,項(xiàng)目結(jié)構(gòu)可以是這樣子的:

          │   go.mod
          │   go.sum
          │   main.go

          ├───cmd
          │       add.go
          │       clone.go
          │       common.go
          │       init.go
          │       mv.go
          |       ......

          └───pkg
              ├───hash
              │       hash.go
              │
              ├───zip
              |       zip.go
              │
              ├───......

          main.go 是程序入口,為了保持結(jié)構(gòu)清晰,這里只是初始化并運(yùn)行 cli.App

          package main

          import (
              "log"
              "mygit/cmd"
              "os"

              "github.com/urfave/cli/v2"
          )

          func main() {
              app := &cli.App{
                  Name:                   "mygit",
                  Usage:                  "a free and open source distributed version control system",
                  Version:                "v0.0.1",
                  UseShortOptionHandling: true,
                  Flags:                  cmd.GlobalOptions,
                  // Before 在任意命令執(zhí)行前執(zhí)行,這里用來(lái)處理全局選項(xiàng)
                  Before: cmd.LoadGlobalOptions,
                  // 同理,也可以定義 After 來(lái)執(zhí)行收尾操作
                     // After: xxx
                  Commands: cmd.Commands,
              }

              err := app.Run(os.Args)
              if err != nil && err != cmd.ErrPrintAndExit {
                  log.Fatal(err)
              }
          }

          具體的代碼實(shí)現(xiàn)放到 cmd 包,基本上一個(gè)子命令對(duì)應(yīng)一個(gè)源文件,代碼查找起來(lái)非常清晰。

          common.go 存放 cmd 包的公共內(nèi)容:

          package cmd

          import (
              "errors"
              "fmt"

              "github.com/urfave/cli/v2"
          )

          // Commands 將子命令統(tǒng)一暴露給 main 包
          var Commands = []*cli.Command{
              cloneCmd,
              initCmd,
              addCmd,
              mvCmd,
              // more subcommands ...
          }

          // GlobalOptions 將全局選項(xiàng)暴露給 main 包
          var GlobalOptions = []cli.Flag{
              &cli.PathFlag{
                  Name:  "C",
                  Usage: "Run as if git was started in `path` instead of the current working directory",
              },
              &cli.PathFlag{
                  Name:  "exec-path",
                  Usage: "`path` to wherever your core Git programs are installed",
              },
              &cli.BoolFlag{
                  Name:  "html-path",
                  Usage: "Print the path, without trailing slash, where Git’s HTML documentation is installed and exit",
              },
              // 省略 man-path, info-path, paginate, no-pager...
              // more ...
          }

          // ErrPrintAndExit 表示遇到需要打印信息并提前退出的情形,不需要打印錯(cuò)誤信息
          var ErrPrintAndExit = errors.New("print and exit")

          // LoadGlobalOptions 加載全局選項(xiàng)
          var LoadGlobalOptions = func(ctx *cli.Context) error {
              // 并非實(shí)際實(shí)現(xiàn),所以遇到對(duì)應(yīng)的參數(shù)只是輸出信息,方便觀察
              // 全局選項(xiàng)既可以在這里讀取并設(shè)置全局狀態(tài)(如有)
              // 也可以在具體實(shí)現(xiàn)處再通過(guò) ctx 讀取(參考 add)
              if ctx.IsSet("C") {
                  fmt.Println("started path changed to", ctx.Path("C"))
              }
              // 省略 exec-path ...
              if ctx.Bool("html-path") {
                  fmt.Println("html-path is xxx")
                  return ErrPrintAndExit
              }
              // 省略 man-path, info-path ...
              if ctx.Bool("paginate") || !ctx.Bool("no-pager") {
                  fmt.Println("pipe output into pager like less")
              } else {
                  fmt.Println("no pager")
              }
              return nil
          }

          // 子命令分組
          const (
              cmdGroupStart = "start a working area"
              cmdGroupWork  = "work on current change"
              // ...
          )

          除了業(yè)務(wù)相關(guān)的公共邏輯放在 common.go,還有一些業(yè)務(wù)中立的底層公共類庫(kù),就可以放在 pkg 下面,例如 hash.go

          package hash

          // MyHash 返回 source 的 hash 結(jié)果
          func MyHash(source string) string {
              // 這是一個(gè)假的實(shí)現(xiàn)
              return "hash of " + source
          }

          看一下其中一個(gè)子命令 add 的代碼:

          package cmd

          import (
              "fmt"
              "mygit/pkg/hash"

              "github.com/urfave/cli/v2"
          )

          var addCmd = &cli.Command{
              Name:     "add",
              Usage:    "Add file contents to the index",
              Category: cmdGroupWork, // 子命令分組
              Flags: []cli.Flag{
                  &cli.BoolFlag{
                      Name:    "verbose",
                      Aliases: []string{"v"},
                      Usage:   "Be verbose",
                  },
                  &cli.BoolFlag{
                      Name:    "force",
                      Aliases: []string{"f"},
                      Usage:   "Allow adding otherwise ignored files",
                  },
                  // more options ...
              },
              Action: func(ctx *cli.Context) error {
                  // 僅輸出信息,查看效果,不是真實(shí)實(shí)現(xiàn)

                  // 這里也能讀取全局選項(xiàng)
                  if ctx.IsSet("C") {
                      // do something
                  }
                  items := ctx.Args().Slice()
                  if ctx.Bool("verbose") {
                      for _, item := range items {
                          fmt.Println("add", item, ", hash is [", hash.MyHash(item), "]")
                      }
                  }
                  fmt.Println("add", items, "successfully.")
                  return nil
              },
          }

          擁有相同 Category 字段的命令會(huì)自動(dòng)分組。這里在 common.go 預(yù)定義了一系列的分組,然后直接引用。之所以不是直接用字面量,是因?yàn)樵诙嗵幰米置媪浚浅H菀壮鲥e(cuò),也不利于后續(xù)修改。

          舉例說(shuō),如果不小心在組名里輸入多了一個(gè) "s" ,就會(huì)變成下面這樣:

          COMMANDS:
             help, h  Shows a list of commands or help for one command
             start a working area:
               clone  Clone a repository into a new directory
               init   Create an empty Git repository or reinitialize an existing one
             work on current change:
               add  Add file contents to the index
             work on current changes:
               mv  Move or rename a file, a directory, or a symlink

          好了,一個(gè)連低仿都不算的 git 算是搭出一個(gè)空架子,編譯執(zhí)行看看:

          >go build
          # help 命令和 --help, --version 框架會(huì)自動(dòng)添加,如果不需要可以通過(guò)特定的字段關(guān)閉
          >mygit help
          pipe output into pager like less
          NAME:
             mygit - a free and open source distributed version control system

          USAGE:
             mygit [global options] command [command options] [arguments...]

          VERSION:
             v0.0.1

          COMMANDS:
             help, h  Shows a list of commands or help for one command
             start a working area:
               clone  Clone a repository into a new directory
               init   Create an empty Git repository or reinitialize an existing one
             work on current change:
               add  Add file contents to the index
               mv   Move or rename a file, a directory, or a symlink

          GLOBAL OPTIONS:
             -C path           Run as if git was started in path instead of the current working directory
             --exec-path path  path to wherever your core Git programs are installed
             --html-path       Print the path, without trailing slash, where Git’s HTML documentation is installed and exit (default: false)
             --man-path        Print the manpath (see man(1)) for the man pages for this version of Git and exit (default: false)
             --info-path       Print the path where the Info files documenting this version of Git are installed and exit (default: false)
             --paginate, -p    Pipe all output into less (or if set$PAGERif standard output is a terminal (default: false)
             --no-pager        Do not pipe Git output into a pager (default: false)
             --help, -h        show help (default: false)
             --version, -v     print the version (default: false)


          # help 命令連子命令的幫助信息也自動(dòng)生成了
          >mygit help add
          pipe output into pager like less
          NAME:
             mygit add - Add file contents to the index

          USAGE:
             mygit add [command options] [arguments...]

          CATEGORY:
             work on current change

          OPTIONS:
             --verbose, -v  Be verbose (default: false)
             --force, -f    Allow adding otherwise ignored files (default: false)


          >mygit -C here add a b c
          started path changed to here
          pipe output into pager like less
          started path changed to here
          add [a b c] successfully.


          >mygit add -v a b c
          pipe output into pager like less
          add a , hash is [ hash of a ]
          add b , hash is [ hash of b ]
          add c , hash is [ hash of c ]
          add [a b c] successfully.

          光看幫助信息是不是感覺(jué)還挺像回事。

          希望通過(guò)這個(gè)粗糙的例子,能讓大家對(duì) urfave/cli 這個(gè)框架建立一點(diǎn)直觀的印象。

          更多的例子、更詳細(xì)的字段用法,可以參考

          • 項(xiàng)目主頁(yè):https://github.com/urfave/cli
          • 文檔:https://pkg.go.dev/github.com/urfave/cli/v2

          最后

          在實(shí)際寫(xiě)過(guò)幾個(gè) go 程序之后,相信大家對(duì)于 go 已經(jīng)有一些直觀的認(rèn)識(shí)。與此同時(shí),前面只介紹了很少一部分語(yǔ)言特性,在實(shí)際編程中可能會(huì)產(chǎn)生各種疑惑。


          推薦閱讀


          福利

          我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 165
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久这里只有 | 亚洲熟女毛片 | 嫩操视频在线观看 | 日韩黄色视频毛片 | 欧美黄片小视频 |