Hello, it has often been mentioned that the mdoc(7) .Lb macro - which was not part of the original language as designed by Cynthia Livingston for USENIX and the Computer Systems Research Group (CSRG) at the University of California in Berkeley, but introduced about a decade later - was significantly misdesigned in two respects.
1. The inventors of .Lb introduxed an additional section, LIBRARY, to be placed between NAME and SYNOPSIS. That is bad design for two reasons: first, documentation ought to get to the meat of the matter as directly as possible, but this additional section adds three lines of output before getting to the crucial SYNOPSIS section. On top of being wordy, the information is not even specific to the manual page itself but usually shared with several other pages, which is particularly bad in such a prominent place. Finally, having a dedicated section that essentially only contains a single word of information is overblown. 2. Arguably, the fact that the macro expects to have access to an exhaustive list of libraries, associating library names with library descriptions, is even worse misdesign. These lists are almost always out of date, inconsistent between formatters and operating systems, sometimes even conflicting. These lists have proven unmaintainable in practice. A need for a formal one-line library description simply doesn't exist. Compiling and linking against a library requires a library file name, but not a description. *If* a high-level overwiew page of a library is desired (which some libraries don't even need), a dedicated page like https://man.openbsd.org/crypto.3 can be provided, but there is no need to bloat every page by copying a library description line all over the place. The reason why .Lb got traction in FreeBSD and NetBSD in spite of these gross design issues is that it addresses a genuine concern: for functions that are not part of the C library, programmers need to know which library to link to, and the .Lb macro serves that purpose, however poorly. Since OpenBSD deliberately minimizes the number of system libraries, whereas FreeBSD has added large numbers of system libraries over the years, the need for such information has been much less pronounced in OpenBSD than in FreeBSD, resulting in the fact that FreeBSD (and NetBSD) use the .Lb macro but OpenBSD does not. In May 2025, Ted Unangst <tedu@> proposed a way to 1. solve the actual task, saying which library to link 2. display it to the user in a minimal way 3. cause minimal effort for authors 4. be reasonably backward-compatible with .Lb 5. avoid all the defects of .Lb Normal usage will go like this: .Sh SYNOPSIS .Lb libutil .In util.h .Ft int .Fn scan_scaled "char *number_w_scale" "long long *result" displaying as: SYNOPSIS /* -lutil */ #include <util.h> int scan_scaled(char *number_w_scale, long long *result); The meaning of "-l" is obvious to any C programmer, so additional words (like /* link with -lutil */) would be pointless verbosity, and using C comment syntax blends in with the overall syntax of the SYNOPSIS section. The input syntax ".Lb libutil" was chosen for three reasons: 1. That's the name of the library in the file system, and also what the library is often called in conversation. It's common to hear programmers talk about "libm" or "libssl". 2. Consequently, it's also what people are likely to search for with commands like "man -k Lb=libutil". 3. It is compatible with current syntax established in FreeBSD, NetBSD, and illumos. In complicated cases, where linking one library usually requires linking one or more additional libraries, too, it will be the author's choice to either only list the library containing the functions in question (just as above), or to list all libraries that should typically be linked, for example .Sh SYNOPSIS .Lb libtls libssl libcrypto .In tls.h .Ft int .Fn tls_init void displaying as: SYNOPSIS /* -ltls -lssl -lcrypto */ #include <tls.h> int tls_init(void); Allowing multiple .Lb arguments extends the currently valid syntax, but the result with an old formatter is merely slightly ugly and not unintelligble, so backward compatibility is not too bad: SYNOPSIS library "libtls" libssl libcrypto #include <tls.h> int tls_init(void); Since formatting will *not* change for .Lb macros outside the SYNOPSIS section, compatibility in the other direction - old manual pages with a new formatter - is excellent. I checked that neither FreeBSD nor NetBSD contain any .Lb macros in the SYNOPSIS section, so there is no problem whatsoever for them. In illumos, there is a very small number of manual pages that already have an .Lb macro in the SYNOPSIS section. Presumably they had a similar idea as tedu@ some years ago, so it seems likely they will profit rather than suffer from the change. Either way, it will be trivial for them to adapt. There is consensus in OpenBSD that we want to try this direction. Before applying it to our about 950 library manual pages, i wanted to ask for additional feedback while there is still time for tweaks. If this turns out to work well in OpenBSD, FreeBSD and NetBSD will have the *option* to follow, but there will be no pressure for them to follow because their current scheme will just continue to work. I'm appending the mandoc(1) diff mainly to show how easy this is to implement. The implementation in groff_mdoc(7) may turn out to be minimally more tricky, but i don't expect needing rocket science there, either. Yours, Ingo Index: mdoc_validate.c =================================================================== RCS file: /cvs/src/usr.bin/mandoc/mdoc_validate.c,v diff -u -p -r1.307 mdoc_validate.c --- mdoc_validate.c 20 Sep 2024 02:00:46 -0000 1.307 +++ mdoc_validate.c 28 May 2025 13:09:57 -0000 @@ -989,13 +989,35 @@ post_ex(POST_ARGS) static void post_lb(POST_ARGS) { - struct roff_node *n; + struct roff_node *n, *nch; + char *cp; post_delim_nb(mdoc); n = mdoc->last; - assert(n->child->type == ROFFT_TEXT); + nch = n->child; + assert(nch->type == ROFFT_TEXT); mdoc->next = ROFF_NEXT_CHILD; + + if (n->sec == SEC_SYNOPSIS) { + roff_word_alloc(mdoc, n->line, n->pos, "/*"); + mdoc->last->flags = NODE_NOSRC; + while (nch != NULL) { + roff_word_alloc(mdoc, n->line, n->pos, "-l"); + mdoc->last->flags = NODE_DELIMO | NODE_NOSRC; + mdoc->last = nch; + assert(nch->type == ROFFT_TEXT); + cp = nch->string; + if (strncmp(cp, "lib", 3) == 0) + memmove(cp, cp + 3, strlen(cp) - 3 + 1); + nch = nch->next; + } + roff_word_alloc(mdoc, n->line, n->pos, "*/"); + mdoc->last->flags = NODE_NOSRC; + mdoc->last = n; + return; + } + roff_word_alloc(mdoc, n->line, n->pos, "library"); mdoc->last->flags = NODE_NOSRC; roff_word_alloc(mdoc, n->line, n->pos, "\\(lq"); Index: mandocdb.c =================================================================== RCS file: /cvs/src/usr.bin/mandoc/mandocdb.c,v diff -u -p -r1.221 mandocdb.c --- mandocdb.c 14 May 2024 21:12:44 -0000 1.221 +++ mandocdb.c 28 May 2025 13:09:57 -0000 @@ -136,6 +136,8 @@ static int parse_mdoc_Fn(struct mpage * const struct roff_node *); static int parse_mdoc_Fo(struct mpage *, const struct roff_meta *, const struct roff_node *); +static int parse_mdoc_Lb(struct mpage *, const struct roff_meta *, + const struct roff_node *); static int parse_mdoc_Nd(struct mpage *, const struct roff_meta *, const struct roff_node *); static int parse_mdoc_Nm(struct mpage *, const struct roff_meta *, @@ -281,7 +283,7 @@ static const struct mdoc_handler mdoc_ha { NULL, 0, 0 }, /* Hf */ { NULL, 0, 0 }, /* Fr */ { NULL, 0, 0 }, /* Ud */ - { NULL, TYPE_Lb, NODE_NOSRC }, /* Lb */ + { parse_mdoc_Lb, 0, 0 }, /* Lb */ { NULL, 0, 0 }, /* Lp */ { NULL, TYPE_Lk, 0 }, /* Lk */ { NULL, TYPE_Mt, NODE_NOSRC }, /* Mt */ @@ -1706,6 +1708,25 @@ parse_mdoc_Fo(struct mpage *mpage, const if (n->child != NULL) parse_mdoc_fname(mpage, n->child); + return 0; +} + +static int +parse_mdoc_Lb(struct mpage *mpage, const struct roff_meta *meta, + const struct roff_node *n) +{ + char *cp; + + for (n = n->child; n != NULL; n = n->next) { + if (n->flags & NODE_NOSRC) + continue; + cp = n->string; + if (n->sec == SEC_SYNOPSIS) + mandoc_asprintf(&cp, "lib%s", cp); + putkey(mpage, cp, TYPE_Lb); + if (n->sec == SEC_SYNOPSIS) + free(cp); + } return 0; }