本文介绍如何在 go 程序中启动外部交互式进程(如 `rm -i`),并实时读取其提示信息、写入用户响应,
实现真正的终端级交互,而非仅捕获一次性输出。核心在于正确管理标准输入/输出管道、避免使用阻塞式 `combinedoutput`,并灵活处理非换行终止的提示文本。
在 Go 中调用外部命令时,exec.Command 默认提供的是单向、批处理式交互(如 Output() 或 CombinedOutput()),适用于无需用户干预的场景。但当目标程序(如 rm -i、gpg --sign、ssh 交互式会话等)需要实时响应输入(例如确认提示 "Remove file 'somefile.txt'?")时,必须建立双向流式管道(stdin/stderr),并手动控制读写时序。
rm -i 将提示信息输出到 stderr(而非 stdout),因此需调用 cmd.StderrPipe() 获取读取端;同时通过 cmd.StdinPipe() 获取写入端,向进程发送响应(如 "y\n")。关键点如下:
package main
import (
"bufio"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("rm", "-i", "somefile.txt")
// rm 的提示写入 stderr
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal("获取 stderr 管道失败:", err)
}
reader := bufio.NewReader(stderr)
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal("获取 stdin 管道失败:", err)
}
defer stdin.Close()
// 启动进程(非 Run!)
if err := cmd.Start(); err != nil {
log.Fatal("启动 rm 失败:", err)
}
// 逐行读取提示(注意:ReadLine 不保证以 \n 结尾,需检查 isPrefix)
for {
line, isPrefix, err := reader.ReadLine()
if err != nil {
break // EOF 或其他错误
}
if isPrefix {
// 行太长被截断,需继续读取(实际中 rm 提示通常很短)
continue
}
prompt := string(line)
if prompt == "Remove file 'somefile.txt'?" ||
prompt == "rm: remove regular empty file ‘somefile.txt’" {
stdin.Write([]byte("y\n"))
}
}
// 等待进程退出
if err := cmd.Wait(); err != nil {
log.Printf("rm 执行异常: %v", err)
}
}某些交互程序(如 rm 在不同 locale 下)可能输出无换行的提示,或以 ? 结尾但无 \n。此时需自定义 bufio.Scanner.SplitFunc,将 ? 也视为行结束符:
package main
import (
"bytes"
"bufio"
"log"
"os/exec"
)
// 自定义分隔符:支持 \n 和 ? 作为行边界
func scanOnQuestion(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
return i + 1, data[:i], nil
}
if i := bytes.IndexByte(data, '?'); i >= 0 {
return i + 1, data[:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
func main() {
cmd := exec.Command("rm", "-i", "somefile.txt")
stderr, _ := cmd.StderrPipe()
scanner := bufio.NewScanner(stderr)
scanner.Split(scanOnQuestion) // 注册自定义分割函数
stdin, _ := cmd.StdinPipe()
defer stdin.Close()
if err := cmd.Start(); err != nil {
log.Fatal("启动失败:", err)
}
for scanner.Scan() {
line := scanner.Text()
// 注意:不同系统/语言环境下提示文本可能不同,建议日志调试确认
if line == "rm: remove regular empty file ‘somefile.txt’" ||
line == "Remove file 'somefile.txt'" {
stdin.Write([]byte("y\n"))
}
}
if err := scanner.Err(); err != nil {
log.Fatal("扫描 stderr 出错:", err)
}
if err := cmd.Wait(); err != nil {
log.Printf("rm 退出异常: %v", err)
}
}掌握管道的显式控制与流式解析,即可让 Go 程序无缝集成各类交互式 CLI 工具,大幅提升自动化脚本的健壮性与适用范围。