Golang 基於 flag 庫實現一個簡單命令行工具

前言

Golang 標準庫中的 flag 庫提供瞭解析命令行選項的能力,我們可以基於此來開發命令行工具。

假設我們想做一個命令行工具,我們通過參數提供【城市】,它自動能夠返回當前這個【城市】的天氣狀況。這樣一個簡單的需求,今天我們就來試一下,看怎樣實現。

flag 庫

Package flag implements command-line flag parsing.

flag 庫 能夠支持基礎的命令行 flag 解析。使用起來並不復雜,

我們可以針對 string, integer, bool 三種類型來定義 flag,如:flag.String(), Bool(), Int()。

比如下面這樣,我們就定義瞭一個 -n 的選項,默認值為 1234, 提示信息為 help message for flag n。返回值是一個 int 的指針。

import "flag"
var nFlag = flag.Int("n", 1234, "help message for flag n")

當然,我們也可以直接將 flag 和變量綁定,這裡要在上面三種方法的前面加上 Var 即可:

var flagvar int
func init() {
	flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
}

區別隻在於首個參數是個指針,直接賦值,而不是 return 回來。簽名都是類似的,我們看一個 Int64Var:

在所有 flag 都定義好之後,我們調用 flag.Parse() 方法,將命令行數據解析到對應的 flag 中。這之後就可以直接用瞭:

fmt.Println("ip has value ", *ip)
fmt.Println("flagvar has value ", flagvar)

有時候我們不止是一個簡單的 flag,還需要參數,這個時候我們就可以用 flag.Args() 拿到一個 slice,或者直接 flag.Arg(i) 來拿指定參數,下標從 0 開始。

不熟悉的同學建議多看看看 go by example 的示例,講的很清楚。

從開發者的角度看,其實我們隻要定義好變量,用 flag.XXVar 來綁定,最後 flag.Parse 就可以用:

package main

import (
  "fmt"
  "flag"
)
var (
  intflag int
  boolflag bool
  stringflag string
)
func init() {
  flag.IntVar(&intflag, "intflag", 0, "int flag value")
  flag.BoolVar(&boolflag, "boolflag", false, "bool flag value")
  flag.StringVar(&stringflag, "stringflag", "default", "string flag value")
}
func main() {
  flag.Parse()

  fmt.Println("int flag:", intflag)
  fmt.Println("bool flag:", boolflag)
  fmt.Println("string flag:", stringflag)
}

編譯之後我們運行一下看看

$ ./main -intflag 12 -boolflag 1 -stringflag test

int flag: 12
bool flag: true
string flag: test

如果沒有設置 flag 的值,會取我們設置的默認值。

flag 支持的解析類型有下面四種:

  • -flag
  • –flag
  • -flag=x
  • -flag x (bool 不能用這個)

有時候我們隻需要一個 flag 就夠瞭,選項本身就帶著含義,不需要參數。而有些時候我們既需要 flag,也需要參數。註意區分好場景即可。如果用瞭第一種和第二種這種不帶參數的,本質含義就是個 bool,出現就是 true,不出現就看默認值。

FlagSet

FlagSet 就是一組 flag 定義的集合,在 flag 庫的底層是一個結構體:

type FlagSet struct {
	// Usage is the function called when an error occurs while parsing flags.
	// The field is a function (not a method) that may be changed to point to
	// a custom error handler. What happens after Usage is called depends
	// on the ErrorHandling setting; for the command line, this defaults
	// to ExitOnError, which exits the program after calling Usage.
	Usage func()
	// contains filtered or unexported fields
}

註意有一個 Usage 函數,當解析 flag 出問題時就會調用這個。前面 flag 庫封裝的那些能力底層都是共用同一個默認的 CommandLine FlagSet實現的:

// src/flag/flag.go
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func Parse() {
  CommandLine.Parse(os.Args[1:])
}

func IntVar(p *int, name string, value int, usage string) {
  CommandLine.Var(newIntValue(value, p), name, usage)
}

func Int(name string, value int, usage string) *int {
  return CommandLine.Int(name, value, usage)
}

func NFlag() int { return len(CommandLine.actual) }

func Arg(i int) string {
  return CommandLine.Arg(i)
}

func NArg() int { return len(CommandLine.args) }

當我們調用 NewFlagSet 時需要指定這個集合的名稱以及對應的錯誤處理。

第二個參數這個錯誤處理有三種選項,flag 已經提供:

  • ContinueOnError:發生錯誤後繼續解析,CommandLine就是使用這個選項;
  • ExitOnError:出錯時調用os.Exit(2)退出程序;
  • PanicOnError:出錯時產生 panic。

需求拆解

