5

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?

1

3 Answers 3

3

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.

0

If I understand your question correctly, your desired behavior is to interrupt sleep and return to the interactive vim-like mode with : .

The answer lies in how the sleep functionality is handled, as sleep is not a regular shell command in itself, but a pretty special command that creates a dummy job that executes for a specific time, and accordingly this has to be taken into account with regards to interrupt handling. However, your behavior can be easily achieved by modifying your script as such.

#!/bin/bash 


trap 'kill $(jobs -p)' INT


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

sleep infinity &
wait

printf '91\n'

sleep 10 &
printf 'done\n'

I modified your original script a little so it sleeps for infinity for testing.

When you reach and then attempt to move past line 90, the sleep engages and runs forever. In order to exit it just hit CTRL+C , which thanks to the signal trap will not only interrupt our sleep job but give you back the desired : and its functionality (you may need to press a key after executing CTRL+C). Additionally, once you again try to move past line 90, line 91 will be printed immediately, as the script execution has moved past the first sleep job.

3
  • Apologies for being unclear, the script is just a proxy for any slow-running command that sends lines to stdout. So while this is interesting, it's not what I intended to be asking about. Commented Oct 21, 2019 at 2:58
  • I think the issue lies with using sleep in the example script. As that actually does not represent a slow running command quite well. I think you may be interested more in a situation where for example one of the lines of code in the script executes a long time consuming processs i.e. find / -iname '*.log' for example. Let me set that script up and see what I can cook up.
    – NetIceCat
    Commented Oct 21, 2019 at 3:12
  • @BarBar1234 regardless of the command, you won’t be able to fix this: the Ctrl+C kills the command feeding the pipe... Commented Oct 21, 2019 at 18:29
0

The least bad solution is to use setsid(1):

setsid bash example.sh | less

or

setsid find $HOME -xtype l | less

or

setsid find $HOME -perm 777 | less

"Least bad" both because setsid(1) as a command is a non-standard Linux-ism, so you will need to install it from your nearest package manager if it is not already available (it has the fact it's portable going for it), and because the command in question will escape job control as well, so if it gets really stuck you will have no choice but to locate it in ps x and use the kill command manually with the located PID.

There are alternatives, but they have their own portability or ergonomic headaches. cf: https://unix.stackexchange.com/a/736048/54340

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .