How to Ship man pages with your Node Programs

09 July 2021

The Javascript ecosystem uses the npm package registry to distribute libraries - and also binaries. If you've ever done npm install -g yarn, you've used this feature. The yarn package exposes a binary (a node js script) that executes on your system as a program, often invoked in the command line. Your npm installation then installs that package in a central global directory, and links each binary exposed by the package to a central directory that's in your shell. Then you can invoke the binary on your command line.

hugh@hugh-XPS-13-9343 ~> yarn --version
1.22.10

Pretty neat right?

In unix systems, the man utility is a common way to look up how to use a given command.

hugh@hugh-XPS-13-9343 ~> man yarn
No manual entry for yarn

But not all packages provide man pages. This isn't terrible - in the case of yarn, there is a whole 'help' subcommand to look up information about how to use yarn's cli - plus a documentation website. But, if like me, you think the developer experience is improved by meeting your developer where they expect you to be, you might like to distribute at least a nearly empty man page pointing devs in the right direction. If there's some chance of some portion of your users reaching for man {your binary}, I would suggest that it's worth weighing the effort of providing at least a man page with the benefit that those users would get from having docs (or a pointer to docs) where they expect.

Isn't that effort huge? Aren't man pages in a weird format for fancy native developers? How would I even install them from an npm package? In this post I'll attempt to convince you that making a basic man page is not a huge lift, and might be worth the your work for the developer experience benefit.

Let's start off with a creating a basic empty package for demonstration purposes, and installing some dependencies that will help us create our man pages.

I've created an example repo for you to refer to if you like.

$ mkdir my-package && cd my-package
$ npm init --yes
$ npm install --save marked<1 marked-man

Now we find ourselves with an empty package with two dependencies - marked, and marked-man. marked is a peer dependency of marked-man, the package that will take our markdown document and convert it to roff, the format used by man-pages. If you'd like to cut down dependencies and write in roff directly, go ahead! But I figure most javascript developers will be more familiar with Markdown.

Right now, there's a bug in marked-man causing it not to support versions of it's peerDependency marked greater than 1.0.0. At the moment, I suggest installing a version of marked below 1, and keeping an eye on the bug.

Next, let's write a simple document. Store the following in README.md.

# my-package(1) -- demo package

## Synopsis

my-package is a demonstration package that does nothing

## See also

example.com

There's a git more going on here than simple markdown syntax. Let's walk through it. On the first line, we have a heading containing our package name, followed immediately by a number in brackets. This number refers to the man 'section number' for your page. In our case, we're using section number 1 to show that our documentation concerns "Executable programs or shell commands" - but you can specify library calls, special files, games, etc. Check out the table in the man man document. The package name(section number) is followed by a -- spacer and a short description of your command.

Later on in the document, we can see sections headed by h2s. These are man "section names". You can have custom sections, but conventional section names include NAME (which is automatically generated for you), SYNOPSIS, CONFIGURATION, DESCRIPTION, FILES, NOTES, BUGS, AUTHORS, SEE ALSO, and more that are shown in the man man document.

Time to use marked-man to generate the man file. Man files are stored in the roff format, and we can use marked-man as follows to generate our roff file from our README.md.

$ ./node_modules/.bin/marked-man README.md

You'll see the following output:

.TH "MY\-PACKAGE" "1" "June 2021" "" ""
.SH "NAME"
\fBmy-package\fR \- demo package
.SH Synopsis
.P
my\-package is a demonstration package that does nothing
.SH See Also
.P
example\.com

I'm certainly glad I don't have to manually write in that format! Let's store this in a directory, and add an npm script so that we don't have to type out the full command every time. Add the following entry to your 'scripts' object in your package.json.

    "generate-man-page": "mkdir -p man && marked-man README.md > ./man/my-package.1"

Note that the roff output is stored in the man directory in a file called my-package.1. For your man file, you should follow the same naming convention: {packageName}.{section number}.

How do we make the man utility aware of the document when we install the package? We add an entry pointing at the file in our package.json as follows:

  "man": ["./man/my-package.1"]

We can test this out by running "npm install --global .", which will install the package in the current directory onto the system globally. Then, run man my-package to see the man page in action.

MY-PACKAGE(1)                                                                        MY-PACKAGE(1)

NAME
       my-package - demo package

Synopsis
       my-package is a demonstration package that does nothing

See Also
       example.com

                                          June 2021                                  MY-PACKAGE(1)

There we have it! Man pages for your node packages. If you'd like to have multiple man pages for your package (say your command is configurable by a dotfile that you'd like to document, for example), you can write multiple markdown documents, modify your npm script to generate them all, and add them to the list of exports on your package.json man object.

You may notice in the npm documentation that there's a directories.man configuration in the package.json spec, which is documented as exporting all the man pages to the system. I wasn't able to get that to work. If you are, please let me know!

Thanks for reading. I hope that I've covered the procedure for generating man pages from markdown documents such that it's clear to you - and I hope you agree that it's a relatively low amount of effort to add a touch of delight to your package's developer experience. If you have any feedback, please get in touch on twitter or on mastodon. I'd love to hear from you!