Restore interactive commands after accidentally overpaging pipe input to less

lesspagerpipesignalsstdin

Consider a script like this:

$ cat example.sh
#! /usr/bin/env bash

for i in {1..90}
do
    printf '%s\n' "$i"
done
sleep 10
printf '91\n'
sleep 10
printf 'done\n'

and suppose the output is piped to less, like so:

$ bash example.sh | less

If I scroll down as far as line 90,
I can scroll back up again, search,
and use any other interactive commands that less provides.
However, as soon as I try to go past line 90 with e.g. j or Ctrl-N,
less stops responding to interactive commands
until another line of input is available.
And if I try to scroll a full pageful past line 90 with e.g. spacebar,
less stops responding to interactive commands
until a full page of input is available
or it receives EOF.

This can be undesirable if I want to look at previous output
and don't realize I have just gone over the available lines
and must wait for more lines to appear,
which could require an arbitrary amount of time.

If I use Ctrl-C to send SIGINT,
I can immediately get interactivity again,
but then less will stop listening for more input from the pipe.

The script is just an easily reproducible example,
but it could be replaced by any long-running command
that slowly generates lines of output, such as finding broken symbolic links:

$ find $HOME -xtype l | less

or world-readable permissions in my home directory:

$ find $HOME -perm 777 | less

or any number of other slow, resource-intensive commands that send lines to stdout.

Is there any way I can tell less to stop waiting for more input
and regain interactive commands
without waiting for the required lines of input
to be generated from the pipe?

Best Answer

In

bash example.sh | less

The whole pipeline is put in foreground (the foreground process group has both the process running bash and all the processes it spawns itself, and less), so when you press Ctrl + C, all of bash, sleep and less receive the SIGINT.

sleep will die upon receiving it, so will bash¹. less intercepts that SIGINT where it's handled as cancel current action.

But if bash hadn't died, you would have been able to resume reading inside less.

So what you can do is prevent bash from dying of SIGINT:

(trap '' INT; bash example.sh) | less

Then you'll be able to use Ctrl + C to interrupt less without affecting bash and the processes launched by that script.

To stop the script, you can use Ctrl + \ which sends a SIGQUIT (beware it may cause a core dump if you haven't set the core dump size limit to 0), or Ctrl + Z to suspend the job (SIGTSTP) followed by kill % to kill it (with SIGTERM).


¹ here, as bash realised sleep died of a SIGINT; it would have been different if sleep had handled that SIGINT and exited normally, that's a special SIGINT handling done by bash and a a few other shells.

Related Question