不应直接拼接字符串调用 shell,因存在命令注入、路径空格处理错误、环境变量不继承、错误码丢失等风险;应使用 exec.Command 传参方式,避免 sh -c,必要时用占位符传递参数,并手动处理重定向、管道和环境变量。

为什么不用 os/exec.Command 直接拼字符串调 shell
因为容易被注入、路径空格处理错、环境变量不继承、错误码丢失。比如 cmd := exec.Command("sh", "-c", "ls "+userInput),只要 userInput 是 ; rm -rf / 就完蛋。真实脚本要像 shell 一样做参数隔离,不是“调 shell”,而是“模拟 shell 行为”。
用 exec.Command 传参而非拼接命令行
所有参数必须单独作为 exec.Command 的后续参数传入,让 OS 层负责转义和分隔:
cmd := exec.Command("find", "/tmp", "-name", "*.log", "-mtime", "+7")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
注意点:
-
exec.Command第一个参数是可执行文件名,不能带路径空格(如/usr/local/bin/my-tool要写全,不能依赖$PATH查找,除非显式设置cmd.Env) - 不要用
sh -c包一层,除非你真需要 shell 特性(管道、重定向、通配符展开),否则绕开更安全 - 如果必须用
sh -c,参数必须用占位符:exec.Command("sh", "-c", "ls $1 | grep $2", "sh", dir, pattern)—— 第四个起的参数会绑定到$1,$2…
重定向、管道、环境变量得自己接管
Go 没有内置 shell 解析器,|、>、< 都不会自动生效。常见做法:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 重定向输出:设
cmd.Stdout = &bytes.Buffer{}或os.File,别依赖sh -c "cmd > out.txt" - 简单管道:用
io.Pipe()连两个exec.Cmd的Stdin/Stdout,但要注意死锁(先启后 cmd,再启前 cmd) - 环境变量:修改
cmd.Env,例如append(os.Environ(), "LANG=C"),不要靠os.Setenv全局改 - 当前目录:设
cmd.Dir = "/path",别在命令里写cd /path && cmd
替代 grep/sed/awk 时别硬套正则
很多 shell 脚本用 grep -v "pattern" 做过滤,Go 里直接读行 + strings.Contains 或 regexp.MatchString 更快更可控:
scanner := bufio.NewScanner(cmd.Stdout)
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "skipme") {
fmt.Println(line)
}
}
注意:
- 避免用
regexp处理每行日志——90% 场景strings足够,性能差 5–10 倍 -
sed 's/old/new/g'等价于strings.ReplaceAll(line, "old", "new"),别启动新进程 - 如果逻辑复杂(字段切分、多条件统计),直接在 Go 里解析比拼
awk '{print $3}'更可靠,尤其遇到 tab/空格混排时
真正的难点不在语法转换,而在于 shell 脚本里那些隐式行为:子 shell 变量作用域、set -e 的错误传播、$(cmd) 的截断逻辑——这些都得显式建模,漏一点就和原脚本行为不一致。

















