1

I'm trying to get the width of the current terminal window in a script and use 80 as a fallback in case that isn't possible. I thought that's pretty simple:

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

100 is correct, I made the terminal 100 chars wide. As cols is no POSIX conform argument to tput, not all systems will support it, thus the fallback. Now let's test the fallback:

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

Hmmmm... not so nice. I don't want to see that error. It's printed on stderr, so let's just suppress it:

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

Yes, that's much better. So the final code is

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

80? WTF? Why does it run into the fallback now? Let's try this:

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

Ahhhh... redirecting stderr changes the output of tput? How's that possible? Let's confirm that:

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

Okay, now I'm lost! Can someone please explain me what's going on here?

Mecki
  • 125,244
  • 33
  • 244
  • 253
  • I can't duplicate your problem, my `BASH_VERSION=4.4.12(1)-release` . Good luck! – shellter Oct 02 '20 at 13:40
  • @shellter I use `zsh 5.7.1` but switching to `bash` makes no difference, same behavior. So if it is some system oddity that is causing this problem, it seems to affect all my shells. – Mecki Oct 02 '20 at 14:11
  • Something in you `stty` settings? Good luck. – shellter Oct 02 '20 at 21:24
  • And of course hide/comment out any/all `.bash_rc`, `.profile`. (there's a zillion of them). Then turn them back on one at a time to isolate where it is coming form, then if no obvious suspects, comment except 1, and add back 1 at a time will sourcing that file. (Out of ideas for now, maybe more later). Or look at their modification dates, which is newest, and what was changed? Let us know if you find anything. Good luck. – shellter Oct 02 '20 at 21:26
  • @Mecki - are you running `tput` on a Linux system? what's your distro, release? I was able to repro on a Debian system. – Milag Oct 02 '20 at 21:51
  • @shellter I have emptied all these files for testing purposes (`.bashrc`, `.bash_profile`, and `.zshrc`, there never has been a `.profile` on my system), make no difference. – Mecki Oct 05 '20 at 17:28
  • @Milag It's actually macOS 10.15.6. Note however that macOS is officially POSIX certified and based on a FreeBSD clone at its core. – Mecki Oct 05 '20 at 17:30
  • @Mecki - OK, might be worth adding a tag for macOS – Milag Oct 05 '20 at 17:44
  • @Milag Not required, as it is not really a macOS issue. Basically you already answered the question, see my answer as well. Other systems may have better fallbacks than macOS and thus report better values but the problem is that when I redirect `stderr` and at the same time capture `stdout`, `tput` has no `tty` any longer. – Mecki Oct 05 '20 at 18:14

2 Answers2

2

There's a partial answer here

tput cols can lead to data from different sources based on these cases:

  • one of fd 1,2 is a tty
  • both fd 1,2 are not a tty

When running tput cols : terminal setting columns may be fetched with an ioctl() using fd 1,2 if possible, otherwise get terminfo capability cols.

This session sets 99 for terminal columns,

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

Only fd 1 redirected (to a non-tty):

$ tput cols > out; cat out
99

Only fd 2 redirected:

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

Both fd 1,2 redirected:

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

fd 1 not a tty:

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

fd 1,2 not a tty:

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

To show cols cabability being fetched when fd 1,2 are redirected, a terminfo named tmp with different cols was created and installed, then:

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

fd 1,2 not a tty:

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

fake cap, tput exits non-zero:

$ echo $(tput kol 2> /dev/null || echo 100)
100
Milag
  • 1,793
  • 2
  • 9
  • 8
1

Actually the answer of Milag solves the riddle! Well, it's a bit complicated and the real answer is a bit hidden within all details given there, so I'm providing a simpler to understand reply here for the interested reader, yet kudos goes to Milag, thus I will accept this answer (also for the reputation).

In simple words, here is what's going on:

tput cols requires a tty to get the real terminal width. When I open a terminal window, both stdout and stderr are ttys, thus

tput cols

prints a correct result. If I now redirect stderr to /dev/null, as in

tput cols 2>/dev/null

then stderr is no longer a tty, it is a char file. Yet this is no problem, as stdout is still a tty.

However, if I capture the output in a variable, as in

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

stdout is no tty either any longer, it is a pipe to the shell which captures the output of the command. Now tput has no tty at all anymore and thus cannot obtain the real width any longer, so it uses some fallback and this fallback reports 80 on my system (other systems may have better fallback mechanisms and still report the correct value).

So for my script, I will have to work around this issue a bit:

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

The first if checks if tput knows the cols argument, without producing any visible output at all or capturing anything, I just want to know what the exit code is. If it does support this argument, I capture the output, otherwise I use the fallback directly.

Mecki
  • 125,244
  • 33
  • 244
  • 253