Go命令行开发

举报
nanjunjie 发表于 2019/12/27 18:38:32 2019/12/27
【摘要】 命令行接受输入,进行处理,然后展示结果。输入分交互模式和非交互模式。

命令行接受输入,进行处理,然后展示结果。输入分交互模式和非交互模式。


这里有三件事:


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写命令行程序可谓得天独厚,社区在这块也繁荣,可谓百花齐放。列位在做选 择的时候,尽量避免使用有侵入式的包,选那些小而精的,在最大程度上发挥命 令行程序威力,提高用户体验。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。