重定向 stderr 会更改 tput 的输出,但仅在将输出捕获到变量时才会更改?

Redirecting stderr changes the output of tput but only when capturing the output to a variable?

我正在尝试在脚本中获取当前终端 window 的宽度,并使用 80 作为后备以防万一。我认为这很简单:

cols=$( tput cols || echo 80 ) ; echo $cols
# -> 100

100 是正确的,我将终端设为 100 个字符宽。由于 cols 没有 POSIX 符合 tput 的参数,并非所有系统都支持它,因此回退。现在让我们测试回退:

cols=$( tput colsx || echo 80 ) ; echo $cols
# -> tput: unknown terminfo capability 'colsx'
# -> 80

嗯……不太好。我不想看到那个错误。它打印在 stderr 上,所以让我们抑制它:

cols=$( tput colsx 2>/dev/null || echo 80 ) ; echo $cols
# -> 80

是的,这样好多了。所以最后的代码是

cols=$( tput cols 2>/dev/null || echo 80 ) ; echo $cols 
# -> 80

80?什么鬼?为什么现在 运行 进入回退?让我们试试这个:

cols=$( tput cols 2>/dev/null ) ; echo $cols  
# -> 80

啊啊...重定向 stderr 会改变 tput 的输出吗?这怎么可能?让我们确认一下:

tput cols 2>/dev/null
# -> 100

好吧,我迷路了!有人可以向我解释一下这是怎么回事吗?

有部分答案here

tput cols 可以根据这些情况导致来自不同来源的数据:

  • fd 1,2 之一是 tty
  • fd 1,2 都不是 tty

当 运行 tput cols : 终端设置 columns 可能被获取 如果可能,使用 fd 1,2 的 ioctl(),否则获取 terminfo 能力 cols.

本期设置终端列99,

$ stty columns 99; stty -a | grep columns
        speed 38400 baud; rows 24; columns 99; line = 0;

只有 fd 1 被重定向(到 non-tty):

$ tput cols > out; cat out
99

只有 fd 2 被重定向:

$ tput cols 2> err; wc -c err
99
0 err

两个 fd 1,2 重定向:

$ tput cols > out 2>&1; cat out
80

fd 1 不是 tty:

$ echo $(tput cols || echo 100)
99

fd 1,2 不是 tty:

$ echo $(tput cols 2> /dev/null || echo 100)
80

显示 cols 重定向 fd 1,2 时获取的能力, 创建并安装了名为 tmp 且具有不同 cols 的术语信息,然后:

$ export TERM=tmp TERMINFO=$HOME/.ti
$ infocmp | grep cols
        colors#8, cols#132, it#8, lines#24, pairs#64,

fd 1,2 不是 tty:

$ echo $(tput cols 2> /dev/null || echo 100)
132

假上限,tput退出non-zero:

$ echo $(tput kol 2> /dev/null || echo 100)
100

实际上 Milag 的答案解决了这个谜题! 好吧,它有点复杂,真正的答案有点隐藏在所有给出的细节中,所以我提供了一个对于感兴趣的 reader,这里的回复更容易理解,但是荣誉归于 Milag,因此我会接受这个答案(也是为了声誉)。

简而言之,事情是这样的:

tput cols 需要 tty 才能获得真正的终端宽度。当我打开终端 window 时,stdoutstderr 都是 tty,因此

tput cols

打印出正确的结果。如果我现在将 stderr 重定向到 /dev/null,如

tput cols 2>/dev/null

那么stderr不再是tty,而是一个char文件。但这没问题,因为 stdout 仍然是 tty.

但是,如果我在变量中捕获输出,如

cols=$( tput cols 2>/dev/null )

stdout 不再是 tty,它是 shell 的管道,它捕获命令的输出。现在 tput 已经没有 tty 了,因此无法再获得真实的宽度,所以它使用了一些回退,这个回退在我的系统上报告 80(其他系统可能有更好的回退机制并且仍然报告正确的值)。

所以对于我的脚本,我将不得不稍微解决这个问题:

if tput cols >/dev/null 2>&1; then
    cols=$( tput cols )
else
    cols=80
fi

第一个 if 检查 tput 是否知道 cols 参数,根本不产生任何可见输出或捕获任何内容,我只想知道退出代码是什么。如果它确实支持这个论点,我捕获输出,否则我直接使用回退。