
本文详解如何在 go 中使用 exec.Command 正确将当前进程的标准输入(stdin)透传至 ruby 等交互式子进程,解决 gets 等阻塞读取不响应的问题。关键在于确保 cmd.Stdin = os.Stdin 生效且无缓冲干扰。
本文详解如何在 go 中使用 `exec.command` 正确将当前进程的标准输入(stdin)透传至 ruby 等交互式子进程,解决 `gets` 等阻塞读取不响应的问题。关键在于确保 `cmd.stdin = os.stdin` 生效且无缓冲干扰。
在 Go 中调用外部解释器(如 Ruby、Python 或 bash)执行交互式脚本时,若子进程内部使用 gets、input() 或 read 等阻塞式标准输入读取操作,常出现“卡住不等待输入”的现象。这并非 Go 的 bug,而是因 stdin 流未被正确继承或终端模式未适配所致。
✅ 正确做法:直接透传 os.Stdin
核心原则是——无需额外处理,只需将 cmd.Stdin 显式设为 os.Stdin,并确保父进程运行在交互式终端中(即非重定向/管道环境)。以下是最简可靠实现:
package main import ( "fmt" "os" "os/exec" ) func runCommand(cmdName string, arg ...string) { cmd := exec.Command(cmdName, arg...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin // ? 关键:直接复用当前进程 stdin if err := cmd.Run(); err != nil { fmt.Printf("Failed to run %s: %vn", cmdName, err) os.Exit(1) } } func main() { // 示例:Ruby 脚本中使用 gets 读取用户输入 runCommand("ruby", "-e", `puts "Enter your name:"; name = gets.chomp; puts "Hello, #{name}!"`) }
运行后,程序会正常输出提示,并暂停等待你在终端输入内容,输入回车后继续执行并打印结果。
⚠️ 常见误区与注意事项
- 不要自行包装 os.Stdin:避免使用 bufio.NewReader(os.Stdin) 或 io.MultiReader 等二次封装 stdin 后赋值给 cmd.Stdin —— 这会破坏底层文件描述符的直通性,导致子进程无法获得原始终端控制权。
- 终端环境必须有效:该方案仅在真实 TTY 终端中可靠(如本地终端、ssh 会话)。若父进程 stdin 已被重定向(如 go run main.go
- windows 用户注意:部分 Windows 终端(如旧版 CMD)对 stdin 继承支持不稳定,建议使用 Windows Terminal 或 WSL 验证;若仍异常,可尝试添加 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}(需导入 “syscall”)以增强进程组控制。
- 错误处理建议:cmd.Run() 会等待子进程完全退出;如需更细粒度控制(如超时、信号中断),应改用 cmd.Start() + cmd.Wait() 组合。
? 验证是否生效的小技巧
可在 Ruby 脚本中加入终端检测逻辑辅助调试:
# -e 'require "io/console"; puts "TTY? #{STDIN.tty?}"; puts "Input: #{gets.chomp}"'
若输出 TTY? true,说明 stdin 已正确连接终端;若为 false,则表明 stdin 被重定向或继承失败。
总之,cmd.Stdin = os.Stdin 是 Go 标准库支持交互式子进程的官方推荐方式,简洁、高效且跨平台兼容。只要确保运行环境具备交互式终端,即可无缝支持 gets、readline、input() 等所有依赖标准输入的交互行为。