PAYATHON 2026

为什么 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 的结果;排查时应检查它是否被固定设置。
  • 获取到的尺寸只是查询时的值。长时间运行的程序若需要适应窗口缩放,应重新查询。

备注:内容仅供参考。