1

I'm using less to capture the output of a live program. Using F activates follow mode, showing me real-time output. However, after entering follow mode (or, more generally, scrolling past the buffered output, with G, for example), I'm not able to control less anymore.

Ctrl-c kind of works here, but unfortunately it kills the other program.

Is there a way to regain control of less after scrolling past the buffer?

1
  • As a workaround: why don't you redirect to a file instead of piping to less? Then you could use less on the file in another terminal. btw the OS and shell you are using may be helpful in your question.
    – Furty
    May 1, 2020 at 8:54

1 Answer 1

4

Simple case

In my Debian 9, if I run less to read a regular file, e.g.:

less /etc/fstab

and then pass F (i.e. Shift+F) then I will see

Waiting for data... (interrupt to abort)

This indicates an interrupt is by design the right way to "undo" F. Indeed Ctrl+C does this.


Your case

I'm using less to capture the output of a live program.

I'm guessing it's like this:

program1 | less
# or
program1 | program2 | … | less

Now the problem arises (Ctrl+C kills the other program) because the following happens:

  1. The shell places every part of the pipeline (i.e. less and every programN) in a process group with PGID (process group ID) equal to the PID (process ID) of the first command (i.e. program1).
  2. The shell notifies the terminal this PGID is now the foreground process group.
  3. When you hit Ctrl+C, the terminal sends SIGINT to the foreground process group.
  4. In effect less and every programN receive SIGINT.

See this answer and this blog entry. A remotely related interesting case is in this question.

Some really basic shells (e.g. posh) may run everything in the process group of the shell itself. This does not affect the problem (less and every programN receive SIGINT), but can invalidate some solutions. Your shell is most likely "sophisticated enough". In my Debian even sh from busybox is sophisticated enough.


Solutions

You want less and less only to receive SIGINT. Possibilities:

  1. Send SIGINT only to less:

     kill -s SIGINT $pid_of_less
    

where $pid_of_less denotes PID of the right less process (you need to find it somehow). If you're sure the right less is the only less you are allowed to signal or if you don't mind signalling other less processes then this is the simplest way:

    killall -s SIGINT less

This solution should always work. It does not require you to modify the pipeline beforehand.

You can run kill (or killall) in a separate shell (separate console) or (if your shell allows this) suspend the pipeline with Ctrl+Z, run whatever you need and finally bring the pipeline back to the foreground with fg.

  1. Make everything but less immune to SIGINT. Few possibilities:

     sh -c 'trap "" SIGINT; program1' | less
     sh -c 'trap "" SIGINT; exec program1' | less
     sh -c 'trap "" SIGINT; program1 | program2 | …' | less
    
     ( trap "" SIGINT; program1 ) | less
     ( trap "" SIGINT; exec program1 ) | less
     ( trap "" SIGINT; program1 | program2 | … ) | less
    

