使用 Go 内置模板构建丰富的 CLI 应用程序
争做团队核心程序员,关注「幽鬼」
大家好,我是程序员幽鬼。
今天介绍如何通过 text/template 构建富文本 CLI 程序。
01 概述
模板包 text/template[1] 实现了用于生成文本输出的数据驱动模板。虽然我们不会从多次执行模板输出中受益,但我们发现它易于使用且有助于输出带颜色的文本、编码数据和呈现表格信息。
通过按名称映射附加函数,可以使用更多功能扩展模板引擎。函数可以接受来自模板引擎的输入作为参数并返回将呈现到输出中的值。
映射应该在我们调用模板Parse
函数功能之前完成 。
为了说明如何做到这一点,让我们看一下以下示例:
rand.Seed(time.Now().UnixNano())
tmpl := template.Must(template.
New("").
Funcs(map[string]interface{}{
"rand": func() int {
return rand.Intn(100)
},
}).
Parse(`Hi {{.}}, you are number {{rand}}.`))
_ = tmpl.Execute(os.Stdout, "User")
输出:
Hi User, you are number 6.
在这种情况下,我们将rand
函数映射到一个返回 0 到 100(不包括)之间随机数的 rand.Intn
函数。模板引擎将接受返回值并将其字符串化为输出。
模板可以接受的函数应该有一个有效的名称,可以用作模板的一部分(由字母和数字或下划线组成,不应以数字开头)并返回值类型或带 error 的值。
现在让我们看一下 CLI 中的一些用例。
02 彩色输出
为了输出彩色文本,我们使用 go-pretty[2] 包。
使用此包的好处之一是它提供的彩色文本功能以及完全禁用/启用颜色支持的选项。
让我们将一些颜色映射到我们的模板
data := struct {
Passed int
Failed int
}{
Passed: 1,
Failed: 5,
}
tmpl := template.Must(template.
New("").
Funcs(map[string]interface{}{
"red": func(v interface{}) string {
return text.FgHiRed.Sprint(v)
},
"green": func(v interface{}) string {
return text.FgHiGreen.Sprint(v)
},
"yellow": func(v interface{}) string {
return text.FgHiYellow.Sprint(v)
},
}).
Parse(`{{ "Results" | yellow }}
Passed: {{ .Passed | green }}
Failed: {{ .Failed | red }}
`))
_ = tmpl.Execute(os.Stdout, data)
使用 term[3] 包,我们可以检测我们的输出是否进入终端。虽然用户希望在终端上看到带有颜色的输出,但将输出重定向或通过另一个命令进行管道处理,会不喜欢处理产生的颜色转义码。
通过检查终端,我们可以禁用包上的颜色go-pretty
,运行上面的代码将产生相同的但只有文本输出:
if !term.IsTerminal(int(os.Stdout.Fd())) {
text.DisableColors()
}
03 数据为 JSON
一个常见的用例是需要打印出数据模型——配置、服务器响应或其他复杂的结构。JSON 通常用于此目的。
我们可以通过在映射中添加一个 json
函数来轻松呈现我们的输出:
data := struct {
ID int `json:"id"`
UpdateTime time.Time `json:"update_time"`
Path string `json:"path,omitempty"`
}{
ID: 1,
UpdateTime: time.Now(),
Path: "path/to/data",
}
tmpl := template.Must(template.
New("").
Funcs(map[string]interface{}{
"json": func(v interface{}) (string, error) {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {return "", err}
return string(b), nil
},
}).
Parse(`Record information {{ . | json }}`))
_ = tmpl.Execute(os.Stdout, data)
Record information {
"id": 1,
"update_time": "2021-10-18T21:18:25.973140953+03:00",
"path": "path/to/data"
}
请注意,这种情况下的映射函数也会返回错误。如果我们的调用失败,Execute
方法将返回错误MarshalIndent
。
04 表
以表格格式处理和显示数据很常见。我们将提供给模板的函数使用了一个通用的数据结构来呈现表格。
// type table.Row interface{} - holds any value
type Table struct {
Headers table.Row
Rows []table.Row
}
该 Table
结构由标题和行组成,用于对任何表格信息进行建模。
标题的数量反映了每行中的单元格数量(我们假设它是对齐的)。
就像我们对彩色输出所做的那样,将表格渲染到终端与将其渲染到文件不同。
根据 isTerminal 的值(假设是通过 term.IsTerminal 设置的),我们会渲染出对齐的表格或者 CSV 格式,以便于处理。
tmpl := template.Must(template.
New("").
Funcs(map[string]interface{}{
"table": func(tab *Table) string {
w := table.NewWriter()
w.AppendHeader(tab.Headers)
w.AppendRows(tab.Rows)
if isTerminal {
return w.Render()
}
return w.RenderCSV()
},
}).
Parse(`{{ . | table }}`))
tbl := &Table{
Headers: table.Row{"id", "path"},
Rows: []table.Row{{1, "file1"}, {2, "file2"}, {3, "file3"}},
}
_ = tmpl.Execute(os.Stdout, tbl)
终端输出:
+----+-------+
| ID | PATH |
+----+-------+
| 1 | file1 |
| 2 | file2 |
| 3 | file3 |
+----+-------+
非终端输出:
id,path
1,file1
2,file2
3,file3
05 其他想法
review 代码总会提出如何改进代码或使其更易于使用的想法。例如:
使用 reflect 对表格信息建模——标签或附加结构来描述模型。如果不需要,将阻止转换 Table
。使用连续数据源呈现表格——提取信息的 API 通常依赖于分页。使用反射来渲染数据并捕获我们拉取数据的方式,我们可以有一个通用的方式来连续获取和显示数据。
原文链接:https://lakefs.io/building-rich-cli-applications-with-gos-built-in-templating/
参考资料
[1]text/template: https://pkg.go.dev/text/template
[2]go-pretty: https://github.com/jedib0t/go-pretty
[3]term: https://pkg.go.dev/golang.org/x/term
往期推荐
欢迎关注「幽鬼」,像她一样做团队的核心。