unix

Man Page PDFs Redux

Last year I posted a script that could be used in place of the Unix "man" command to generate PDFs of the man pages in question. If Mint is to be believed, this continues to be a fairly popular page, so here's an update on the old version.

When I posted it, Tiger (Mac OS X 10.4) was still current. After Leopard (10.5) came out I found it necessary to update the script slightly. Not because the old one didn't work, but because Leopard brought many updated man pages with it. The caching mechanism used by the script had no way to detect that the cached PDF was out of date relative to the man page it was based on.

Anyway, here's an updated version of the script. PDFs are cached, as before, but are re-generated if the cached PDF is stale.

As with the previous version, you can use it just like "man": "manpdf time" and "manpdf 3 time", for example, differ from the stock "man" command only in that a PDF is generated.

Source is below, you can also download it directly.

#!/bin/bash

# Convert man pages to PDF and open them in the default PDF viewer.
# PDFs are cached when created.
# By Tom Harrington, tph at atomicbird dot com, 16 March 2005

# Directory to save cached PDFs in 
# (if you don't want long-term caching, you could use /tmp/).
CACHEDIR=~/Library/Caches

# Command to read PS from stdin and write PDF to an output file.
PS2PDF_CMD="/usr/bin/pstopdf -i -o"
# Command to open the resulting PDF once it's created.
OPEN_CMD="/usr/bin/open"
# Path to "man", which is expected to take the -w, -S, and -t args.
MAN_CMD="/usr/bin/man"

CACHEDIR="$CACHEDIR/manpdf"
if [ ! -e $CACHEDIR ]; then
	mkdir -p $CACHEDIR
fi