Notes and quirks:

  • Examples with sh -c … require additional level of quoting. They may be inconvenient if there are quotes in your original line. If there's anything that should be expanded in the main shell then single-quotes cannot embrace it (the outer quotes matter). It may get complicated.

  • Examples with sh -c … require syntax compatible with sh. If your original line uses syntax or tools (like builtins) that sh cannot understand, then you should consider other possibilities.

  • Examples with subshells (( … )) use whatever shell you use. It will most likely understand trap "" SIGINT and work as desired, but in general it may not.

  • Each programN on its own can register a handler for SIGINT. Note less does it, so we can even start it from a shell with SIGINT ignored:

         ( trap "" SIGINT; program1 | less )
         # similarly with other examples
    

    and it will still be able to react to Ctrl+C. But if another program makes itself vulnerable again then it will defeat the purpose.

  • Any programN may not terminate when you exit less. It may be because

    • it doesn't know yet the pipe it writes to is closed, because it hasn't tried to write anything since the pipe was closed, so SIGPIPE has not been generated yet (it's normal, compare this answer of mine);
    • or by design it doesn't exit on SIGPIPE;
    • or it's a loop which runs command(s) that do exit on SIGPIPE, but the loop doesn't care and runs them again and again.

    In such case the easiest way to recover would be to hit Ctrl+C, but obviously if you make programN immune to SIGINT then this will not work for it. You will need to kill it with SIGTERM or another signal. Note just killing the subshell or the additional sh shell will not kill its children. Signalling the entire process group of the (sub)shell is a good way, like this:

         kill -- -$pgid
    

    where $pgid denotes the right process group ID (and - just before tells kill to target a group, not just one process). $pgid will be the PID of the (sub)shell, if the subshell was first in the pipe.

    You don't need to know any ID if job control is enabled in your interactive shell. E.g. in Bash hit Ctrl+Z and then kill %+. This should kill the entire job.

  1. Place everything but less outside of the foreground process group.

  2. With setsid:

        setsid -w program1 | less
        # or
        setsid -w sh -c 'program1 | program2 | …' | less
    

    As before, any programN may not terminate when less exits. As before, if this happens, you will need to kill it with a signal. The difference is this time SIGINT can be the signal. On the other hand the trick with kill %- will not work.

  3. With process substitution, if your shell supports it. I will elaborate for Bash.

        # in Bash
        less -f <(program1)
        # or
        less -f <(program1 | program2 | …)
    

    And again any programN may not terminate when you exit less, unfortunately. But note anything in <( ) starts in the process group of the original shell, so you can send SIGINT to it with Ctrl+C as soon as the shell registers its own process group as the foreground group again. In a script this could be a problem, but in an interactive shell it's enough to hit Ctrl+C when you get the prompt after less exits. Yes, in this case Ctrl+C will send SIGINT to some processes that seem to work in the background (but they belong to the current foreground process group, so formally they are in the foreground).

    You can automate this: kill 0 will signal its own process group, so it's enough to run kill in the process group of the shell and this is very easy. Like this:

        # in Bash
        less -f <(program1); kill -s SIGINT 0
        # or
        less -f <(program1 | program2 | …); kill -s SIGINT 0
    

    The above solution is quite elegant. Remember kill here is only to signal programs that would otherwise remain. If your programs exit by themselves anyway then you may not run kill at all. On the other hand if there are other programs in the process group of the shell (e.g. you did exec 3< <(whatever)) then kill 0 will signal them as well.

    Note about Zsh: my tests indicate that in Zsh you can use less -f <( … ) and prevent Ctrl+C from sending SIGINT to programs, just like in Bash; but Zsh will run them in yet another process group (not the process group of the shell), so the trick with kill 0 will not work.

    Also note this will not work:

        # not a solution
        less < <(program1)
        # or
        less < <(program1 | program2 | …)
    

    because this time everything inside <( ) will formally descend from less and share its process group, which will be the foreground process group, which will receive SIGINT upon Ctrl+C. This formal descendance from less happens because less originally starts as a subshell which sets redirections for itself and only then replaces itself with less (this is a normal way for shells to run programs). So in the process tree it will appear less spawned a shell which spawned program1 etc. (compare this question where sshd allegedly spawned sleep).

    And while in this case Ctrl+C can reach each programN, any programN may still not terminate when you exit less without Ctrl+C. Then the trick with kill 0 will not work. Totally not a solution.

  4. With separate invocations.

    Invoke less separately in a way so it's the only command in the foreground process group and no other process will receive SIGINT upon Ctrl+C. You need a named pipe or a regular file to pass data from program1 … to less.

    1. Create a file:

          mkfifo myfile    # of the type named fifo
          # or
          : > myfile       # of the type regular file
      
    2. Run program1 … and let it write to the file

    • either in a separate shell, separate terminal (not the one where you're going to run less):

              program1 … >> myfile
      
    • or in the background in a terminal where you're going to run less (note this is not a valid solution for shells that run everything in the process group of the shell itself – really basic shells, remember?):

              program1 … >> myfile &
      

      I used >>, not >. For a fifo it doesn't matter. For a regular file it allows you to truncate the file later without issues (compare this answer).

    1. Run less:

          less -f myfile
          # or
          less < myfile
      

      (-f not needed in case of a regular file).

      Now Ctrl+C in this terminal will only affect less.

    2. If you exit less and the rest is still running, and you want it to stop, then

    • if it runs in the separate terminal, Ctrl+C there;
    • if it runs in the background in the same terminal: - fg, then Ctrl+C, - or kill -s SIGINT %+, if supported (specify different signal if needed), - or kill -s SIGINT -- -"$!" (specify different signal if needed).
    1. Remove the file:

          rm myfile
      

Notes

  • Running program1 in the background or inside <( ) will detach its stdin from the terminal. If it was going to be a problem, you would have experienced issues when less and the program tried to read from the terminal simultaneously. You reported no issues (or had already taken care and detached program1 from the terminal), so apparently the program doesn't read from the terminal. Therefore running in the background or inside <( ) should not break anything.
  • In my tests less, after successfully "undoing" F without killing anything (per any solution), eventually stops reading data. This blocks the rest of the pipe (usually; the writing programN may be designed not to wait, it may exit or whatever). This is rather expected, I'm mentioning this for the sake of less experienced users. To unblock I need to pass F again. But if I use a regular file as an interface between programN and less then the program will not block because of less (the filesystem may get full though).

You must log in to answer this question.

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