How to overwrite each line of STDOUT with the next one?
I wrote a filter program to overwrite each line with the next one.
$ cat /usr/local/bin/ovr
#!/bin/sh
sed '2,$s/^/\x1B[1A\x1B[K/';
Here's the behavior:
$ echo | ovr
$ echo 'foo' | ovr
foo
$ echo -e 'foo\nbar' | ovr
bar
$ echo -e 'foo\nbar\nbaz' | ovr
baz
$ echo -e 'foo\nbar\nbaz' | ovr | wc -l
3
It is useful for example when compiling a large program, where you might not want to pollute your screen with so many lines. For example, in build systems like the Linux kerenl one, for each file that's compiled, one line goes to stdout, of the form CC some/file.o
.
I usually compile it with make -j24 | ts -s
, to know for how long it's been running, from which I can guess how much is remaining. But I don't really need to see all lines; just the last one. So I do the following.
$ time make -j24 | ts -s | ovr
00:00:04 CC drivers/pinctrl/intel/pinctrl-sunrisepoint.o
The line will be updating all the time.
This program has a some flaw, which I'd like to fix.
If the line is long (longer than the terminal width), the overwriting doesn't work well, and only overwrites the part of the lines that has been written in the last "visual" line in the terminal.
For an 80-col terminal, this is an example:
$ echo -e '123456789q123456789w123456789e123456789r123456789t123456789y123456789u123456789i123456789o\nfoo' | ovr
123456789q123456789w123456789e123456789r123456789t123456789y123456789u123456789ifoo
How can this bug be fixed? Is it possible to fix it?
3 answers
Instead of having new stdout lines overwrite the previous one, it seems better to specify that old stdout lines should be overwritten by anything that comes after them.
This has the benefit that stdout cannot overwrite stderr (or anything else that goes in the tty).
#!/bin/sh
setterm --linewrap off;
sed -e '$ ! s/$/\x1B[1A/' \
-e '2,$ s/^/\x1B[K/';
setterm --linewrap on;
This uses setterm --linewrap off
to reliably trim long lines, as learnt from this other answer.
$ echo | ovr --no-alt
$ echo foo | ovr --no-alt
foo
$ echo -e 'foo\nbar' | ovr --no-alt
bar
$ echo -e 'foo\nbar\nbaz' | ovr --no-alt
baz
$ echo -e 'foo\nbar\nbaz' | ovr --no-alt | wc -l
3
$ (echo foo; echo e >&2; echo bar; echo xxxxx >&2; echo bz) | ovr --no-alt
e
xxxxx
bz
$ echo -e '123456789q123456789w123456789e123456789r123456789t123456789y123456789u123456789i123456789o\nfoo' | ovr --no-alt
foo
$ echo '123456789q123456789w123456789e123456789r123456789t123456789y123456789u123456789i123456789o' | ovr --no-alt
123456789q123456789w123456789e123456789r123456789t123456789y123456789u123456789o
(I did actually implement it as an option to a script that can also use alternate screen as in this other answer, so I can choose; thus the --no-alt
option.)
This implementation is much simpler than using the alternate screen.
A disadvantage is that it's not possible to set the number of lines to keep. If that's wanted, you can pipe also to tail(1) with pee(1):
$ (echo foo; echo e >&2; echo bar; echo xxxxx >&2; echo bz) | pee "ovr --no-alt" "tail -n2"
e
xxxxx
bz
bar
bz
0 comment threads
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
alx | (no comment) | Oct 9, 2023 at 01:14 |
This other answer tries to truncate each line to make it fit the width of the terminal. It's hard to do this reliably because e.g. a tab character counts as one, but it looks like several spaces; on the other hand unprintable characters and escape sequences do not show at all.
My answer is a completely different approach. The idea is to print to the alternate screen. The alternate screen is used by less
, vi
, nano
and many similar programs. When such program exits, there is no trace of its output.
The below script switches the terminal to the alternate screen and pipes its input there. Eventually it switches the terminal back. Then it prints (repeats) one last line to its stdout, this is the line that will remain.
#!/bin/sh
trap '' INT
enter() { setterm --linewrap off; tput smcup; } >/dev/tty
leave() { setterm --linewrap on; tput rmcup; } >/dev/tty
trap leave QUIT TERM
n="${1:-1}"
enter
lines="$(tee /dev/tty | tail -n "$n"; echo x)"
leave
printf '%s' "${lines%x}"
Notes:
-
The script uses at most one argument. If specified, the argument is passed to
tail -n
. This way you can get more than one last line in your normal screen. If not specified,1
is used. -
tput smcup
andtput rmcup
are responsible for entering and leaving the alternate screen respectively. -
setterm --linewrap off
tells the terminal not to wrap long lines. You may prefer not usingsetterm --linewrap off
andsetterm --linewrap on
in the script. Delete them at will. -
The last line(s) is printed (repeated) outside of the alternate screen in full, i.e. with wrapping.
-
Tools that read from the terminal (
sudo
asking for password, simple y/n prompts) should work in the alternate screen. -
The script will not capture your input (like an answer to a y/n prompt) to print (repeat) it after leaving the alternate screen, even if the input is echoed inside the alternate screen and seems to belong to the last line(s).
-
Tools that print directly to the terminal (
/dev/tty
) or to their stdout (which happens to be the terminal) should work in the alternate screen, but such lines will not be among lines captured by the script and repeated after leaving the alternate screen. Inside the alternate screen such lines may seem out of sync with respect to stdout because only stdout is "delayed" and buffered aroundtee
. -
If you want, you can merge stderr with stdout and pipe the merged stream to the script:
something 2>&1 | ovr
. This way stdout and stderr will stay in sync. -
The script works reasonably well when the pipeline gets interrupted with Ctrl+c.
-
Ctrl+z is somewhat troublesome because the script will stop without leaving the alternate screen. If you make it continue in the foreground (
fg
) then it will leave the alternate screen eventually like it normally would. If you kill it with SIGTERM (kill %%
) then it will leave the alternate screen because of the trap.
4 comment threads
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
alx | (no comment) | Oct 2, 2023 at 23:49 |
In Bash, you could use the $COLUMNS
environment variable to detect the width of your terminal and truncate each line to that length in your sed script. Something like this should work:
sed "s/^\(.\{,$COLUMNS\}\).*$/\1/;2,\$s/^/\x1B[1A\x1B[K/"
For a sh-compatible alternative, replace $COLUMNS
with $(tput cols)
.
0 comment threads