if [ $# -eq 1 ]
then
	MANSECT=""
	MANARG=$1
elif [ $# -eq 2 ]
then
	MANSECT=$1
	MANARG=$2
else
	echo "Usage: " `basename $0` "[section] name"
	exit
fi

if [ "$MANSECT" != "" ]; then
	MANSECTARG="-S $MANSECT"
fi

# Look for the command's man page.
MANFILE=`$MAN_CMD $MANSECTARG -w $MANARG 2>/dev/null`
if [ -z $MANFILE ]; then
	echo No manual entry for $MANARG
	exit
fi
# OK, got the man page.  Now get the name for the corresponding
# PDF.  The goal here is to organize cached PDFs in CACHEDIR
# according to man-page section, similar to the way /usr/share/man
# is organized into "man1", "man3", etc.
echo file: $MANFILE
MANSECT=`echo "$MANFILE" | sed 's/.*\.\([0-9]\).*/\1/'`
MANPDF=$CACHEDIR/pdf$MANSECT/$MANARG.$MANSECT.pdf
FINALDIR=`dirname $MANPDF`
echo PDF: $MANPDF
# Create the PDF if it's not in the cache dir or if the cached version is obsolete.
CREATEPDF=0
if [ ! -e $MANPDF ]; then
	echo Generating man page...
	CREATEPDF=1
else
	if [ $MANFILE -nt $MANPDF ]; then
		echo Man page newer than PDF, regenerating...
		CREATEPDF=1
	fi
fi

if [ $CREATEPDF -eq 1 ]; then
	mkdir -p $FINALDIR
	$MAN_CMD $MANSECTARG -t $MANARG | $PS2PDF_CMD $MANPDF
else
	echo Reading man page from cache...
fi
# Now open the PDF.
$OPEN_CMD $MANPDF

Man page PDFs

Jon Wight posted a script that gives you a command-line tool for opening man pages in Xcode's documentation viewer. There are lots of man-page viewers for Mac OS X but most work as Mac applications, leaving the command line behind. That's fine for some but not for longtime Unix geeks like me who spend a lot more quality time with Terminal.app than is probably healthy on a Mac.

So I thought I'd add my contribution, a script I wrote a while ago that renders man pages as PDFs and then opens them in Preview (or whatever your default PDF viewer is). The script is called "manpdf" (original). Use it as you would "man"-- for example either "manpdf time", or with a section number as "manpdf 3 time" (if you try those with plain "man" you'll see why adding a section number is useful).

Since there's a certain amount of overhead in rendering a man page to PDF, this script caches the PDFs once they're created. Later calls with the same arguments should be nearly instantaneous, even for big man pages like gcc's. The default cache location is ~/Library/Caches/manpdf. If you don't care about caching you could set that to /private/tmp/, or if you use multiple accounts on your Mac you might set it to /Users/Shared/.

This could be modified to use HTML, but the default HTML rendering is awful and I'm not sure how to insert improvements into the flow.

Update: A newer version of this script can be found in a later post.

#!/bin/bash

# Convert man pages to PDF and open them in the default PDF viewer.
# PDFs are cached when created.
# By Tom Harrington, tph at atomicbird dot com, 16 March 2005

# Directory to save cached PDFs in 
# (if you don't want long-term caching, you could use /tmp/).
CACHEDIR=~/Library/Caches

# Command to read PS from stdin and write PDF to an output file.
PS2PDF_CMD="/usr/bin/pstopdf -i -o"
# Command to open the resulting PDF once it's created.
OPEN_CMD="/usr/bin/open"
# Path to "man", which is expected to take the -w, -S, and -t args.
MAN_CMD="/usr/bin/man"

CACHEDIR="$CACHEDIR/manpdf"
if [ ! -e $CACHEDIR ]; then
	mkdir -p $CACHEDIR
fi

if [ $# -eq 1 ]
then
	MANSECT=""
	MANARG=$1
elif [ $# -eq 2 ]
then
	MANSECT=$1
	MANARG=$2
else
	echo "Usage: " `basename $0` "[section] name"
	exit
fi

if [ "$MANSECT" != "" ]; then
	MANSECTARG="-S $MANSECT"
fi

# Look for the command's man page.
MANFILE=`$MAN_CMD $MANSECTARG -w $MANARG 2>/dev/null`
if [ -z $MANFILE ]; then
	echo No manual entry for $MANARG
	exit
fi
# OK, got the man page.  Now get the name for the corresponding
# PDF.  The goal here is to organize cached PDFs in CACHEDIR
# according to man-page section, similar to the way /usr/share/man
# is organized into "man1", "man3", etc.
echo file: $MANFILE
MANSECT=`echo "$MANFILE" | sed 's/.*\.\([0-9]\)/\1/'`
MANPDF=$CACHEDIR/pdf$MANSECT/$MANARG.$MANSECT.pdf
FINALDIR=`dirname $MANPDF`
echo PDF: $MANPDF
# Create the PDF if it's not in the cache dir.
if [ ! -e $MANPDF ]; then
	echo Generating man page...
	mkdir -p $FINALDIR
	$MAN_CMD $MANSECTARG -t $MANARG | $PS2PDF_CMD $MANPDF
else
	echo Reading man page from cache...
fi
# Now open the PDF.
$OPEN_CMD $MANPDF

Jon's script has the advantage that Xcode's viewer includes hyperlinks to cross-referenced man pages. That can be really handy. Mine however doesn't require Xcode, which can also be really nice if you aren't working with Xcode but still want something nicer than ASCII rendering of the man page.


Getting the system boot time

Recently someone in comp.sys.mac.programmer.help asked about code to find the system boot time on Mac OS X. Every so often I find a question like that and I just get the urge to figure it out.

He mentioned that he'd tried using getutxent(3) but that apparently it doesn't work on Mac OS X. I suspect this is because it relies on the user accounting database, and that this is not enabled. This approach might well have worked on Mac OS X Server.

This is the kind of thing that immediately suggests sysctl(3) to me, since sysctl knows all (or seems to). There's probably a solution buried somewhere in Carbon too, but a quick check at the command line confirmed I was on the right trail so I didn't look much farther:

$ sysctl -a | grep boot
kern.exec: unknown type returned
kern.boottime = Wed May  9 09:22:28 2007
kern.netboot = 0
kern.bootsignature: 48e4380a2edc32cb41fcd951c7d7882529f3bddf

Now to get it into code without requiring an external tool.

With sysctl() my first instinct is to use sysctlbyname(3) instead, since it's generally much easier to deal with. That's one of the methods SparklePlus uses to look up information about the host Mac-- the CPU type (Intel vs. PPC), CPU subtype (G5, G4, etc), CPU model (a unique string identifying the model of Mac), and number of CPUs are all looked up this way.

Since the sysctl(3) man page reveals that the KERN_BOOTTIME parameter is a struct timeval, this should be a simple matter of:

struct timeval boottime;
int error;
unsigned long length = sizeof(boottime);
error = sysctlbyname("kern.boottime", &boottime, &length,
	NULL, 0);

For some reason this always fails-- error is -1 and errno is ENOENT, suggesting I've passed an invalid name. Since the sysctl command-line tool accepts kern.boottime, I don't know why that's a problem.

Oh well, if the shortcut doesn't work, there's always the hard(er) way. It's not especially difficult but it seems a bit arcane until you get used to it.

time_t boottime()
{
	int mib[2] = { CTL_KERN, KERN_BOOTTIME };
	char *value;
	struct timeval boottime;
	size_t size = sizeof(boottime);

	if (sysctl(mib, 2, &boottime, &size, NULL, 0) == -1) {
		perror("sysctl(kern.boottime)");
		return -1;
	}
	printf("system boot time (seconds since epoch): %d\n",
		boottime.tv_sec);
	return boottime.tv_sec;
}

That's the short version, and as the printf indicates it gives you the boot time in terms of seconds since the epoch. From that you can convert it into whatever other format you like, so long as you remember when dealing with Unix the "epoch" is January 1, 1970 GMT, not the Mac OS X "reference date" of January 1 2001. A Cocoa developer might pass the result to NSDate's +dateWithTimeIntervalSince1970:, for example.

The above code could use some cleanup for production use, of course. However it should work on BSD systems other than Mac OS X.

As long as I'm on the subject I'll also include the longer approach to dealing with sysctl. This version looks up how much memory the sysctl result will need before actually getting that result. In this case I know the result is a struct timeval, so that's not necessary. In other cases you won't know the size requirements in advance. For example the hw.model key is a string of arbitrary length (probably up to a limit, but the limit's not documented).

The following does the same as the above, using this longer approach:

time_t boottime_long()
{
	int mib[2] = { CTL_KERN, KERN_BOOTTIME };
	char *value;
	size_t size;
	struct timeval boottime;

	// Pass NULL the first time so we just get size
	if (sysctl(mib, 2, NULL, &size, NULL, 0) == -1) {
		perror("sysctl(kern.boottime)");
		return -1;
	}
	if (size < sizeof(boottime)) {
		perror("sizeof(kern.boottime) < sizeof(timeval)");
		return -1;
	}
	// Allocate space for the result
	value = malloc(size);
	if (value == NULL) {
		perror("malloc");
		return -1;
	}
	// Now finally do the lookup
	if (sysctl(mib, 2, value, &size, NULL, 0) == -1) {
		perror("sysctl(kern.boottime)");
		return -1;
	}
	memcpy(&boottime, value, sizeof(boottime));
	free(value);
	printf("system boot time (seconds since epoch): %d\n",
		boottime.tv_sec);
	return boottime.tv_sec;
}

It's a lot more steps, but as I said sometimes it's the only way.



Atomic Bird, LLC