Go命令行开发
命令行接受输入,进行处理,然后展示结果。输入分交互模式和非交互模式。
这里有三件事:
1. 命令行输入,
2. 结果展示,
3. 交互模式处理。
这三件事不难,社区有很多包都在做。这反倒使选择变得困难了。
本文选的三个包:
1. cobra,
2. tablewriter,
3. liner
以下分别介绍。
命令行输入
命令行的输入包括:
1. 主命令,
2. 子命令,
3. 命令参数,
4. 命令选项。
主命令
用cobra写一个主命令,大致如下:
package main import ( "os" "github.com/spf13/cobra" ) var ( Root = &cobra.Command{ Use: "root", RunE: func(cmd *cobra.Command, args []string) (err error) { return }, } ) func main() { if err := Root.Execute(); err != nil { Root.Println(err) os.Exit(1) } }
其中RunE是主命令触发会调用的方法。
子命令
package main import "github.com/spf13/cobra" func init() { listCmd := &cobra.Command{ Use: "list", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) (err error) { return }, } showCmd := &cobra.Command{ Use: "show <name>", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { name := args[0] cmd.Println(name) return }, } Root.AddCommand(listCmd, showCmd) }
Go的init方法比较适合将子命令加入主命令。这里要注意执行顺序:
变量初始化,
init,
main。
命令参数
注意RunE的参数args就是命令行参数。用Args的命令行参数做些校验后, 可以直接取用。
命令选项
package main import ( "github.com/spf13/cobra" ) func init() { delCmd := &cobra.Command{ Use: "delete <name>", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { name := args[0] force, err := cmd.Flags().GetBool("force") if err != nil { return } cmd.Print(name, force) return }, } flags := delCmd.Flags() flags.Bool("force", false, "force delete") Root.AddCommand(delCmd) }
注意命令选项force的声明,是在init里:
flags.Bool("force", false, "force delete")
其使用,是在RunE方法里:
force, err := cmd.Flags().GetBool("force")
这是可以的。因为RunE在main执行,在init之后。
cobra命令选项还有一种用法,如下:
package main import ( "github.com/spf13/cobra" ) func init() { var status string updCmd := &cobra.Command{ Use: "update <name>", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { name := args[0] cmd.Print(name, status) return }, } flags := updCmd.Flags() flags.StringVar(&status, "status", "", "status") Root.AddCommand(updCmd) }
这也是可以的。但如果把变量status提为全局变量,在init里使用,就可能出 问题,因为status可能还没有赋值。
环境变量
cobra作者推荐大家使用viper绑定环境变量。环境变量读取较简单,自己实 现也可以:
package main import ( "os" "strings" ) func Getenv(name, defaultValue) (value string) { for _, key := range []string{name, strings.ToLower(name), strings.ToUpper(name)} { if value = os.Getenv(key); value != "" { return } } return defaultValue }
然后这么使用:
flags.StringVar(&status, "status", Getenv("status",""), "status")
结果展示#
tablewriter很容易使用:
package main import ( "github.com/olekukonko/tablewriter" ) func NewTable(header []string) (table *tablewriter.Table) { table = tablewriter.NewWriter(writer) table.SetBorder(false) table.SetRowLine(false) table.SetHeader(header) return }
输出表格
比如:
table := NewTable([]string{"id", "node", "leader"}) table.Apppend([]string{"1","127.0.0.1:4321","yes"}) table.Render() 输出为: ID | ADDRESS | LEADER -----+----------------+--------- 1 | 127.0.0.1:4321 | yes
查询结果
下面是一个较复杂的例子,输出sql.Rows:
func PrintRows(rows *sql.Rows) (err error) { cNames, err := rows.Columns() if err != nil { return } table := NewTable(cNames) cSize := len(cNames) for rows.Next() { r := make([]string, cSize, cSize) values := make([]interface{}, cSize, cSize) for i := 0; i < cSize; i++ { value := make([]byte, 0) values[i] = &value } err = rows.Scan(values...) if err != nil { return } for i := 0; i < cSize; i++ { r[i] = string(*(values[i].(*[]byte))) } table.Append(r) } if table.NumLines() != 0 { table.Render() } return }
这样可以展示任意的查询结果:
dqlite> select * from food; ID | NAME | PRICE | CREATED AT -----+------+-------+---------------------- 1 | 大米 | 7.5 | 2019-11-26 08:43:37
取值复杂,模式是一样的。
交互模式
交互模式最关键的是行编辑。在Unix世界,行编辑大多由readline完成。在Go 语言里,最好的行编辑器是liner。
开启行
开启行:
var( line = liner.NewLiner() )
保证程序退出之前要关闭line, 否则程序退出后console就乱了:
line.Close()
获取输入
input, err := line.Prompt("dqlite> ")
这样就得到了一行。用户输入就由liner接管了,包括快捷键,光标移动,删 除等。
自动完成
另外liner还支持自动完成。
读入自动完成:
line.SetCompleter(func(line string) (c []string) { for _, n := range names { if strings.HasPrefix(n, strings.ToLower(line)) { c = append(c, n) } } return })
一般的,自动完成的内容是一些内置命令,以下是dqlited的一个使用:
package main import ( "strings" "github.com/peterh/liner" ) var ( line = liner.NewLiner() ) func AutoComplete(prefix string) (c []string) { for name := range commands { if strings.HasPrefix(name, prefix) { c = append(c, name) } } for _, name := range []string{ "detach", "attach", "create", "drop", "alert", "delete", "update", "select", "insert", "replace", "explain", "begin", "end", "commit", "savepoint", "rollback", "release"} { if strings.HasPrefix(name, prefix) { c = append(c, name) } } return } func init() { line.SetCompleter(AutoComplete) }
可以看到,我们把一些内置命令和sql语句的一些关键词作为自动完成的项。
历史记录
Ctrl+R上翻历史记录,和bash体验一致。
程序启动读入历史记录:
p := GetHistoryPath() if p != "" { if f, err := os.Open(p); err == nil { defer f.Close() line.ReadHistory(f) } }
在程序退出是,记得保存记录:
p := GetHistoryPath() if p != "" { if w, err := os.Create(p); err == nil { defer w.Close() line.WriteHistory(w) } }
否则程序退出历史记录就丢了。
小结
Go写命令行程序可谓得天独厚,社区在这块也繁荣,可谓百花齐放。列位在做选 择的时候,尽量避免使用有侵入式的包,选那些小而精的,在最大程度上发挥命 令行程序威力,提高用户体验。
- 点赞
- 收藏
- 关注作者
评论(0)