4

I will occasionally use the pickaxe functionality of git to locate changes of interest. This can be quite slow, obviously (the same would apply to, say, hg grep), but more significantly it is bursty: a few results clumped together, separated by inactivity.

For these reasons, I try and read the results as they come, in practice by piping the output to less (in fact, this is what git does by default): when a burst comes, you can't possibly hit ctrl-S fast enough for the software flow control functionality to be useful these days.

But I often hit a… behaviour of less where if I scroll enough that even just one line of content to show on screen has yet to come out of the git command, less gets stuck on that and I lose its prompt and most ways to control it, until more content comes. Now, I understand why it happens, but if that was the behaviour of a shell command, I'd use ctrl-Z to recover a prompt (and then do bg). Is there an equivalent in less to recover the pager prompt?

In this situation, ctrl-Z is useless: Unix job control is inappropriate for commands that manage the screen contents. Besides, I don't want to get back to the shell prompt anyway: typically, what I want to do is check back something I might have glimpsed while I was repeatedly hitting the space bar, so I don't want to leave less.

Ctrl-C is also useless: while it does allow me to recover a prompt, this will additionally result in the received content being frozen; that is, when my curiosity is satisfied and I try and get back to the latest content (with a few taps of the space bar, or G, etc.), nothing more than what I had comes up, no matter how long I wait. Even with F (tail -f mode).

I use this command to simulate the git behaviour:

(echo -e 'foo\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.' ; sleep 3 ; echo -e 'bar\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.' ; sleep 10; echo baz) | less

1 Answer 1

5

The solution is in fact to use ctrl-C, but simultaneously make sure the resulting signal is not sent to the other command(s) that pipe to less. That was the decisive insight I failed to initially get: while only less performs reads from the terminal, so ordinary input makes it to that command only, a signal originating from the terminal driver, on the other hand, is dispatched to all processes in the pipeline.

Controlling signal dispatch in this way is easier said than done. To the best of my knowledge, there is no good solution, so I'm presenting the least bad one: use setsid(1) to isolate the other commands to their own session:

% setsid sh -c "/bin/echo -e 'foo\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.' ; sleep 3 ; /bin/echo -e 'bar\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.' ; sleep 10; /bin/echo baz" sh-c | less

With this setup, when I hit ctrl-C I get the expected behaviour: the less prompt appears just after what it could receive before the sleep and I can browse back to everything before that, and when I direct to get back to the latest content, the last of it (baz, here) eventually shows up. By stuffing content production to its own session, I shield that process from the signal (that I need to involve in order to interrupt the reading operation in which less would otherwise be stuck).

The main catch is that setsid(1) is completely nonstandard; while all Unix and Unix-like kernels support the setsid(2) syscall, its availability as a standalone command is something of a Linux-ism. If it isn't available on your install, your favorite package manager ought to provide it (the source is simple and in the public domain), either by itself or as part of a util-linux package:

% brew install util-linux # MacOS

Don't be misled by the name: setsid(1), at least, is portable. (Note: the util-linux cask from Homebrew shown here is keg-only, i.e. nothing is installed in the search paths because of collisions; you will have to manually add its bin directory to $PATH)

7
  • That sleep 5 at the start was bothering me, so I looked at ways to fix that. Here is what I found: Linux actually avoid the issue I mention on MacOS in a rather heavy-handed way: opening a FIFO for writing (without reading) blocks until someone else has the same FIFO open for reading, thus delaying the writes that could otherwise have been subject to SIGPIPE. So this solves the issue, but such rendez-vous-based synchronization is almost always problematic: for instance, if I needed to involve multiple FIFO, I would need to remember to open them in the same order, or I will deadlock. Commented Feb 20, 2023 at 0:52
  • But involving multiple FIFO is not (yet) the issue here. Since Linux also blocks opening a FIFO for reading only until someone else has it open for writing (MacOS also does that, but not the converse), the combination of the two behaviors means you can't do the two operations in succession! Even in a single command! You need to perform the two opens (>fifo and < fifo) in two parallel branches, the background and the foreground ones; but then you risk synchronization issues on other platforms. We definitely want to avoid examples that work on one platform but break unpredictably elsewhere. Commented Feb 20, 2023 at 1:00
  • To obviate such cross-platform mismatches, portable scripts will open the FIFO while execution is still serial, but do so in a read-write manner, which never blocks. But this is incompatible with defensive programming: I definitely don't want my command to think it could read from the fil descriptor provided. Worse, if less is provided with such a file descriptor, it will never recieve an end of file status… as there is still a writer left: itself. Commented Feb 20, 2023 at 1:08
  • In the end, I did find various workarounds to obtain the behavior I wanted, but they are all so ugly that I would rather stick with the sleep 5 until I have improved one of them to be acceptable. Commented Feb 20, 2023 at 1:12
  • For posterity, here is the solution I initially found for controlling signal dispatch: have the command of interest run in the background and have it write to a FIFO (named pipe), and have less open that: (sleep 5; ( echo -e 'foo\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.' ; sleep 3 ; echo -e 'bar\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.' ; sleep 10; echo baz ) > fifo ) & less < fifo As you can see, there is one complication… Commented Feb 20, 2023 at 22:02

You must log in to answer this question.

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