用 go-prompt 给 Go CLI 接入交互式输入:自动补全、历史记录与快捷键
写命令行工具时,如果用户每次都要输入完整命令,体验会比较差;给 CLI 加自动补全、历史记录、多行输入和快捷键,自己实现又容易出错。go-prompt 是一个 Go 的交互式命令行库,下面按接入路径记录它的用法。
1. 为什么需要 go-prompt
先说一个场景:写数据库查询工具。用标准库的实现:
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
scanner.Scan()
query := scanner.Text()
// 执行查询...
}
问题:
- 没有自动补全
- 没有历史记录
- 没有快捷键支持
- 用户体验差
用 go-prompt:
package main
import (
"fmt"
"github.com/c-bata/go-prompt"
)
func completer(d prompt.Document) []prompt.Suggest {
s := []prompt.Suggest{
{Text: "SELECT", Description: "查询数据"},
{Text: "INSERT", Description: "插入数据"},
{Text: "UPDATE", Description: "更新数据"},
{Text: "DELETE", Description: "删除数据"},
{Text: "FROM", Description: "指定表"},
{Text: "WHERE", Description: "条件过滤"},
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
func main() {
p := prompt.New(
executor,
completer,
prompt.Title("sql"),
prompt.PrefixColor(prompt.Cyan),
)
p.Run()
}
func executor(s string) {
fmt.Printf("执行:%s\n", s)
}
自动补全、历史记录、快捷键都在库内提供。
2. go-prompt 是什么
go-prompt 是 Go 语言的交互式命令行库。官方描述:
A library for building powerful interactive command line tools in Go.
它提供:
- 交互式命令行库
- 支持自动补全
- 支持历史记录
- 支持快捷键
核心定位
go-prompt 不是命令行参数解析库,也不是 TUI 框架。它是交互式输入库,用于构建智能命令行工具。
它做的事情:
- 提供交互式输入界面
- 支持自动补全
- 支持历史记录
- 支持快捷键
这些功能解决的是命令行工具的输入体验问题。
3. 核心特性:自动补全和交互
go-prompt 的主要特性是自动补全和交互。
自动补全
func completer(d prompt.Document) []prompt.Suggest {
s := []prompt.Suggest{
{Text: "users", Description: "用户表"},
{Text: "orders", Description: "订单表"},
{Text: "products", Description: "商品表"},
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
多级补全
func completer(d prompt.Document) []prompt.Suggest {
// 根据上下文提供不同的补全
if d.Text() == "SELECT " {
return []prompt.Suggest{
{Text: "users", Description: "用户表"},
{Text: "orders", Description: "订单表"},
}
}
if strings.HasPrefix(d.Text(), "SELECT * FROM ") {
return []prompt.Suggest{
{Text: "WHERE", Description: "条件过滤"},
{Text: "ORDER BY", Description: "排序"},
}
}
return []prompt.Suggest{}
}
历史记录
p := prompt.New(
executor,
completer,
prompt.History(history),
)
快捷键
支持 Emacs 风格的快捷键:
- Ctrl+A:移动到行首
- Ctrl+E:移动到行尾
- Ctrl+K:删除到行尾
- Ctrl+U:删除到行首
- Ctrl+W:删除前一个单词
4. 安装和使用
安装:
go get github.com/c-bata/go-prompt
基本使用
1. 简单示例
package main
import (
"fmt"
"github.com/c-bata/go-prompt"
)
func executor(s string) {
fmt.Printf("你输入了:%s\n", s)
}
func completer(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{
{Text: "help", Description: "显示帮助"},
{Text: "exit", Description: "退出程序"},
{Text: "clear", Description: "清屏"},
}
}
func main() {
p := prompt.New(
executor,
completer,
prompt.PrefixColor(prompt.Cyan),
)
p.Run()
}
2. 动态补全
func completer(d prompt.Document) []prompt.Suggest {
word := d.GetWordBeforeCursor()
commands := []prompt.Suggest{
{Text: "select", Description: "查询"},
{Text: "insert", Description: "插入"},
{Text: "update", Description: "更新"},
{Text: "delete", Description: "删除"},
}
tables := []prompt.Suggest{
{Text: "users", Description: "用户表"},
{Text: "orders", Description: "订单表"},
{Text: "products", Description: "商品表"},
}
// 根据输入位置提供不同的补全
if strings.HasPrefix(d.TextBeforeCursor(), "select ") {
return prompt.FilterHasPrefix(tables, word, true)
}
return prompt.FilterHasPrefix(commands, word, true)
}
3. 实时补全
type SQLCompleter struct {
tables []string
columns map[string][]string
}
func (c *SQLCompleter) Complete(d prompt.Document) []prompt.Suggest {
word := d.GetWordBeforeCursor()
// 分析 SQL 上下文
text := d.TextBeforeCursor()
// SELECT 后面补表名
if matched, _ := regexp.MatchString(`SELECT\s+\w*$`, text); matched {
var suggestions []prompt.Suggest
for _, table := range c.tables {
suggestions = append(suggestions, prompt.Suggest{
Text: table,
Description: "表",
})
}
return prompt.FilterHasPrefix(suggestions, word, true)
}
// FROM 后面补表名
if matched, _ := regexp.MatchString(`FROM\s+\w*$`, text); matched {
var suggestions []prompt.Suggest
for _, table := range c.tables {
suggestions = append(suggestions, prompt.Suggest{
Text: table,
Description: "表",
})
}
return prompt.FilterHasPrefix(suggestions, word, true)
}
return []prompt.Suggest{}
}
4. 自定义样式
p := prompt.New(
executor,
completer,
prompt.Title("my-app"),
prompt.PrefixText("$ "),
prompt.PrefixColor(prompt.Green),
prompt.InputTextColor(prompt.White),
prompt.SuggestionBGColor(prompt.Blue),
prompt.SuggestionTextColor(prompt.White),
)
5. 核心功能
go-prompt 定位是交互式输入库,功能包括多行输入、密码输入、自定义快捷键和实时验证。
功能一:多行输入
func executor(s string) {
// 支持多行输入
lines := strings.Split(s, "\n")
for _, line := range lines {
fmt.Printf("行:%s\n", line)
}
}
功能二:密码输入
p := prompt.New(
func(s string) {
fmt.Printf("密码长度:%d\n", len(s))
},
completer,
prompt.OptionPrefixText("密码:"),
prompt.OptionInputTextColor(prompt.Yellow),
)
功能三:自定义快捷键
func keyBindFunc(d prompt.Document) []prompt.KeyBind {
return []prompt.KeyBind{
{
Key: prompt.ControlC,
Fn: func(buf prompt.Buffer) {
os.Exit(0)
},
},
{
Key: prompt.ControlL,
Fn: func(buf prompt.Buffer) {
// 清屏
fmt.Print("\033[2J\033[H")
},
},
}
}
功能四:实时验证
func validator(d prompt.Document) error {
text := d.Text()
if len(text) == 0 {
return prompt.ValidationError("输入不能为空")
}
if len(text) > 100 {
return prompt.ValidationError("输入太长")
}
return nil
}
p := prompt.New(
executor,
completer,
prompt.OptionValidator(validator),
)
6. 实战场景
场景一:SQL 客户端
需要一个智能 SQL 客户端。
type SQLClient struct {
db *sql.DB
tables []string
columns map[string][]string
}
func (c *SQLClient) completer(d prompt.Document) []prompt.Suggest {
word := d.GetWordBeforeCursor()
text := strings.ToLower(d.TextBeforeCursor())
// SQL 关键字
keywords := []prompt.Suggest{
{Text: "SELECT", Description: "查询"},
{Text: "INSERT", Description: "插入"},
{Text: "UPDATE", Description: "更新"},
{Text: "DELETE", Description: "删除"},
{Text: "FROM", Description: "表"},
{Text: "WHERE", Description: "条件"},
}
// 表名
var tables []prompt.Suggest
for _, t := range c.tables {
tables = append(tables, prompt.Suggest{
Text: t,
Description: "表",
})
}
// 根据上下文选择补全
if strings.Contains(text, "from ") {
return prompt.FilterHasPrefix(tables, word, true)
}
return prompt.FilterHasPrefix(keywords, word, true)
}
func (c *SQLClient) executor(s string) {
if strings.TrimSpace(s) == "" {
return
}
rows, err := c.db.Query(s)
if err != nil {
fmt.Printf("错误:%v\n", err)
return
}
defer rows.Close()
// 显示结果...
}
func main() {
client := &SQLClient{
tables: []string{"users", "orders", "products"},
}
p := prompt.New(
client.executor,
client.completer,
prompt.Title("sql"),
prompt.PrefixText("sql> "),
)
p.Run()
}
效果:SQL 关键字、表名自动补全。
场景二:配置编辑器
需要一个智能配置编辑器。
type ConfigEditor struct {
config map[string]interface{}
}
func (e *ConfigEditor) completer(d prompt.Document) []prompt.Suggest {
word := d.GetWordBeforeCursor()
var suggestions []prompt.Suggest
for key := range e.config {
suggestions = append(suggestions, prompt.Suggest{
Text: key,
Description: fmt.Sprintf("当前值:%v", e.config[key]),
})
}
return prompt.FilterHasPrefix(suggestions, word, true)
}
func (e *ConfigEditor) executor(s string) {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
fmt.Println("格式:key=value")
return
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
e.config[key] = value
fmt.Printf("设置 %s = %v\n", key, value)
}
效果:配置项自动补全,并显示当前值。
场景三:命令调试器
需要一个交互式命令调试器。
type Debugger struct {
commands map[string]func([]string)
}
func (d *Debugger) completer(doc prompt.Document) []prompt.Suggest {
word := doc.GetWordBeforeCursor()
var suggestions []prompt.Suggest
for cmd := range d.commands {
suggestions = append(suggestions, prompt.Suggest{
Text: cmd,
Description: "命令",
})
}
return prompt.FilterHasPrefix(suggestions, word, true)
}
func (d *Debugger) executor(s string) {
parts := strings.Fields(s)
if len(parts) == 0 {
return
}
cmd := parts[0]
args := parts[1:]
if fn, ok := d.commands[cmd]; ok {
fn(args)
} else {
fmt.Printf("未知命令:%s\n", cmd)
}
}
效果:命令自动补全,支持交互式调试。
7. 设计亮点
亮点一:纯 Go 实现
不依赖外部库,纯 Go 实现。好处:跨平台、易于集成、性能优秀。
亮点二:灵活补全
支持动态、上下文感知的补全。好处:智能补全、适应场景、用户体验好。
亮点三:丰富功能
支持历史记录、快捷键、多行输入等。好处:功能全面、一个库搞定、减少依赖。
亮点四:自定义样式
支持自定义颜色、提示符等。好处:个性化、品牌一致、视觉友好。
亮点五:活跃维护
社区活跃,持续更新。好处:Bug 修复快、新特性多、文档完善。
8. 和类似方案对比
vs. readline
| 维度 | go-prompt | readline |
|---|---|---|
| Star | 8K | 1K |
| 自动补全 | 强大 | 基础 |
| 颜色支持 | 丰富 | 有限 |
| 维护 | 活跃 | 较少 |
结论:go-prompt 功能更全。
vs. promptui
| 维度 | go-prompt | promptui |
|---|---|---|
| Star | 8K | 3K |
| 定位 | 交互式输入 | 表单输入 |
| 补全 | 强大 | 无 |
| 场景 | CLI 工具 | 交互式表单 |
结论:go-prompt 适合 CLI,promptui 适合表单。
vs. 标准库
| 维度 | go-prompt | bufio.Scanner |
|---|---|---|
| 自动补全 | 支持 | 无 |
| 历史记录 | 支持 | 无 |
| 快捷键 | 支持 | 无 |
| 用户体验 | 优秀 | 基础 |
结论:go-prompt 体验更好。
9. 局限性
- 学习曲线:API 较多,需要学习
- 性能开销:大量补全时可能较慢
- 平台限制:某些平台支持有限
- 调试困难:交互式调试较难
- 文档简单:某些高级功能文档不足
- 依赖终端:依赖终端能力
10. 总结
从 go-prompt 可以看到几个点:
- 用户体验很重要:CLI 也需要好体验
- 自动补全是刚需:减少输入错误
- 交互式工具有价值:提高生产力
- 纯 Go 实现是优势:跨平台,易集成
- 灵活设计很重要:适应不同场景
go-prompt 解决的是 CLI 输入体验这个具体问题。如果还在用标准库写 CLI 工具,可以试一下 go-prompt。也许有一天 Go 标准库会提供更强大的交互式输入功能,但在那之前,go-prompt 已经覆盖了这类交互式 CLI 的常见需求。
参考资料: