12

I tried the following command after watch this video on pipe shenanigans.

man -k . | dmenu -l 20 | awk '{print $1}' | xargs -r man -Tpdf | zathura -

It basically prints a list of manpages to dmenu for the user to select one of them, then it uses xargs to run man -Tpdf % (print to stdout a pdf of the manpage git from the xargs' input) and pass the pdf to a pdf reader (zathura).

The problem is that (as you can see in the video) the pdf reader starts even before I select one manpage in dmenu. And if I click Esc and select none, the pdf reader is still open showing no document at all.

How can I make the pdf reader (and any other command in a pipe chain) to only run when its input reach a end-of-file or when it receives an input at all? Or, alternatively, how can I make a pipe chain to stop after one of the chained commands returns a non-zero exit status (so that if dmenu returns an error for not selecting an option, the following commands are not run)?

Seninha
  • 1,035

3 Answers3

13

How can I make the pdf reader (and any other command in a pipe chain) to only run when its input reach a end-of-file or when it receives an input at all?

There is ifne (in Debian it's in moreutils package):

ifne runs the following command if and only if the standard input is not empty.

In your case:

… | ifne zathura -
  • Thanks for the answer, I did not know this command! This command (and the others in moreutils) should have been in the original Unix and specified by posix... It is such a basic and Unix-ish tool... – Seninha Apr 27 '19 at 23:10
  • @Seninha The simplicity of ifne is a little bit deceptive. Unix has no "pipe peek" operation, so ifne must actually read at least one byte before deciding to run the dependent command. That means it can't just do the test and exec the command, but must create another pipe, fork another process to run the dependent command, and copy the whole stream from the stdin pipe to the downstream pipe. If the "input empty" case is not common, ifne could easily cost more resources than it saves, on average. –  Apr 28 '19 at 04:14
  • 1
    @Wumpus.Q.Wumbley that's a myth -- you do not have to read any byte to determine if there's data in a pipe. See here. And on Linux you can actually peek data from a pipe (ie read the data without removing it). I've mentioned this and more in comments to a "canonical" answer here but they were removed by mods because they probably felt that those facts were like detracting from the awesomeness of the answer. –  Apr 28 '19 at 05:18
6

All commands in a pipeline starts pretty much at the same time. It's only the I/O over the pipe that synchronises them. Also, a pipe can only hold as much information as the pipe's buffer allows.

You can therefore not avoid running one stage of a pipeline, because

  1. the command in that stage is started as soon as all other stages are started anyway, and
  2. if the command did not consume the input that comes in over the pipe, it would block the previous stages of the pipeline.

Instead, write the output to a file while letting the pipeline finish. Then use that file.

Example (as a function taking one argument):

myman () {
    tmpfile=$( mktemp )

    if man -k "$1" | dmenu -l 20 | awk '{print $1}' | xargs -r man -Tpdf >"$tmpfile" && [ -s  "$tmpfile" ]
    then
        zathura "$tmpfile"
    fi

    rm -f "$tmpfile"
}

This additionally would not run the zathura program if the pipeline failed (the xargs part returned non-zero) or the generated file is empty.

In the bash shell, you could also want to set the pipefail shell option with set -o pipefail to have the pipeline return the exit status of the first command in the pipeline that fails. And you would want to make the tmpfile variable local:

myman () {
    local tmpfile=$( mktemp )

    if [ -o pipefail ]; then
        set -o pipefail
        trap 'set +o pipefail' RETURN
    fi

    if man -k "$1" | dmenu -l 20 | awk '{print $1}' | xargs -r man -Tpdf >"$tmpfile"
    then
        zathura "$tmpfile"
    fi

    rm -f "$tmpfile"
}

This sets the pipefail option for the duration of the function, if it wasn't already set, and then unsets it if needed. It gets rid of the -s test on the output file.

Kusalananda
  • 333,661
  • 1
    Why rm -f? Are you thinking of cases where the pipe changes the permissions of the tmpfile? – terdon Apr 27 '19 at 13:13
  • 2
    @terdon I'm thinking of cases where the temporary file is prematurely removed. rm -f would not error out if the file already was removed (possibly by zathura, I don't know). – Kusalananda Apr 27 '19 at 13:13
  • The first function does not work as expected: It will also make zathura show a black window, but now zathura runs after the pipeline finish, rather than running alongside the pipeline. This is because the pipeline returns the exit status of xargs, which is 0. The command that fails in the pipeline is dmenu (which returns 1 when I select nothing). The bash function with the pipefail option works as expected (and in zsh too, which has the same option). – Seninha Apr 27 '19 at 19:19
  • 1
    @Seninha I fixed the first function by letting it check whether the generated file is non-empty. – Kusalananda Apr 27 '19 at 19:27
6

Pdf files are supposed to be seekable; any pdf viewer will have to look first at the trailer and from there jump to the offsets from the xref table.

Since pipes are not seekable, zathura is using an obfuscating trick, where it's copying all the input to a temporary file, and then use that temporary file as usually. This kind of "clever" trick is creating false hopes and is leading people to assume that pdf files are streamable.

But anyways, zathura really does wait for the EOF before displaying the document, you don't have to do anything for that to hapen:

(sleep 10; cat file.pdf) | zathura -
# will really show the content of file.pdf after 10 seconds

The problem is that zathura has no option to only open the window if the file's OK, and exit with an error if that's not the case -- it will just stay there as if everything's OK:

$ dd if=file.pdf bs=50000 count=1 status=none | zathura -
error: could not open document  # its window still hanging around showing nothing

$ echo $?
0  # really?

So even if you're redirecting the output to a temporary file yourself, and are only running zathura if everything was OK, there's no guarantee that the user will not be served with a black window if zathura doesn't like the output for one reason or another.


Btw,

man -X man

will display a manpage in an X11 window with gxditview, even if it looks straight out of the '70 ;-)

And, of course, you could always use:

... | xargs xterm -e man

which, besides many other enhancements, will let you use regular expressions in searches and proper text selection.