如何使用Golang编写基础的交互式Shell工具

1次阅读

应使用 golang.org/x/term.readpassword 读取密码以隐藏输入,避免明文显示;执行命令须绕过 shell、用 os/exec.command 显式传参防注入;交互输入推荐 github.com/chzyer/readline;ctrl+c 需用 os/signal 捕获并安全终止子进程。

如何使用Golang编写基础的交互式Shell工具

golang.org/x/term 读取密码或隐藏输入

默认的 fmt.Scanlnbufio.NewReader(os.Stdin).ReadString('n') 会把密码明文打在终端上,这不行。得用 golang.org/x/term —— 它是 Go 官方维护的、跨平台的终端控制包,比自己调 syscall 稳定得多。

常见错误是直接 import syscall 或硬写 ANSI 转义序列,结果在 windows 上崩,或在某些 ide 内置终端里失效。

  • go get golang.org/x/term
  • 读密码:用 term.ReadPassword(int(os.Stdin.Fd())),它自动关回显、清缓存、处理 Ctrl+C
  • 注意:返回的是 []byte,记得用 string(pwd) 转成字符串;别漏掉 err 检查,比如用户按 Ctrl+D 会返回 io.EOF
  • Windows 下如果报 not a terminal,说明 stdin 被重定向了(比如管道或 IDE 运行),这时不能用 ReadPassword,得降级提示并用普通读取(但要明确告诉用户“密码将明文显示”)

os/exec 安全执行用户命令,避免 shell 注入

交互式 Shell 工具常需要运行用户输入的命令,比如 ls -l /tmp。直接拼接字符串再丢给 sh -c 是高危操作——rm -rf /; echo "done" 这种输入会直接执行两条命令。

正确做法是绕过 shell,用 os/exec.Command 显式拆解命令和参数:

立即学习go语言免费学习笔记(深入)”;

  • 不要:exec.Command("sh", "-c", userCmd)
  • 要:exec.Command("ls", "-l", "/tmp") —— 把用户输入按空格切分后,逐个传进参数列表(注意:简单空格分割不够健壮,真实场景建议用 shlex.Split 类逻辑,或限制只支持固定命令集)
  • 如果真要支持管道或重定向,别自己解析,改用 github.com/mitchellh/go-homedir + os/user 做路径展开,然后交由 shell 执行,但必须对输入做白名单校验(比如只允许 catgrephead 等几个命令)
  • cmd.Run() 会阻塞,适合简单命令;需要实时输出时用 cmd.StdoutPipe() + io.copy,别用 cmd.Output() 吞大文件,容易 OOM

readline 库实现带历史和补全的输入行

fmt.print + bufio 只能实现“输完回车才响应”,没法上下键翻历史、Tab 补全、Ctrl+A 跳行首——这不是 Shell,是填空题。

Go 生态里最轻量靠谱的是 github.com/chzyer/readline(虽已归档,但稳定、无依赖、文档清晰)。别用 promptuisurvey,它们面向表单,不是持续交互的 Shell。

  • 初始化:rl, _ := readline.New(readline.Config{Prompt: "> "}),记得 defer rl.Close()
  • 历史记录自动保存在内存,想持久化就加 historyFile: "/path/to/history",它会自动读写 json
  • 补全靠 rl.SetCompleter(),函数接收当前输入前缀,返回匹配项列表;注意别在补全函数里做耗时操作(如查网络),会卡住整个行编辑
  • Windows 下如果光标乱跳,确认没开 VS Code 的“integrated terminal”兼容模式,或换用 github.com/abiosoft/ishell(更重但 Win 支持更好)

信号处理:让 Ctrl+C 不直接 kill 进程

交互式工具里,用户按 Ctrl+C 通常想取消当前命令,而不是退出整个程序。默认行为是进程收到 SIGINT 后直接终止,没法拦截。

得用 os/signal 显式捕获,并区分场景处理:

  • 全局注册:signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM),但别在主 goroutine 里 阻塞,否则无法响应新命令
  • 推荐方式:每个命令执行前起一个 goroutine 监听 SIGINT,一旦收到,调用 cmd.Process.Kill() 终止子进程,然后继续主循环
  • 注意:cmd.Run() 是同步的,cmd.Start() + cmd.Wait() 才能配合信号中断;同时记得恢复终端状态(比如 readline 被中断后可能残留原始模式)
  • macos/linuxSIGINT 默认发给整个进程组,Windows 用 os.Interrupt 即可,不用 syscall

事情说清了就结束。真正的难点不在代码怎么写,而在于你得想清楚:这个 Shell 是给谁用?要不要支持脚本化?是否允许用户逃逸到系统 shell?每多一个“要”,底层就要多一层隔离和校验。

text=ZqhQzanResources