Bash 写了 2>&1 仍漏掉错误日志:重定向顺序相反
Bash 从左向右处理重定向。2>&1 放在输出文件重定向之前,会让错误保留在旧目的地;用两路输出探针核对,并区分覆盖与追加。
命令后面已经写了 2>&1,日志文件里却只有正常输出,错误仍出现在终端上。若完整写法是 ./build.sh 2>&1 > ./build.log,问题在重定向顺序:Bash 先复制了标准输出当时的去向,随后才把标准输出改到文件。
这里讨论 Bash 命令行中的文件描述符重定向,规则可用于 Bash 5.2。前提是程序把错误写到标准错误,而不是自行打开另一个日志文件。仅仅看到日志不完整,还不能直接认定是这条原因。
对照整条命令的顺序
标准输出是文件描述符 1,标准错误是 2。> 没有写描述符编号时,默认处理标准输出。将两路输出写入同一份日志,应写成:
./build.sh > ./build.log 2>&1
Bash 从左向右处理:先打开 build.log,使描述符 1 指向它;再把描述符 2 复制到 1 当前指向的目的地。程序启动时,两路输出都写入该文件。
顺序相反时:
./build.sh 2>&1 > ./build.log
第一步的 2>&1 让标准错误指向标准输出当时的目的地。如果原来是终端,标准错误就保留在终端。第二步只改变描述符 1,不会再改变描述符 2。2>&1 不是建立一条“以后始终跟着标准输出走”的动态关联。
因此,修正时应看完整命令,而不只是搜索其中是否存在 2>&1。如果启动脚本外面还有包装命令或另一层 shell,也要确认自己修改的是实际执行的那一层。
用同时产生两路输出的命令检查
普通 echo 通常只产生标准输出,用它验证这两个顺序,很容易得到“两种都能写入日志”的印象。可以在本机测试目录使用下面的无敏感内容探针。probe.log 必须是可以新建或覆盖的测试文件,不要换成需要保留的业务日志:
bash -c 'printf "%s\n" stdout; printf "%s\n" stderr >&2' > ./probe.log 2>&1
cat ./probe.log
按重定向规则,文件里应有 stdout 和 stderr 两行。内部第二个 printf 的 >&2 专门把这一行写到标准错误,所以两行都出现才同时验证了两条通道。
如果把外层顺序改成 2>&1 > ./probe.log,且运行前标准输出连接终端,文件里应只留下 stdout,stderr 则显示在终端。这是区分顺序问题的检查,不需要拿真实构建错误或凭据内容做测试。
保留旧日志时使用追加
> 会截断已经存在的目标文件。若需求是保留旧日志并追加本次输出,改为:
./build.sh >> ./build.log 2>&1
顺序仍然相同:先设置标准输出,再复制给标准错误。追加并不负责日志轮转,也不会让多个并发任务的输出自动分段;是否允许多个任务写同一文件,需要另行约定。
不要为两路输出分别打开同一目标文件来替代这个写法。这里需要的是两个描述符共享已经打开的输出目的地,而不是分别创建两次文件重定向。
探针正常后再检查实际程序
保留原启动命令,只调整末尾重定向后,检查程序的一次正常输出和一次可控的错误输出是否都进入预期文件。不要为了制造错误在生产环境删除文件或修改权限。
如果连探针都写不进去,先看 shell 是否报告文件不存在或权限不足。日志所在目录必须存在且可写;若 shell 无法打开重定向目标,命令可能尚未启动。启用了 noclobber 时,覆盖已有文件也可能被拒绝,不应直接关闭保护来掩盖目标路径错误。
探针正常、实际程序仍缺日志时,确认应用究竟写向哪里。应用自己的日志文件不受这段标准输出与标准错误重定向控制;已经关闭或改接描述符的启动包装也需要单独检查。顺序修正只解决当前命令的这两条输出通道。
变更前保存原启动配置,并限制日志文件的读取权限,避免新增收集的错误内容泄露敏感信息。需要回退时恢复原命令;被 > 覆盖的旧日志不能靠恢复命令找回,所以文件保留策略应在首次运行前确定。


