Search code examples
bashstty

Bash: pause and continue execution with spacebar


I am running code that produces a huge amount of output during more than an hour. During execution, I am logging my output to a file using tee. However, the huge amount of output that is still sent to my terminal is a bit cumbersome, so I made a small function ShowLastLines whereto I can pipe my output, that will show only the last 15 lines of output and updates in realtime.

This is the script:

function ShowLastLines {
    numberoflines=15
    i=1
    while read var
    do
        echo $var
        if [ $i -gt $numberoflines ]
        then
            tput sc && tput cuu $(( $numberoflines +1 )) && tput dl 1 && tput rc && tput cuu 1
        fi
        (( i++ ))
    done
}

In practice I use it like so:

( long code ) 2>&1 | tee output.log | ShowLastLines

This works nicely as expected, however I'd like to add one more functionality, namely I want to pause the output (in ShowLastLines, of course not in the original execution) when I hit spacebar (e.g. to investigate some detail I noticed), and let it continue when I hit spacebar again (where it will catch up with the execution output).

I tried a few things, but so far have not succeeded. I believe it might possibly be done by putting the standard input in non-blocking mode by using stty (as in this thread):

stty -echo -icanon time 0 min 0

but I don't manage to combine it with my existing read.

An important side remark: I am on an AFS cluster, so I cannot access the log file (written by tee) until the calculation has finished due to how the synchronisation between the different machines is implemented (I know that otherwise it would have been easier to use a tail -f on the log file).

Ps: the most useful implementation would be that, when the output is paused by hitting spacebar, it is possible to scroll up within the output to also show the lines before. Any suggestions are welcome, but I realise that this probably requires a totally different implementation that might add a lot of calculational overhead, so I am happy enough with a solution that gives a simple pause by spacebar.


Solution

  • This would probably be best done with an actual program using select() to read the input from the pipe and the user at the same time.

    But, in a Bash script, you could use read with a small timeout to check if the user has hit a key:

    buf() {
        stty -echo < /dev/tty
        while read line ; do
            echo "$line"
            if read -n1 -t0.0001 -u3 3</dev/tty ; then 
                echo paused.
                read -n1 -u3 3</dev/tty
            fi
        done
        stty echo < /dev/tty
    }
    

    read -n1 will not wait for a whole line, but will return immediately after a character has been received. We need to read from /dev/tty (stderr might also do), since we expect the piped input from stdin, as in somecmd | buf.

    The downside here is that if you stop the output for too long, the pipeline will stall and stop the process producing the output. You can work around this by using pv (say, ...| pv -qB 64k |...) in the pipeline to act as a buffer. Though viewing the output that was sent while the reading side was paused is somewhat difficult.

    As mentioned in the comments, you could also use the terminal emulator's buffering to do this, too. That would have the same downside that stopping the output for too long would again stall the writing side of the pipe.