为什么 macOS 中 tput cols 在命令替换内重定向 stderr 后返回 80?
结论
命令替换会把 tput 的 stdout 接到捕获输出的管道。如果此时再把 stderr 重定向到 /dev/null 或普通文件,相关 ncurses 实现就无法通过这两个文件描述符查询实际终端尺寸,因此可能返回 80。
这里的“失败”是指没有取得实际宽度。输出 80 本身不能证明命令以非零状态退出。
为什么 stderr 会影响终端宽度?
Apple 公布的 tput 源码使用 setupterm(term, STDOUT_FILENO, &errret) 初始化终端信息。对应的 ncurses 实现发现 stdout 不是终端时,会改用 stderr 作为终端文件描述符。参见 Apple 的 tput 源码和 ncurses 终端初始化源码。
题目中的各组结果可以这样解释:
| 调用方式 | stdout 指向 | stderr 指向 | 查询实际尺寸的条件 |
|---|---|---|---|
tput cols | 终端 | 终端 | 可通过 stdout 查询 |
tput cols 2>/dev/null | 终端 | /dev/null | 仍可通过 stdout 查询 |
$(tput cols) | 捕获管道 | 终端 | 可改用 stderr 查询 |
$(tput cols 2>/dev/null) | 捕获管道 | /dev/null | 两者都无法提供终端尺寸 |
$(tput cols 2>/tmp/file.tmp) | 捕获管道 | 普通文件 | 两者都无法提供终端尺寸 |
$(tput cols 2>/dev/ttys002) | 捕获管道 | 终端设备 | 可通过 stderr 查询该设备的尺寸 |
这里的 stderr 也用于访问终端设备。即使 stdin 仍然连接着终端,上述初始化路径也不会自动改用 stdin。
为什么返回的是 80?
在上述 ncurses 实现中,列数可以来自 terminfo、操作系统提供的窗口尺寸和环境变量 COLUMNS。查不到动态尺寸时,仍可能取得静态配置值;如果列数最终仍不可用,代码会以 80 列兜底。相关逻辑见 终端尺寸计算源码。
题目中的 80 符合这种回退行为,但仅凭现有输出,无法判断它来自 terminfo、已导出的 COLUMNS,还是最终兜底值。也不能据此认为所有环境都会固定返回 80。
如何获取实际宽度?
保留 stderr 的终端连接
如果不需要隐藏错误输出,直接捕获 stdout 即可:
cols=$(tput cols)
printf '%s\n' "$cols"
如果 stderr 已被上层脚本重定向,而当前进程仍有可访问的控制终端,可以显式使用 /dev/tty:
cols=$(tput cols 2>/dev/tty)
printf '%s\n' "$cols"
这与题目中重定向到 /dev/ttys002 的思路相同。/dev/tty 指向当前进程的控制终端,可以避免写死某次会话的设备路径。此写法会将错误信息发送到终端。
需要隐藏错误时,用 stty 查询控制终端
如果只需要取得当前控制终端的列数,可以使用 stty size。它输出行数和列数,再用 Bash 的 read 拆分:
if size=$(stty size 2>/dev/null </dev/tty); then
read -r rows cols <<< "$size"
else
cols=80
fi
printf '%s\n' "$cols"
这里的 80 是脚本主动选择的布局默认值。将 2>/dev/null 放在打开 /dev/tty 之前,也能隐藏打开控制终端失败时的错误信息。
使用时需要注意什么?
- 原问题只说明了 Bash 3.2,没有提供
tput和 ncurses 的具体版本。上述源码路径与现象吻合,其他实现的终端选择方式可能不同。 /dev/tty要求进程具有可访问的控制终端。定时任务或部分 CI 环境可能不满足这个条件,需要明确设置默认宽度。- 已导出的
COLUMNS可能影响tput cols的结果;排查时应检查它是否被固定设置。 - 获取到的尺寸只是查询时的值。长时间运行的程序若需要适应窗口缩放,应重新查询。
备注:内容仅供参考。
