2

I have a Bash function to display man pages rendered as postscript, in a PDF:

function psman () {
    man -t "$@" | ps2pdf - /tmp/manpage.pdf
    evince /tmp/manpage.pdf
}

(Update: I stripped out peripheral complications like dynamically generating the temp file name, and using 'nohup')

This works fine. For a screenshot of it in use, see https://www.tartley.com/postscript-formatted-man-pages.

For my own edification, I tried to implement it without using tempfiles. For example, using process substitution:

$ evince <(man -t ls | ps2pdf - -)

This doesn't work. Evince displays an error in its GUI:

Unable to open document "file:///dev/fd/63".
PDF document is damaged

Why? How can I generate and view the PDF without generating any intermediate files?

The error message above is different than the messages evince shows for missing or empty files, so it's not simply that.

Update: To get more info, I tried replacing 'evince' with 'ls':

$ ls -l <(man -t ls | ps2pdf - -)
lr-x------. 1 jhartley jhartley 64 Aug 23 08:59 /dev/fd/63 -> pipe:[196475]

where dircolors is coloring:

  • /dev/fd/63 as 'ORPHAN' (a symbolic link that points to a nonexistent file), and
  • pipe:[196475] as 'MISSING' (a nonexistent file pointed to by a symbolic link)

So maybe evince is just being given a link pointing to a file that doesn't exist? To mimic this, I created a symbolic link that points to a nonexistent file, then opened it with 'evince'. But instead of the 'PDF is damaged' message above, this gives me "No such file or directory."

Update: I think the ORPHAN/MISSING filetypes are a red herring. I see the same ORPHAN/MISSING symlink when doing a very simple process substitution:

$ ls -l <( echo 123 )

and using the same man|ps2pdt pipeline works fine when the process substitution is fed to diff:

$ diff <(man -t ls | ps2pdf - - | tr "\0" "0") <(man -t ls | ps2pdf - - | tr "\0" "0")
248c248
< /ID [<95A81B38FAE8E6FE3C899586A1DEE861><95A81B38FAE8E6FE3C899586A1DEE861>]
---
> /ID [<2F9164BD9265C8540A4A8E7068076344><2F9164BD9265C8540A4A8E7068076344>]

(Here I added 'tr' to the pipelines to eliminate null/zero characters in the pdf output, so that diff would treat the files as textual instead of binary.)

So, in summary, I've no idea why I get the "PDF is damaged" error above. My goal, other than understanding, is to view the generated PDF without generating any files along the way.

4
  • I'm starting to think my problem is related to the way evince itself opens and reads from files. Other tools (such as 'diff' as described above) seem to open the filename resulting from my process substitution without problem. Aug 23, 2017 at 15:12
  • I notice that evince <( cat man-ls.pdf ) opens without errors, displaying 4 pages (the correct number), but all the pages are blank. Like it's partly read the file successfully, but then failed at some point. Aug 23, 2017 at 16:43
  • I think perhaps I should have posed this on unix.stackexchange.com Aug 23, 2017 at 16:46
  • I can't get evince <( cat test.pdf ) to work. It responds "Unable to open document file:///dev/fd/63" mupdf is also failing.
    – Diagon
    Jun 7, 2020 at 10:55

3 Answers 3

3

Just a guess, but plausible one:

evince seeks through the "file", the stream it gets is not seekable. Compare Why does BASH process substitution not work with some commands?

This means it's (nearly?) impossible to achieve what you want without any intermediate file. The best I can think of is a script like this:

#!/bin/bash

tmpd="/dev/shm"

( tmpf="$(mktemp -p "$tmpd" "tmp [man $*] XXX.pdf")"
man -t "$@" | ps2pdf - > "$tmpf"
evince "$tmpf"
rm "$tmpf" ) 2>/dev/null &

Remarks, pitfalls etc.:

  1. When $tmpd is /dev/shm, a temporary file is created in memory. I guess it's as close to "without generating any intermediate files" as you can easily get, while keeping it seekable.
  2. Regardless of where it is, we should remove it afterwards. If the script is interrupted (e.g. with Ctrl + C) between mktemp and rm, the file survives and we don't want it. There are few approaches to this problem, you can trap signals if you want; I chose to run the entire sequence in background (( … ) &) which may be good enough.
  3. My evince won't open a file from /dev/shm unless its name ends with .pdf (this behavior is case insensitive). That's why there is .pdf in the filename template. There is no such problem in /tmp. Why? I don't know.
  4. The filename template is created with $* in it to make it somewhat meaningful (it shows in the title of evince window).
1
  • This makes a lot of sense. Thanks for the link. I learned something today. Aug 23, 2017 at 16:42
2

PDF files are a collection of inter-related objects, identified with ids. At the end of the file, there is an index to the objects, which maps ids to file offsets. It's really impossible to use a PDF file without this index, so the usual approach to reading a PDF file is to seek to close to the end and try to find the beginning of the index, which is then read into memory. The index indicates which object is the root object, and from there you can walk through the object graph, always using the index to find the file offset of each related object.

In theory you could read (or mmap) the entire file into memory, but that wouldn't work with really big files and PDF is intended to be able to cope with really big files (and, indeed, print quality PDF files can be really big). So seeking is an intrinsic part of using a PDF file, and process substitution doesn't support seeking.

There are other command line applications which need to seek, or think that they do. (Sometimes the seek is just an attempt by the programmer to figure out how big the file is, for convenience.) There are other file formats which put an index at the end (such as Zip compression), and really do rely on seeking. Databases, for example, do not really even have a sense of linear reading, and probably no-one would even think of providing a database backing file through process substitution. But PDF is a kind of poster child for non-linear processing, and that is sometimes surprising.

-1

You only need to add the file name for example use:

(man -t ls | ps2pdf - ~/man_ls.pdf) > evince

This is going to create the man_ls.pdf file in your home directory

5
  • Thanks for the ideas, but I don't understand yet. Are you sure you meant '>' near the end of that bash? It writes an empty file called 'evince' Aug 22, 2017 at 20:16
  • Remember, my goal is to run the program called 'evince' (a gnome PDF viewer) on the PDF, without writing any files at all along the way. Aug 22, 2017 at 20:16
  • My apologies. I'm going to mark this answer down because the command doesn't work, and the explanation doesn't appear to address my question at all. Apologies if I'm misinterpreting. Aug 23, 2017 at 14:27
  • Why are you attempting to write a pdf file without using any file? you must store somewhere the information, if you are not trying to get temp files or any files what is your approach? Aug 23, 2017 at 14:51
  • Hey Genaro. The Bash approach to doing this is a feature called process substitution, using the cmd1 <( cmd2 ) syntax. The stdout of cmd2 (in my example, ps2pdf) goes into a pipe, and that pipe is given a name on the filesystem, and that name is passed to cmd1 (in my example, evince). cmd1 can open the filename it was given, read it, and gets the stdout of cmd2. Neither comand has any idea that process substitution is being used. However, at no point does Bash write bytes to disk. This is all in-memory, much like redirection & pipes. The point is for performance, & personal education. Aug 23, 2017 at 15:04

You must log in to answer this question.

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