我們的需求很簡單,提供一個 weather flag,接受輸入的城市名稱,隨後我們返回天氣數據即可。

所以,從 flag 的角度看,這裡並不復雜,我們將【城市名稱】綁定到一個 flag 上即可。

關鍵是要實現基於城市名稱查天氣的能力。這個也有公開的網站能實現,參照此前 Golang 教程中給出的 wttr 就可以。大傢可以試一下,訪問 wttr.in/wuhan 將會展示【武漢】的天氣預報:

這裡其實比較 trick,由於是網站,並不是公開的 open api,所以返回的數據也是 html 格式的,我們要思考一下怎麼在命令行展示。

下面我們一步步來解決。

實現 weather flag

這一步基本是復用 flag 包提供的能力,這裡我們用 StringVar 從命令行拿到值之後寫入變量,這裡相對比較通用,大傢以後有需求可以直接改一下即可:

package main
import (
    "flag"
    "fmt"
    "os"
)
type arguments struct {
	weatherCity string
}
func (a *arguments) parseArgs(args []string) error {
	f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)

	f.StringVar(&a.weatherCity, "weather", "", "check weather")

	f.Usage = func() {
		fmt.Fprintf(os.Stderr, `flags: %s`, os.Args[0])
		f.PrintDefaults()
		os.Exit(1)
	}
	if err := f.Parse(args[1:]); err != nil {
		return err
	}

	return nil
}

func Execute() error {

	args := &arguments{}
	if err := args.parseArgs(os.Args); err != nil {
		fmt.Println(err)
		os.Exit(2)
	}

	// weather
	if args.weatherCity != "" {
                // TODO 實現根據 city 名稱拿天氣,並打印的效果
	}

	return nil
}

最終在 main() 函數中直接調用我們的 Execute 即可,註意我們解析到 weatherCity 不為空時,核心邏輯就在這個分支,我們留瞭個 TODO,下面看看怎麼解。

天氣數據打印

前一節的 TODO 裡本質需要我們實現的簽名很簡單:

func GetWeather(city string) (string, error)

這樣拿到一個用字符串表示的天氣數據,然後回到主流程裡一個 fmt.Printf 就可以解決。

而同時我們也有瞭 wttr 的能力,可以拿到數據,隻不過是 html。關鍵是怎麼轉字符串。我們一步一步來:

獲取源數據

一個簡單的 http.Get 拿到 html 數據即可,這一步不復雜,大傢直接看代碼:

func getWeatherData(city string) string {
    url := "https://wttr.in/" + city
    response, err := http.Get(url)
    if err != nil {
            return "", err
    }
    all, err := ioutil.ReadAll(response.Body)
    if err != nil {
            return "", err
    }
    weather := string(all)
    return weather
}

數據轉換

在開源社區,我們找到瞭 "github.com/JohannesKaufmann/html-to-markdown" 這個庫提供 html 轉換為 markdown 的能力,而 "github.com/MichaelMure/go-term-markdown" 又可以將 markdown 格式轉為可在 terminal 打印的字符串,我們可以通過這兩步來轉換,實現整體的 GetWeather 函數:

package weather
import (
	"io/ioutil"
	"net/http"

	md "github.com/JohannesKaufmann/html-to-markdown"
	markdown "github.com/MichaelMure/go-term-markdown"
)
func GetWeather(city string) (string, error) {
	url := "https://wttr.in/" + city
	response, err := http.Get(url)
	if err != nil {
		return "", err
	}
	all, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return "", err
	}
	weather := string(all)
	md := getMD(weather)
	result := markdown.Render(md, 280, 6)
	return string(result), nil
}

func getMD(html string) string {
	converter := md.NewConverter("", true, nil)
	markdown, err := converter.ConvertString(html)
	if err != nil {
		return ""
	}
	return markdown
}

運行效果

好瞭,現在我們實現瞭兩步,大傢隻需要把主流程裡 TODO 的註釋換成實際對下面 GetWeather 函數的調用即可,我們來看看運行效果。

$ opb -weather beijing

完美,一個展示天氣狀況的命令行工具就做完瞭。這裡的 opb 是我們的 package 名稱,大傢可以自己試一下,包名更換為你喜歡的名稱即可。

小結

其實開源社區各種能力基本都有同學研究過,大傢可以打開思路,碰到知識點就思考如何能落地。筆者也是初學 flag 的時候本著實踐的目的來試一試。正好發現瞭 html => markdown => terminal 打印這條路徑,不一定是最好的,但作為一個 toy tool 足夠瞭。

到此這篇關於Golang 基於 flag 庫實現一個簡單命令行工具的文章就介紹到這瞭,更多相關Golang  flag內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: