Skip to content

watch: Fix buggy line-deletion behavior with --no-linewrap

Justin Gottula requested to merge jgottula/procps:fix-nowrap-newlib into newlib

I used the --no-linewrap (-w) option for the first time today, watching some wide output that didn't quite fit in my tmux pane. Quickly I noticed a problem: while --no-linewrap did indeed eliminate the spillover of lines too long for the terminal "window" width, it also resulted in a bunch of lines from the program output being hidden entirely.

After some fiddling around, the exact problematic behavior appears to be as follows:

  1. Lines which would have wrapped (more than $COLUMNS chars long) are handled correctly.
  2. Lines which would not have wrapped (shorter than $COLUMNS) are printed; but then the next line is not printed! For long sequences of non-wrap-length lines, you get an every-other-line-is-visible sort of effect.

The logic underlying the problem seems to be this: in the run_command loop, if the x loop goes all the way to completion (meaning we've reached the right-side edge of the window area), there's a small block of code for --no-linewrap whose main purpose is to call find_eol, which eats input until it hits a newline (or EOF). Clearly this is intended to be done for lines that are too long, so that the excess characters are discarded and the input pointer is ready to go for the subsequent line.

However, this code isn't in any way conditional on the value of eolseen! Short/wouldn't-wrap lines will have encountered a newline character before exhausting the entire x loop, and therefore eolseen will be true. Long/would-wrap lines will not have encountered a newline when the x loop is exhausted, and so eolseen will be false.

Nevertheless, find_eol is called in both cases. For long lines, it does what it's meant to do. For short lines, the newline has already been encountered and dealt with, and so the actual effect of find_eol is to eat the entirety of the next line, all the way through to its newline, such that it isn't printed at all.

The conclusion to this overly-long description, is that I made the relevant block of code conditional on (!line_wrap && !eolseen), rather than solely !line_wrap. And, at least for my use case, this one-liner seems to magically make everything work properly.

(I haven't thought very hard about whether the reset_ansi and attrset calls in that same block should still be unconditional wrt eolseen. Maybe they should be? Not sure.)

To test, the following Python program may be run under watch -w:

#!/usr/bin/env python
import shutil
from itertools import chain, repeat
W = shutil.get_terminal_size().columns
below = lambda x,n: range(x-n, x)
above = lambda x,n: range(x+1, x+1+n)
for w in chain(below(W, 6), repeat(W, 4), above(W, 6)):
    print('w:{:>3d}[[{:=^{:d}s}]]'.format(w, '', w-len('w:xxx[[]]')))

It prints 6 lines that are shorter than the terminal width; then 4 lines that are the exact width of the terminal; and then 6 lines that are longer than the terminal is wide. It should print 16 lines total, and the latter 6 lines should be truncated, of course.

Without this patch, the test program will only appear to print the first, third, and fifth of the first 6 lines (the ones that are too short to wrap). The second, fourth, and sixth lines are eaten by find_eol and aren't displayed.

(And if you fiddle with the test program so the number of 'below' lines is an odd number, you'll see that the after the last of those lines is printed, the first of the exact-terminal-width lines is now omitted.)

Merge request reports