Welcome to little lamb

Code » limb » commit 241529d

Revamp obuffer, remove obuffers.h

author Olivier Brunel
2023-04-23 20:34:52 UTC
committer Olivier Brunel
2023-05-20 18:06:38 UTC
parent bc15470060f46c7a31d6db3089ad62d62d8d77de

Revamp obuffer, remove obuffers.h

It is now possible to attach an output buffer to another, creating a
so-called chain. Then, anything send to any obuffer of the chain will be
sent to the entire chain.

Remove obuffers.h & update the output.h macro, to now simply call
obuffer_putmsg() to either obuffer_1 or obuffer_2.
Then, to e.g. duplicate stdout to a log, simply create an obuffer for
said log and attach it to obuffer_1.

src/doc/autoopt.h/autoopt_parse_buffer_dest.3.md +3 -0
src/doc/autoopt.h/autoopt_quiet.3.md +17 -13
src/doc/obuffer.h.0.md +40 -9
src/doc/obuffer.h/obuffer_attach.3.md +75 -0
src/doc/obuffer.h/obuffer_level.3.md +66 -0
src/doc/obuffer.h/obuffer_put.3.md +8 -17
src/doc/obuffer.h/obuffer_putmsg.3.md +8 -14
src/doc/obuffers.h.0.md +0 -87
src/doc/obuffers.h/obuffers_addextra.3.md +0 -52
src/doc/obuffers.h/obuffers_addlog_full.3.md +0 -86
src/doc/obuffers.h/out_putmsg.3.md +0 -54
src/include/autoopt.h +0 -4
src/liblimb/autoopt.h/autoopt_debug.c +26 -4
src/liblimb/autoopt.h/autoopt_log.c +23 -2
src/liblimb/autoopt.h/autoopt_quiet.c +1 -1
src/liblimb/autoopt.h/autoopt_verbose.c +1 -1
src/liblimb/autoopt.h/logdbg.c +0 -20
src/liblimb/command.h/getcommandordie.c +1 -1
src/liblimb/include/limb/obuffer.h +17 -3
src/liblimb/include/limb/obuffers.h +0 -38
src/liblimb/include/limb/output.h +12 -9
src/liblimb/loadopt.h/loadopt_auto_noconfig.c +1 -1
src/liblimb/{obuffers.h/obuffers_getdbg.c => obuffer.h/obuffer_1_2.c} +3 -8
src/liblimb/{obuffers.h/obuffers_getlog.c => obuffer.h/obuffer_attach.c} +10 -6
src/liblimb/obuffer.h/obuffer_detach.c +19 -0
src/liblimb/obuffer.h/obuffer_put.c +7 -3
src/liblimb/obuffer.h/obuffer_putmsg.c +6 -2
src/liblimb/obuffer.h/obuffers_putmsg.c +0 -13
src/liblimb/obuffers.h/dbg_putmsg.c +0 -15
src/liblimb/obuffers.h/err_putmsg.c +0 -13
src/liblimb/obuffers.h/extras_putmsg.c +0 -13
src/liblimb/obuffers.h/obuffers_adddbg_full.c +0 -21
src/liblimb/obuffers.h/obuffers_addextra.c +0 -18
src/liblimb/obuffers.h/obuffers_addlog_full.c +0 -21
src/liblimb/obuffers.h/obuffers_getextra.c +0 -14
src/liblimb/obuffers.h/obuffers_remdbg.c +0 -16
src/liblimb/obuffers.h/obuffers_remextra.c +0 -17
src/liblimb/obuffers.h/obuffers_remlog.c +0 -16
src/liblimb/obuffers.h/out_putmsg.c +0 -13
src/liblimb/obuffers.h/out_putmsgdie.c +0 -12
src/liblimb/output.h/list_matches.c +2 -2
src/liblimb/output.h/list_matches_full.c +4 -4
src/liblimb/{obuffers.h/err_putmsgdie.c => output.h/obuffer_putmsgdie.c} +3 -3
src/liblimb/parseopt.h/parseopt_warn.c +1 -1
src/liblimb/term.h/ask_password.c +4 -4
src/mkrabintables/err_putmsg.o +0 -1
src/mkrabintables/err_putmsgdie.o +0 -1
src/mkrabintables/extras_putmsg.o +0 -1
src/mkrabintables/obuffer_1_2.o +1 -0
src/mkrabintables/obuffer_putmsgdie.o +1 -0
src/mkrabintables/obuffers_putmsg.o +0 -1

diff --git a/src/doc/autoopt.h/autoopt_parse_buffer_dest.3.md b/src/doc/autoopt.h/autoopt_parse_buffer_dest.3.md
index 10ee8e5..7718cc2 100644
--- a/src/doc/autoopt.h/autoopt_parse_buffer_dest.3.md
+++ b/src/doc/autoopt.h/autoopt_parse_buffer_dest.3.md
@@ -44,6 +44,9 @@ success. It can either be the specified file descriptor on `arg`, or - in case
 of a file name - the file descriptor of the file opened in append mode.
 Otherwise, it returns -1 and sets `errno` to indicate the error.
 
+A warning it also emitted (through the [warn](3) family of functions) describing
+the error.
+
 # ERRORS
 
 The function may fail if :
diff --git a/src/doc/autoopt.h/autoopt_quiet.3.md b/src/doc/autoopt.h/autoopt_quiet.3.md
index 5ed02ee..c038102 100644
--- a/src/doc/autoopt.h/autoopt_quiet.3.md
+++ b/src/doc/autoopt.h/autoopt_quiet.3.md
@@ -34,7 +34,7 @@ order to mention the option's name in the emitted warning.
 
 All the functions also expected to have been defined through their corresponding
 macros, notably if they take a required argument, their argument `arg` is
-expected to point to a NULL-terminated string without checking (unlike with an
+expected to point to a NUL-terminated string without checking (unlike with an
 optional argument).
 
 The *AUTOOPT_QUIET*(), *AUTOOPT_VERBOSE*(), *AUTOOPT_LOG*() and
@@ -49,33 +49,37 @@ for *stdout*) up by one level. If already at *OLVL_MAXIMUM* then nothing is
 done.
 
 ! INFO:
-! The `obuffer_1` output buffer is used by [out_putmsg](3) and all the macros
-! based on it, as can be found in [output.h](0).
+! The `obuffer_1` output buffer is used by many of the [output.h](0) macros.
 
-The `autoopt_log`() function parses `arg` as the destination to set up the log
-output buffer via [obuffers_addlog_full](3). Said parsing is done through
-[autoopt_parse_buffer_dest](3).
+The `autoopt_log`() function parses `arg` as the destination to set up a log
+buffer, which will get all messages sent to either `obuffer_1` or `obuffer_2`.
+Said parsing is done through [autoopt_parse_buffer_dest](3).
 
 The `autoopt_debug`() function raises the output level (of `obuffer_1`) to
 *OLVL_DEBUG* if no argument was specified. Else, it parses `arg` as the
-destination to set up the debug output buffer via [obuffers_adddbg_full](3).
+destination to set up a debug buffer, which will get all messages sent to either
+`obuffer_1`or `obuffer_2`.
 Said parsing is done through [autoopt_parse_buffer_dest](3).
 
+! INFO:
+! Duplicating the output is done by creating 2 output buffers backed by the same
+! buffer/file descriptor. One is attached to `obuffer_1` whilst the other is
+! attached to `obuffer_2`.
+
 # RETURN VALUE
 
 The `autoopt_quiet`() and `autoopt_verbose`() functions always returns 1.
 
 The `autoopt_log`() and `autoopt_debug`() functions return 1 on success.
-Otherwise they return 0 and set `errno` to indicate the error. A warning is
-also emitted (through the [warn](3) family of functions) describing the error.
+Otherwise they return 0 and set `errno` to indicate the error. A warning may be
+emitted in case of parsing error, see [autoopt_parse_buffer_dest](3) for more.
 
 # ERRORS
 
-The `autoopt_log`() function may fail for the errors described in
-[obuffers_addlog_full](3).
+The `autoopt_log`() and `autoopt_debug`() functions may fail if :
 
-The `autoopt_dbg`() function may fail for the errors described in
-[obuffers_adddbg_full](3).
+: *EADDRINUSE*
+:: A buffer was already set up using the same function.
 
 The `autoopt_log`() and `autoopt_debug`() functions may also fail for the errors
 described for [autoopt_parse_buffer_dest](3).
diff --git a/src/doc/obuffer.h.0.md b/src/doc/obuffer.h.0.md
index 4323dba..ec167b3 100644
--- a/src/doc/obuffer.h.0.md
+++ b/src/doc/obuffer.h.0.md
@@ -13,10 +13,13 @@ obuffer.h - output buffer interface
 
 This header defines required functions to write on output buffers.
 
-An output buffer is simply a buffer writing on a file descriptor, with an
-assigned output level. Whenever a message is to be sent, is also comes with its
-own level. Only messages with a level matching the buffer's level or lower will
-be writing to the buffer.
+An output buffer is simply a wrapper around a buffer (from [buffer.h](0)), with
+a level to only receive data from the same level or lower. Refer to
+[obuffer_level](3) for more.
+
+Additionally it is possible to attach one output buffer to another, thusly
+forming a /chain/. Whenever sending data to any buffer in the chain, it will be
+sent to all output buffers in the chain. Refer to [obuffer_attach](3) for more.
 
 ## Constants
 
@@ -38,16 +41,38 @@ The following types are defined :
 The following structures are defined :
 
 : *struct obuffer*
-:: Content of an output buffer. The level for the buffer can be accessed via the
-`lvl` member, of type *u8*.
+:: An opaque structure representing an output buffer.
+
+## Pointers
+
+The following pointers are defined :
+
+: `obuffer_1`
+:: An output buffer based on `buffer_1`, thusly representing *stdout*.
+
+: `obuffer_2`
+:: An output buffer based on `buffer_2`, thusly representing *stderr*.
+
+## Macros
+
+The following macros are defined :
+
+: *OBUFFER_INIT(`buffer`, `level`)*
+:: Value to initialize an obuffer.
 
 ## Functions
 
-The following functions are defined :
+The following functions/macros are defined :
 
 : [obuffer_parse_level](3)
 :: To parse a string representing an output level.
 
+: [obuffer_level](3)
+:: Return the level of an output buffer.
+
+: [obuffer_setlevel](3)
+:: To set the level of an output buffer.
+
 : [obuffer_put](3)
 :: To write given data to an output buffer (if levels match)
 
@@ -55,5 +80,11 @@ The following functions are defined :
 :: To write a message, given as an array of strings, to an output buffer (if
 :: levels match)
 
-: [obuffers_putmsg](3)
-:: Same as [obuffer_putmsg](3) only to an array of output buffers.
+: [obuffer_attach](3)
+:: To attach an output buffer to another, entering its chain.
+
+: [obuffer_detach](3)
+:: To remove an output buffer from its chain.
+
+: [obuffer_is_attached](3)
+:: Return whether or not an output buffer is part of a chain.
diff --git a/src/doc/obuffer.h/obuffer_attach.3.md b/src/doc/obuffer.h/obuffer_attach.3.md
new file mode 100644
index 0000000..59a79cb
--- /dev/null
+++ b/src/doc/obuffer.h/obuffer_attach.3.md
@@ -0,0 +1,75 @@
+% limb manual
+% obuffer_attach(3)
+
+# NAME
+
+obuffer\_attach, obuffer\_detach, obuffer\_is\_attached - manage chains of
+output buffers
+
+# SYNOPSIS
+
+    #include <limb/obuffer.h>
+
+```pre hl
+int obuffer_attach(obuffer *<em>obuf</em>, obuffer *<em>to</em>)
+int obuffer_detach(obuffer *<em>obuf</em>)
+int obuffer_is_attached(obuffer *<em>obuf</em>)
+```
+
+# DESCRIPTION
+
+The `obuffer_attach`() function will attach the output buffer pointed by `obuf`
+to the one pointed by `to`, effectively joining its chain (or creating a new one
+with both output buffers if the output buffer `to` wasn't in a chain yet).
+
+The `obuffer_detach`() function will detach the output buffer pointed by `obuf`
+from its current chain. It is then possible to attach it to a new chain if
+needed.
+
+The `obuffer_is_attached`() macro returns 1 if the output buffer pointed by
+`obuf` is attached to another one, and therefore part of a chain. Otherwise it
+returns zero.
+
+## Chain of output buffers
+
+In addition to having data only written to their underlying buffer /if/ their
+level matches that associated with the data (see [obuffer_level](3) for more),
+the other idea behind output buffers is the ability to "link" them together, or
+/attach/ one output buffer to another.
+
+Upon creation, an output buffer is "independent", i.e. not attached to any
+other, not part of any chain. In such a state, any data sent to it will simply
+to written to it (assuming levels match, as hinted above).
+
+However, once attached to another, they form a so-called /chain/. From this
+point on, any data sent to either one will also be sent to the other. And
+because you can have more than 2 output buffers in a chain, any data sent to
+any of the output buffers in the chain will result if all of them getting the
+data (again, assuming their own levels allow it).
+
+One output buffer cannot be attached to more than one output buffer, more
+precisely only an independent output buffer can be attached to another. After
+that, others (independent) can be attached /to/ it, but /it/ cannot be attached
+anymore.
+
+In other words, it is not possible to join/attach two chains together.
+
+# RETURN VALUE
+
+The `obuffer_attach`() and `obuffer_detach`() functions return 1 on success.
+Otherwise they return 0 and set `errno` to indicate the error.
+
+The `obuffer_is_attached`() macro returns 1 if the output buffer pointed by
+`obuf` is attached/part of a chain. Otherwise it returns 0.
+
+# ERRORS
+
+The `obuffer_attach`() may fail if :
+
+: *EINVAL*
+:: The output buffer pointed by `obuf` is already attached/part of a chain.
+
+The `obuffer_detach`() function may fail if :
+
+: *EINVAL*
+:: The output buffer pointed by `obuf` is not attached/part of a chain.
diff --git a/src/doc/obuffer.h/obuffer_level.3.md b/src/doc/obuffer.h/obuffer_level.3.md
new file mode 100644
index 0000000..138f14c
--- /dev/null
+++ b/src/doc/obuffer.h/obuffer_level.3.md
@@ -0,0 +1,66 @@
+% limb manual
+% obuffer_level(3)
+
+# NAME
+
+obuffer\_level, obuffer\_setlevel - get/set the level of an output buffer
+
+# SYNOPSIS
+
+    #include <limb/obuffer.h>
+
+```pre hl
+u8 obuffer_level(obuffer *<em>obuf</em>)
+void obuffer_setlevel(obuffer *<em>obuf</em>, u8 <em>level</em>)
+```
+
+# DESCRIPTION
+
+The `obuffer_level`() macro returns the current level of the output buffer
+pointed by `obuf`.
+
+The `obuffer_setlevel`() macro will set the level of the output buffer pointed
+by `obuf` to `level`.
+
+The following constants are defined as common levels, ordered from lower to
+higher :
+
+- *OLVL_SILENT*
+: Meant to be silent, i.e. disabled. (There should never be data sent with such
+: a level.)
+
+- *OLVL_QUIET*
+: For the most minimum/required messages only.
+
+- *OLVL_NORMAL*
+: For regular output.
+
+- *OLVL_VERBOSE*
+: For additional/verbose output.
+
+- *OLVL_DEBUG*
+: Meant for debugging output only. See the [dbg](3) macro to automatically
+: prefix messages with file & function name as well as line number.
+
+- *OLVL_MAXIMUM*
+: If you need extra/verbose debugging messages.
+
+As you must have guessed, the idea is simply to assign a level to different
+output, which can then be filtered at runtime by changing the level of the
+output buffer.
+
+Thus, all functions that write data to an output buffer actually check first
+that the output buffer's level is at least that associated with the data. Only
+then will data be sent/written to the underlying buffer.
+
+If the buffer has a lower level, then nothing is done.
+
+Note that this check is actually performed for /every/ output buffer in the
+chain, each buffer having its own level to determine whether or not to get
+written to.
+
+For more about chains of output buffers, refer to [obuffer_attach](3).
+
+# SEE ALSO
+
+[output.h](0)
diff --git a/src/doc/obuffer.h/obuffer_put.3.md b/src/doc/obuffer.h/obuffer_put.3.md
index 164a6e1..974a8e4 100644
--- a/src/doc/obuffer.h/obuffer_put.3.md
+++ b/src/doc/obuffer.h/obuffer_put.3.md
@@ -10,27 +10,18 @@ obuffer\_put - write data to an output buffer
     #include <limb/obuffer.h>
 
 ```pre hl
-ssize_t obuffer_put(obuffer *<em>obuf</em>, u8 <em>level</em>, const char *<em>data</em>, size_t <em>dlen</em>)
+void obuffer_put(obuffer *<em>obuf</em>, u8 <em>level</em>, const char *<em>data</em>, size_t <em>dlen</em>)
 ```
 
 # DESCRIPTION
 
-The `obuffer_put`() function will write the given data pointed to by `data`, of
-length `dlen`, into the output buffer `obuf` if its level is at least `level`.
+The `obuffer_put`() function will write the data pointed to by `data`, of length
+`dlen`, into the output buffer `obuf` and all attached to it, if their level is
+at least `level`.
 
-If `obuf` has a level lower than `level` then nothing is done.
+If an output buffer has a level lower than `level` then nothing is written to
+it.
 
-# RETURN VALUE
+# SEE ALSO
 
-The `obuffer_put`() function returns the number of bytes written into `obuf`
-on success. Otherwise, it returns 0.
-
-! INFO: Error checking
-! It is possible to return 0 on success, if the output buffer has a level lower
-! than `level`. In order to check for error, you should set `errno` to 0 prior
-! to calling `obuffer_put`().
-
-# ERRORS
-
-The `obuffer_put`() function may fail and set `errno` for any of the errors
-specified for [buffer_put](3).
+[obuffer_attach](3), [obuffer_setlevel](3)
diff --git a/src/doc/obuffer.h/obuffer_putmsg.3.md b/src/doc/obuffer.h/obuffer_putmsg.3.md
index c35b2e1..ce18dc9 100644
--- a/src/doc/obuffer.h/obuffer_putmsg.3.md
+++ b/src/doc/obuffer.h/obuffer_putmsg.3.md
@@ -3,8 +3,8 @@
 
 # NAME
 
-obuffer\_putmsg, obuffers_putmsg - write message, given as an array of strings,
-to an output buffer
+obuffer\_putmsg - write message, given as an array of strings, to an output
+buffer
 
 # SYNOPSIS
 
@@ -12,23 +12,17 @@ to an output buffer
 
 ```pre hl
 void obuffer_putmsg(obuffer *<em>obuf</em>, u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
-void obuffers_putmsg(obuffer *<em>obufs</em>, unsigned int <em>nbufs</em>,
-                     u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
 ```
 
 # DESCRIPTION
 
 The `obuffer_putmsg`() function will write the message, given as array of
-strings `as` of `n` elements, into the output buffer `obuf` if its level is at
-least `level`.
+strings `as` of `n` elements, into the output buffer `obuf` and all attached to
+it, if their level is at least `level`.
 
-If `obuf` has a level lower than `level` then nothing is done.
+If an output buffer has a level lower than `level` then nothing is written to
+it.
 
-The `obuffers_putmsg`() is similar, but for the array of `nbufs` output buffers
-pointed to by `obufs`. Each of the output buffers will have its own level
-checked against `level`, and only for those matching will the message be written
-into.
+# SEE ALSO
 
-! NOTE:
-! Every output buffer in `obufs` is checked to be "active", that is to have a
-! buffer set. Otherwise it is skipped.
+[obuffer_attach](3), [obuffer_setlevel](3)
diff --git a/src/doc/obuffers.h.0.md b/src/doc/obuffers.h.0.md
deleted file mode 100644
index dd1d50d..0000000
--- a/src/doc/obuffers.h.0.md
+++ /dev/null
@@ -1,87 +0,0 @@
-% limb manual
-% obuffers.h(0)
-
-# NAME
-
-obuffers.h - output buffer interface for stdout/stderr
-
-# SYNOPSIS
-
-    #include <limb/obuffers.h>
-
-# DESCRIPTION
-
-This header defines required functions to write on output buffers for stdout
-and/or stderr.
-
-Two output buffers are set up, `obuffer_1` and `obuffer_2`, to interface
-respectively *stdout* (via `buffer_1`) and *stderr* (via `buffer_2`).
-
-Additionally up to 3 extra output buffers can be set up. Any messages sent to
-*stdout* via [out_putmsg](3), [out_putmsgdie](3) or [dbg_putmsg](3) will also
-be sent to these extra output buffers.
-
-Similarly, any messages sent to *stderr* via [err_putmsg](3) or
-[err_putmsgdie](3) will also be sent to these extra output buffers.
-
-## Functions
-
-The following functions are defined :
-
-: [obuffers_addextra](3)
-:: Set up a new extra output buffer.
-
-: [obuffers_getextra](3)
-:: Get a pointer to an extra output buffer.
-
-: [obuffers_remextra](3)
-:: Remove an extra output buffer.
-
-
-: [obuffers_addlog_full](3)
-:: Set up a new extra buffer from the given file descriptor (e.g. to have a log
-:: of all stdout/stderr messages) at the specified level.
-
-: [obuffers_addlog](3)
-:: Similar to [obuffers_addlog_full](3) with *OLVL_NORMAL*.
-
-: [obuffers_adddbg_full](3)
-:: Set up a new extra buffer from the given file descriptor (e.g. to have a
-:: debugging log of all stdout/stderr messages) at the specified level.
-
-: [obufers_adddbg](3)
-:: Similar to [obuffers_adddbg_full](3) with *OLVL_DEBUG*.
-
-: [obuffers_getlog](3)
-:: Get a pointer to the extra output buffer set up via [obuffers_addlog](3)
-:: or [obuffers_addlog_full](3).
-
-: [obuffers_getdbg](3)
-:: Get a pointer to the extra output buffer set up via [obuffers_adddbg](3)
-:: or [obuffers_adddbg_full](3).
-
-: [obuffers_remlog](3)
-:: Remove the log buffer from extra output buffers.
-
-: [obuffers_remdbg](3)
-:: Remove the debug buffer from extra output buffers.
-
-
-: [extras_putmsg](3)
-:: Send a message to all extra output buffers.
-
-: [out_putmsg](3)
-:: Send a message to `obuffer_1` and all extra output buffers.
-
-: [err_putmsg](3)
-:: Send a message to `obuffer_2` and all extra output buffers.
-
-: [dbg_putmsg](3)
-:: Same as [out_putmsg](3) but prefix the message with the given function name,
-:: file name and line number. Mostly used via [dbg](3).
-
-: [out_putmsgdie](3)
-:: Same as [out_putmsg](3) but terminate process execution afterwards.
-
-: [err_putmsgdie](3)
-:: Same as [err_putmsg](3) but terminate process execution afterwards.
diff --git a/src/doc/obuffers.h/obuffers_addextra.3.md b/src/doc/obuffers.h/obuffers_addextra.3.md
deleted file mode 100644
index b4bc8b7..0000000
--- a/src/doc/obuffers.h/obuffers_addextra.3.md
+++ /dev/null
@@ -1,52 +0,0 @@
-% limb manual
-% obuffers_addextra(3)
-
-# NAME
-
-obuffers\_addextra, obuffers\_getextra, obuffers\_remextra - add/remove an
-extra output buffer
-
-# SYNOPSIS
-
-    #include <limb/obuffers.h>
-
-```pre hl
-int obuffers_addextra(buffer *<em>buf</em>, u8 <em>level</em>)
-obuffer *obuffers_getextra(buffer *<em>buf</em>)
-int obuffers_remextra(buffer *<em>buf</em>)
-```
-
-# DESCRIPTION
-
-The `obuffers_addextra`() function will set up a new extra output buffer, using
-the given buffer `buf` (which must be set up for writing), using `level` as the
-buffer's level -- meaning only sent messages with the same level or lower will
-be written into `buf`.
-
-The `obuffers_getextra`() function returns a pointer to the extra output buffer
-set up using buffer `buf`. Note that this pointer is only valid if/until this
-extra output buffer is removed (e.g. via `obuffers_remextra`()); It is mostly
-useful to get/change the output buffer's level.
-
-The `obuffers_remextra`() function will remove the extra buffer set up using the
-buffer `buf`.
-
-# RETURN VALUE
-
-The `obuffers_addextra`() and `obuffers_remextra`() functions return 1 on
-success. Otherwise they returns 0 and sets `errno` to indicate the error.
-
-The `obuffers_getextra`() function returns a pointer to the output buffer using
-`buf`, or NULL if none was found.
-
-# ERRORS
-
-The `obuffers_addextra`() function may fail if :
-
-: *ENOBUFS*
-:: All extra buffers are already set up.
-
-The `obuffers_remextra`() function may fail if :
-
-: *ENOENT*
-:: No extra buffer found using the specified buffer
diff --git a/src/doc/obuffers.h/obuffers_addlog_full.3.md b/src/doc/obuffers.h/obuffers_addlog_full.3.md
deleted file mode 100644
index ab89e39..0000000
--- a/src/doc/obuffers.h/obuffers_addlog_full.3.md
+++ /dev/null
@@ -1,86 +0,0 @@
-% limb manual
-% obuffers_addlog_full(3)
-
-# NAME
-
-obuffers\_addlog\_full, obuffers\_addlog, obuffers\_adddbg\_full,
-obuffers\_adddbg, obuffers\_getlog, obuffers\_getdbg, obuffers\_remlog,
-obuffers\_remdbg - set up/remove an extra buffer from a file descriptor
-
-# SYNOPSIS
-
-    #include <limb/obuffers.h>
-
-```pre hl
-int obuffers_addlog_full(int <em>fd</em>, u8 <em>level</em>)
-int obuffers_addlog(int <em>fd</em>)
-int obuffers_adddbg_full(int <em>fd</em>, u8 <em>level</em>)
-int obuffers_adddbg(int <em>fd</em>)
-
-obuffer *obuffers_getlog(void)
-obuffer *obuffers_getdbg(void)
-
-int obuffers_remlog(void)
-int obuffers_remdbg(void)
-```
-
-# DESCRIPTION
-
-The `obuffers_addlog_full`() function will set up an extra buffer, using the
-given file descriptor `fd` which must be opened for writing, with level `level`
-(i.e. only messages to the same level or less will be written to it).
-
-The `obuffers_addlog`() function is similar but with *OLVL_NORMAL*.
-
-The `obuffers_adddbg_full`() function will set up an extra buffer, using the
-given file descriptor `fd` which must be opened for writing, with level `level`
-(i.e. only messages to the same level or less will be written to it).
-
-The `obuffers_adddbg`() function is similar but with *OLVL_DEBUG*.
-
-The `obuffers_getlog`() function will return a pointer to the extra buffer
-previously set up using `obuffers_addlog_full`() or `obuffers_addlog`().
-
-The `obuffers_getdbg`() function will return a pointer to the extra buffer
-previously set up using `obuffers_adddbg_full`() or `obuffers_adddbg`().
-
-! INFO:
-! Pointers to output buffers returned by `obuffers_getlog`() and
-! `obuffers_getdbg`() are only valid until said extra buffers are removed (using
-! functions described below), and mostly intended to get/change their levels.
-
-The `obuffers_remlog`() function will remove the extra buffer previously set up
-using `obuffers_addlog`().
-
-The `obuffers_remdbg`() function will remove the extra buffer previously set up
-using `obuffers_adddbg`().
-
-! NOTE:
-! `obuffers_addlog`() and `obuffers_adddbg`() are implemented as macros to
-! `obuffers_addlog_full`() and `obuffers_adddbg_full`() respectively.
-
-# RETURN VALUE
-
-The `obuffers_addlog_full`(), `obuffers_addlog`(), `obuffers_adddbg_full`() and
-`obuffers_adddbg`() functions return 1 on success. Otherwise they returns 0
-and sets `errno` to indicate the error.
-
-The `obuffers_getlog`() and `obuffers_getdbg`() functions returns a pointer to
-the extra buffer, or NULL is none was found.
-
-The `obuffers_remlog`() and `obuffers_remdbg`() functions return the file
-descriptor that was used in the buffer on success. Otherwise, they return -1 and
-set `errno` to indicate the error.
-
-# ERRORS
-
-The `obuffers_addlog_full`(), `obuffers_addlog`(), `obuffers_adddbg_full`() and
-`obuffers_adddbg`() functions may fail if :
-
-: *EADDRINUSE*
-:: A buffer was already set up using the same function
-
-They may also fail for any of the errors specified for [obuffers_addextra](3).
-
-The `obuffers_remlog`() and `obuffers_remdbg`() functions may fail for any of
-the errors specified for [obuffers_remextra](3).
diff --git a/src/doc/obuffers.h/out_putmsg.3.md b/src/doc/obuffers.h/out_putmsg.3.md
deleted file mode 100644
index 237318c..0000000
--- a/src/doc/obuffers.h/out_putmsg.3.md
+++ /dev/null
@@ -1,54 +0,0 @@
-% limb manual
-% out_putmsg(3)
-
-# NAME
-
-out\_putmsg, err\_putmsg, dbg\_putmsg, extras\_putmsg - write message, given as
-an array of strings, to output buffers for stdout and/or stderr
-
-# SYNOPSIS
-
-    #include <limb/obuffers.h>
-
-```pre hl
-void out_putmsg(u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
-void err_putmsg(u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
-
-void dbg_putmsg(u8 <em>level</em>, const char *<em>func</em>, const char *<em>file</em>, int <em>line</em>,
-                const char * const *<em>as</em>, unsigned int <em>n</em>)
-
-void extras_putmsg(u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
-
-void out_putmsgdie(int <em>exit</em>, u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
-void err_putmsgdie(int <em>exit</em>, u8 <em>level</em>, const char * const *<em>as</em>, unsigned int <em>n</em>)
-```
-
-# DESCRIPTION
-
-The `out_putmsg`() function will write the message, given as an array of
-strings `as` of `n` elements, into the output buffer for *stdout* (`obuffer_1`)
-as well as the extra output buffers.
-
-The `err_putmsg`() function is similar, only with the output buffer for *stderr*
-(`obuffer_2`) instead of *stdout* (`obuffer_1`).
-
-The `dbg_putmsg`() function is similar to `out_putmsg`(), but prefixes the
-message with the given function name, file name and line number, resulting in a
-message written in the form:
-
-    [func@file:line] message
-
-It is mostly used via [dbg](3)
-
-
-The `extras_putmsg`() function is similar to `out_putmsg`(), but only for the
-extra output buffers.
-
-
-The `out_putmsgdie`() function is similar to `out_putmsg`(), but it will
-terminate the process execution (via [\_exit](3)) using `exit` as exit status
-code.
-
-The `err_putmsgdie`() function is similar to `err_putmsg`(), but it will
-terminate the process execution (via [\_exit](3)) using `exit` as exit status
-code.
diff --git a/src/include/autoopt.h b/src/include/autoopt.h
index 02d1414..a6e7d47 100644
--- a/src/include/autoopt.h
+++ b/src/include/autoopt.h
@@ -8,10 +8,6 @@
 #include <limb/autoopt.h>
 #include <limb/gccattributes.h>
 
-typedef int (*obufadd) (int fd, u8 deflvl);
-
-int logdbg(const struct option *option, const char *arg, u8 deflvl, obufadd obufadd) gccattr_hidden;
-
 int parse_buffer_dest(u8 *lvl, const char ** const arg, size_t len) gccattr_hidden;
 
 #endif /* LIMB_LIMB_AUTOOPT_H */
diff --git a/src/liblimb/autoopt.h/autoopt_debug.c b/src/liblimb/autoopt.h/autoopt_debug.c
index 46085e9..fdabde9 100644
--- a/src/liblimb/autoopt.h/autoopt_debug.c
+++ b/src/liblimb/autoopt.h/autoopt_debug.c
@@ -1,15 +1,37 @@
 /* This file is part of limb                           https://lila.oss/limb
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
+#include <errno.h>
+#include <limb/buffer.h>
+#include <limb/obuffer.h>
 #include "autoopt.h"
 
+static char buf[BUFFER_OUTSIZE];
+buffer buffer_dbg_ = BUFFER_INIT(&fd_writev, -1, buf, sizeof(buf));
+obuffer obuffer_dbg1_ = OBUFFER_INIT(&buffer_dbg_, OLVL_NORMAL);
+obuffer obuffer_dbg2_ = OBUFFER_INIT(&buffer_dbg_, OLVL_NORMAL);
+
 int
 autoopt_debug(const struct option *option, const char *arg)
 {
-    if (arg)
-        return logdbg(option, arg, OLVL_DEBUG, obuffers_adddbg_full);
+    if (!arg) {
+        obuffer_setlevel(obuffer_1, OLVL_DEBUG);
+        return 1;
+    }
+
+    if (buffer_dbg_.fd >= 0)
+        return (errno = EADDRINUSE, 0);
+
+    u8 lvl = OLVL_DEBUG;
+    int fd = autoopt_parse_buffer_dest(option, arg, &lvl);
+    if (fd < 0) return 0;
+    buffer_dbg_.fd = fd;
+
+    obuffer_setlevel(&obuffer_dbg1_, lvl);
+    obuffer_attach(&obuffer_dbg1_, obuffer_1);
+
+    obuffer_setlevel(&obuffer_dbg2_, lvl);
+    obuffer_attach(&obuffer_dbg2_, obuffer_2);
 
-    obuffer_1->lvl = OLVL_DEBUG;
     return 1;
 }
diff --git a/src/liblimb/autoopt.h/autoopt_log.c b/src/liblimb/autoopt.h/autoopt_log.c
index e7b6f6e..3ce8ba1 100644
--- a/src/liblimb/autoopt.h/autoopt_log.c
+++ b/src/liblimb/autoopt.h/autoopt_log.c
@@ -1,11 +1,32 @@
 /* This file is part of limb                           https://lila.oss/limb
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
+#include <errno.h>
+#include <limb/buffer.h>
+#include <limb/obuffer.h>
 #include "autoopt.h"
 
+static char buf[BUFFER_OUTSIZE];
+buffer buffer_log_ = BUFFER_INIT(&fd_writev, -1, buf, sizeof(buf));
+obuffer obuffer_log1_ = OBUFFER_INIT(&buffer_log_, OLVL_NORMAL);
+obuffer obuffer_log2_ = OBUFFER_INIT(&buffer_log_, OLVL_NORMAL);
+
 int
 autoopt_log(const struct option *option, const char *arg)
 {
-    return logdbg(option, arg, OLVL_NORMAL, obuffers_addlog_full);
+    if (buffer_log_.fd >= 0)
+        return (errno = EADDRINUSE, 0);
+
+    u8 lvl = OLVL_NORMAL;
+    int fd = autoopt_parse_buffer_dest(option, arg, &lvl);
+    if (fd < 0) return 0;
+    buffer_log_.fd = fd;
+
+    obuffer_setlevel(&obuffer_log1_, lvl);
+    obuffer_attach(&obuffer_log1_, obuffer_1);
+
+    obuffer_setlevel(&obuffer_log2_, lvl);
+    obuffer_attach(&obuffer_log2_, obuffer_2);
+
+    return 1;
 }
diff --git a/src/liblimb/autoopt.h/autoopt_quiet.c b/src/liblimb/autoopt.h/autoopt_quiet.c
index da88726..1a3b469 100644
--- a/src/liblimb/autoopt.h/autoopt_quiet.c
+++ b/src/liblimb/autoopt.h/autoopt_quiet.c
@@ -3,7 +3,7 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 #include <skalibs/gccattributes.h>
 #include <limb/autoopt.h>
-#include <limb/obuffers.h>
+#include <limb/obuffer.h>
 
 int
 autoopt_quiet(const struct option *option gccattr_unused, const char *arg gccattr_unused)
diff --git a/src/liblimb/autoopt.h/autoopt_verbose.c b/src/liblimb/autoopt.h/autoopt_verbose.c
index 30566b1..6fceb8e 100644
--- a/src/liblimb/autoopt.h/autoopt_verbose.c
+++ b/src/liblimb/autoopt.h/autoopt_verbose.c
@@ -3,7 +3,7 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 #include <skalibs/gccattributes.h>
 #include <limb/autoopt.h>
-#include <limb/obuffers.h>
+#include <limb/obuffer.h>
 
 int
 autoopt_verbose(const struct option *option gccattr_unused, const char *arg gccattr_unused)
diff --git a/src/liblimb/autoopt.h/logdbg.c b/src/liblimb/autoopt.h/logdbg.c
deleted file mode 100644
index 10bd9dc..0000000
--- a/src/liblimb/autoopt.h/logdbg.c
+++ /dev/null
@@ -1,20 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/djbunix.h>
-#include "autoopt.h"
-
-int
-logdbg(const struct option *option, const char *arg, u8 deflvl, obufadd obufadd)
-{
-    u8 lvl = deflvl;
-    int fd = autoopt_parse_buffer_dest(option, arg, &lvl);
-    if (fd < 0) return 0;
-
-    if (!obufadd(fd, lvl)) {
-        fd_close(fd);
-        return 0;
-    }
-
-    return 1;
-}
diff --git a/src/liblimb/command.h/getcommandordie.c b/src/liblimb/command.h/getcommandordie.c
index ca13f96..9ef3a42 100644
--- a/src/liblimb/command.h/getcommandordie.c
+++ b/src/liblimb/command.h/getcommandordie.c
@@ -20,7 +20,7 @@ getcommandordie(int exit, const char *usage, const char *arg)
         warn("unknown command: ", arg);
         /* more than 1 partial match? */
         if (first >= 0)
-            list_matches(err_putmsg, OLVL_NORMAL, "did you mean ", NULL,
+            list_matches(obuffer_2, OLVL_NORMAL, "did you mean ", NULL,
                          " or ", " ?", arg, alen, first, names);
         dieusage(exit, usage);
     }
diff --git a/src/liblimb/include/limb/obuffer.h b/src/liblimb/include/limb/obuffer.h
index dd998f7..047f28a 100644
--- a/src/liblimb/include/limb/obuffer.h
+++ b/src/liblimb/include/limb/obuffer.h
@@ -18,16 +18,30 @@ enum olevel {
 
 typedef struct obuffer obuffer;
 
+/* opaque structure */
 struct obuffer {
     buffer *b;
+    obuffer *n;
     u8 lvl;
 };
 
-extern ssize_t obuffer_put(obuffer *obuf, u8 level, const char *s, size_t len);
+#define OBUFFER_INIT(buf,lvl)           { buf, NULL, lvl }
+
+extern void obuffer_put(obuffer *obuf, u8 level, const char *s, size_t len);
 extern void obuffer_putmsg(obuffer *obuf, u8 level, const char * const *as, unsigned int n);
-extern void obuffers_putmsg(obuffer *obufs, unsigned int nbufs,
-                            u8 level, const char * const *as, unsigned int n);
+#define obuffer_level(obuf)             (obuf)->lvl
+#define obuffer_setlevel(obuf,level)    (obuf)->lvl = level
+
+extern int obuffer_attach(obuffer *obuf, obuffer *to);
+extern int obuffer_detach(obuffer *obuf);
+#define obuffer_is_attached(obuf)       ((int) !!(obuf)->n)
 
 extern u8 obuffer_parse_level(const char *str, size_t len);
 
+extern obuffer obuffer_1_;
+#define obuffer_1       (&obuffer_1_)
+
+extern obuffer obuffer_2_;
+#define obuffer_2       (&obuffer_2_)
+
 #endif /* LIMB_OBUFFER_H */
diff --git a/src/liblimb/include/limb/obuffers.h b/src/liblimb/include/limb/obuffers.h
deleted file mode 100644
index b8ca865..0000000
--- a/src/liblimb/include/limb/obuffers.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#ifndef LIMB_OBUFFERS_H
-#define LIMB_OBUFFERS_H
-
-#include <limb/obuffer.h>
-
-extern int obuffers_addextra(buffer *b, u8 level);
-extern obuffer *obuffers_getextra(buffer *b);
-extern int obuffers_remextra(buffer *b);
-
-extern int obuffers_addlog_full(int fd, u8 level);
-#define obuffers_addlog(fd) obuffers_addlog_full(fd, OLVL_NORMAL)
-extern int obuffers_adddbg_full(int fd, u8 level);
-#define obuffers_adddbg(fd) obuffers_adddbg_full(fd, OLVL_DEBUG)
-extern obuffer *obuffers_getlog(void);
-extern obuffer *obuffers_getdbg(void);
-extern int obuffers_remlog(void);
-extern int obuffers_remdbg(void);
-
-extern void extras_putmsg(u8 level, const char * const *as, unsigned int n);
-extern void out_putmsg(u8 level, const char * const *as, unsigned int n);
-extern void err_putmsg(u8 level, const char * const *as, unsigned int n);
-extern void dbg_putmsg(u8 level, const char *func, const char *file, int line,
-                       const char * const *as, unsigned int n);
-extern void out_putmsgdie(int exit, u8 level, const char * const *as, unsigned int n);
-extern void err_putmsgdie(int exit, u8 level, const char * const *as, unsigned int n);
-
-extern obuffer obuffer_1_;
-#define obuffer_1 (&obuffer_1_)
-
-extern obuffer obuffer_2_;
-#define obuffer_2 (&obuffer_2_)
-
-extern obuffer obuffer_extras[3];
-
-#endif /* LIMB_OBUFFERS_H */
diff --git a/src/liblimb/include/limb/output.h b/src/liblimb/include/limb/output.h
index 6339370..3154bff 100644
--- a/src/liblimb/include/limb/output.h
+++ b/src/liblimb/include/limb/output.h
@@ -5,14 +5,15 @@
 #define LIMB_OUTPUT_H
 
 #include <stddef.h> /* size_t */
-#include <limb/obuffers.h>
+#include <limb/obuffer.h>
 
-typedef void (*obuffer_putmsgfn) (u8 level, const char * const *as, unsigned int n);
-extern void list_matches_full(obuffer_putmsgfn putmsg, u8 level, const char *intro,
+extern void obuffer_putmsgdie(obuffer *obuf, u8 level, const char * const str[], unsigned n, int exit);
+
+extern void list_matches_full(obuffer *obuf, u8 level, const char *intro,
                               const char *prefix, const char *sep, const char *outro,
                               const char *str, size_t slen, int first,
                               const void *list_, size_t offset, size_t llen);
-extern void list_matches(obuffer_putmsgfn putmsg, u8 level, const char *intro,
+extern void list_matches(obuffer *obuf, u8 level, const char *intro,
                          const char *prefix, const char *sep, const char *outro,
                          const char *str, size_t slen, int first, const char **list);
 
@@ -21,12 +22,14 @@ extern const char *PROG;
 #define outarray(...)       ((const char * const []) {__VA_ARGS__})
 #define outalen(...)        (sizeof(outarray(__VA_ARGS__)) / sizeof(char *))
 
-#define outmsg(l,...)       out_putmsg(l, outarray(__VA_ARGS__), outalen(__VA_ARGS__))
-#define errmsg(l,...)       err_putmsg(l, outarray(__VA_ARGS__), outalen(__VA_ARGS__))
-#define dbgmsg(l,...)       dbg_putmsg(l, __func__, __FILE__, __LINE__, outarray(__VA_ARGS__), outalen(__VA_ARGS__))
+#define putmsg(b,l,...)     obuffer_putmsg(b, l, outarray(__VA_ARGS__), outalen(__VA_ARGS__))
+#define outmsg(l,...)       putmsg(obuffer_1, l, __VA_ARGS__)
+#define errmsg(l,...)       putmsg(obuffer_2, l, __VA_ARGS__)
+#define dbgmsg(l,...)       outmsg(l, "[", __func__, "@", __FILE__, ":", PUTMSG_INT(__LINE__), "] ", __VA_ARGS__)
 
-#define outmsgdie(e,l,...)  out_putmsgdie(e, l, outarray(__VA_ARGS__), outalen(__VA_ARGS__))
-#define errmsgdie(e,l,...)  err_putmsgdie(e, l, outarray(__VA_ARGS__), outalen(__VA_ARGS__))
+#define putmsgdie(e,b,l,...)obuffer_putmsgdie(b, l, outarray(__VA_ARGS__), outalen(__VA_ARGS__), e)
+#define outmsgdie(e,l,...)  putmsgdie(e, obuffer_1, l, __VA_ARGS__)
+#define errmsgdie(e,l,...)  putmsgdie(e, obuffer_2, l, __VA_ARGS__)
 
 #define quiet(...)          outmsg(OLVL_QUIET, __VA_ARGS__, "\n", PUTMSG_FLUSH)
 #define out(...)            outmsg(OLVL_NORMAL, __VA_ARGS__, "\n", PUTMSG_FLUSH)
diff --git a/src/liblimb/loadopt.h/loadopt_auto_noconfig.c b/src/liblimb/loadopt.h/loadopt_auto_noconfig.c
index ace8cee..98c81a9 100644
--- a/src/liblimb/loadopt.h/loadopt_auto_noconfig.c
+++ b/src/liblimb/loadopt.h/loadopt_auto_noconfig.c
@@ -25,7 +25,7 @@ loadopt_auto_noconfig(int idx, const struct option *options, struct loadopt *ctx
                 warn("invalid argument to option --", options[idx].longopt,
                      (*buf) ? "/-" : "", buf, ": ", ctx->po.arg);
                 if (first >= 0)
-                    list_matches_full(err_putmsg, OLVL_NORMAL, "did you mean ",
+                    list_matches_full(obuffer_2, OLVL_NORMAL, "did you mean ",
                                       "--", " or ", " ?", s, l, first, options,
                                       offsetof(struct option, longopt), sizeof(*options));
                 return (errno = EINVAL, 0);
diff --git a/src/liblimb/obuffers.h/obuffers_getdbg.c b/src/liblimb/obuffer.h/obuffer_1_2.c
similarity index 59%
rename from src/liblimb/obuffers.h/obuffers_getdbg.c
rename to src/liblimb/obuffer.h/obuffer_1_2.c
index 2c3ef26..10fdf56 100644
--- a/src/liblimb/obuffers.h/obuffers_getdbg.c
+++ b/src/liblimb/obuffer.h/obuffer_1_2.c
@@ -1,12 +1,7 @@
 /* This file is part of limb                           https://lila.oss/limb
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
+#include <limb/obuffer.h>
 
-extern buffer buffer_dbg_;
-
-obuffer *
-obuffers_getdbg(void)
-{
-    return obuffers_getextra(&buffer_dbg_);
-}
+obuffer obuffer_1_ = { buffer_1, NULL, OLVL_NORMAL };
+obuffer obuffer_2_ = { buffer_2, NULL, OLVL_NORMAL };
diff --git a/src/liblimb/obuffers.h/obuffers_getlog.c b/src/liblimb/obuffer.h/obuffer_attach.c
similarity index 54%
rename from src/liblimb/obuffers.h/obuffers_getlog.c
rename to src/liblimb/obuffer.h/obuffer_attach.c
index d0ca742..f9bca96 100644
--- a/src/liblimb/obuffers.h/obuffers_getlog.c
+++ b/src/liblimb/obuffer.h/obuffer_attach.c
@@ -1,12 +1,16 @@
 /* This file is part of limb                           https://lila.oss/limb
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
+#include <errno.h>
+#include <limb/obuffer.h>
 
-extern buffer buffer_log_;
-
-obuffer *
-obuffers_getlog(void)
+int
+obuffer_attach(obuffer *b, obuffer *to)
 {
-    return obuffers_getextra(&buffer_log_);
+    if (b->n) return (errno = EINVAL, 0);
+
+    b->n = to;
+    to->n = b;
+
+    return 1;
 }
diff --git a/src/liblimb/obuffer.h/obuffer_detach.c b/src/liblimb/obuffer.h/obuffer_detach.c
new file mode 100644
index 0000000..b2b5f46
--- /dev/null
+++ b/src/liblimb/obuffer.h/obuffer_detach.c
@@ -0,0 +1,19 @@
+/* This file is part of limb                           https://lila.oss/limb
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <errno.h>
+#include <limb/obuffer.h>
+
+int
+obuffer_detach(obuffer *b)
+{
+    if (!b->n) return (errno = EINVAL, 0);
+
+    obuffer *l;
+    for (l = b; l->n != b; l = l->n)
+        ;
+    l->n = b->n;
+    b->n = NULL;
+
+    return 1;
+}
diff --git a/src/liblimb/obuffer.h/obuffer_put.c b/src/liblimb/obuffer.h/obuffer_put.c
index 41e922d..89e66ac 100644
--- a/src/liblimb/obuffer.h/obuffer_put.c
+++ b/src/liblimb/obuffer.h/obuffer_put.c
@@ -3,9 +3,13 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 #include <limb/obuffer.h>
 
-ssize_t
+void
 obuffer_put(obuffer *obuf, u8 level, const char *s, size_t len)
 {
-    if (level > obuf->lvl) return 0;
-    return buffer_put(obuf->b, s, len);
+    obuffer *ref = obuf;
+    do {
+        if (level <= obuf->lvl)
+            buffer_put(obuf->b, s, len);
+        obuf = obuf->n;
+    } while (obuf && obuf != ref);
 }
diff --git a/src/liblimb/obuffer.h/obuffer_putmsg.c b/src/liblimb/obuffer.h/obuffer_putmsg.c
index 8c6351e..5d86c3c 100644
--- a/src/liblimb/obuffer.h/obuffer_putmsg.c
+++ b/src/liblimb/obuffer.h/obuffer_putmsg.c
@@ -6,6 +6,10 @@
 void
 obuffer_putmsg(obuffer *obuf, u8 level, const char * const *as, unsigned int n)
 {
-    if (level > obuf->lvl) return;
-    buffer_putmsg(obuf->b, as, n);
+    obuffer *ref = obuf;
+    do {
+        if (level <= obuf->lvl)
+            buffer_putmsg(obuf->b, as, n);
+        obuf = obuf->n;
+    } while (obuf && obuf != ref);
 }
diff --git a/src/liblimb/obuffer.h/obuffers_putmsg.c b/src/liblimb/obuffer.h/obuffers_putmsg.c
deleted file mode 100644
index eedb636..0000000
--- a/src/liblimb/obuffer.h/obuffers_putmsg.c
+++ /dev/null
@@ -1,13 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffer.h>
-
-void
-obuffers_putmsg(obuffer *obufs, unsigned int nbufs, u8 level,
-                const char * const *as, unsigned int n)
-{
-    for (unsigned int i = 0; i < nbufs; ++i)
-        if (obufs[i].b && level <= obufs[i].lvl)
-            buffer_putmsg(obufs[i].b, as, n);
-}
diff --git a/src/liblimb/obuffers.h/dbg_putmsg.c b/src/liblimb/obuffers.h/dbg_putmsg.c
deleted file mode 100644
index 75c27c6..0000000
--- a/src/liblimb/obuffers.h/dbg_putmsg.c
+++ /dev/null
@@ -1,15 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/output.h>
-
-void
-dbg_putmsg(u8 level, const char *func, const char *file, int line,
-           const char * const *as, unsigned int n)
-{
-    const char *strings[8 + n];
-    /* PUTMSG_INT() expands to 2 arguments, hence the 8 for seemingly 7 elements */
-    memcpy(strings, (const char * const [])  { "[", func, "@", file, ":", PUTMSG_INT(line), "] " }, 8 * sizeof(char *));
-    memcpy(strings + 8, as, n * sizeof(*as));
-    out_putmsg(level, strings, 8 + n);
-}
diff --git a/src/liblimb/obuffers.h/err_putmsg.c b/src/liblimb/obuffers.h/err_putmsg.c
deleted file mode 100644
index f0f091b..0000000
--- a/src/liblimb/obuffers.h/err_putmsg.c
+++ /dev/null
@@ -1,13 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
-
-obuffer obuffer_2_ = { buffer_2, OLVL_NORMAL };
-
-void
-err_putmsg(u8 level, const char * const *as, unsigned int n)
-{
-    obuffer_putmsg(obuffer_2, level, as, n);
-    extras_putmsg(level, as, n);
-}
diff --git a/src/liblimb/obuffers.h/extras_putmsg.c b/src/liblimb/obuffers.h/extras_putmsg.c
deleted file mode 100644
index c865c7c..0000000
--- a/src/liblimb/obuffers.h/extras_putmsg.c
+++ /dev/null
@@ -1,13 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
-
-obuffer obuffer_extras[3] = { 0 };
-
-void
-extras_putmsg(u8 level, const char * const *as, unsigned int n)
-{
-    obuffers_putmsg(obuffer_extras, sizeof(obuffer_extras) / sizeof(obuffer_extras[0]),
-                    level, as, n);
-}
diff --git a/src/liblimb/obuffers.h/obuffers_adddbg_full.c b/src/liblimb/obuffers.h/obuffers_adddbg_full.c
deleted file mode 100644
index f6d02e9..0000000
--- a/src/liblimb/obuffers.h/obuffers_adddbg_full.c
+++ /dev/null
@@ -1,21 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <errno.h>
-#include <limb/obuffers.h>
-
-static char buf[BUFFER_OUTSIZE];
-buffer buffer_dbg_ = BUFFER_INIT(&fd_writev, -1, buf, sizeof(buf));
-
-int
-obuffers_adddbg_full(int fd, u8 level)
-{
-    if (buffer_dbg_.fd >= 0)
-        return (errno = EADDRINUSE, 0);
-
-    if (!obuffers_addextra(&buffer_dbg_, level))
-        return 0;
-
-    buffer_dbg_.fd = fd;
-    return 1;
-}
diff --git a/src/liblimb/obuffers.h/obuffers_addextra.c b/src/liblimb/obuffers.h/obuffers_addextra.c
deleted file mode 100644
index 04972ba..0000000
--- a/src/liblimb/obuffers.h/obuffers_addextra.c
+++ /dev/null
@@ -1,18 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <errno.h>
-#include <limb/obuffers.h>
-
-int
-obuffers_addextra(buffer *b, u8 level)
-{
-    int n = sizeof(obuffer_extras) / sizeof(obuffer_extras[0]);
-    for (int i = 0; i < n; ++i)
-        if (!obuffer_extras[i].b) {
-            obuffer_extras[i].b = b;
-            obuffer_extras[i].lvl = level;
-            return 1;
-        }
-    return (errno = ENOBUFS, 0);
-}
diff --git a/src/liblimb/obuffers.h/obuffers_addlog_full.c b/src/liblimb/obuffers.h/obuffers_addlog_full.c
deleted file mode 100644
index 36e9de8..0000000
--- a/src/liblimb/obuffers.h/obuffers_addlog_full.c
+++ /dev/null
@@ -1,21 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <errno.h>
-#include <limb/obuffers.h>
-
-static char buf[BUFFER_OUTSIZE];
-buffer buffer_log_ = BUFFER_INIT(&fd_writev, -1, buf, sizeof(buf));
-
-int
-obuffers_addlog_full(int fd, u8 level)
-{
-    if (buffer_log_.fd >= 0)
-        return (errno = EADDRINUSE, 0);
-
-    if (!obuffers_addextra(&buffer_log_, level))
-        return 0;
-
-    buffer_log_.fd = fd;
-    return 1;
-}
diff --git a/src/liblimb/obuffers.h/obuffers_getextra.c b/src/liblimb/obuffers.h/obuffers_getextra.c
deleted file mode 100644
index 2c41f91..0000000
--- a/src/liblimb/obuffers.h/obuffers_getextra.c
+++ /dev/null
@@ -1,14 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
-
-obuffer *
-obuffers_getextra(buffer *b)
-{
-    int n = sizeof(obuffer_extras) / sizeof(obuffer_extras[0]);
-    for (int i = 0; i < n; ++i)
-        if (obuffer_extras[i].b == b)
-            return &obuffer_extras[i];
-    return NULL;
-}
diff --git a/src/liblimb/obuffers.h/obuffers_remdbg.c b/src/liblimb/obuffers.h/obuffers_remdbg.c
deleted file mode 100644
index 3e7cdb4..0000000
--- a/src/liblimb/obuffers.h/obuffers_remdbg.c
+++ /dev/null
@@ -1,16 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
-
-extern buffer buffer_dbg_;
-
-int
-obuffers_remdbg(void)
-{
-    int fd = buffer_dbg_.fd;
-    if (!obuffers_remextra(&buffer_dbg_))
-        return -1;
-    buffer_dbg_.fd = -1;
-    return fd;
-}
diff --git a/src/liblimb/obuffers.h/obuffers_remextra.c b/src/liblimb/obuffers.h/obuffers_remextra.c
deleted file mode 100644
index b7a31a7..0000000
--- a/src/liblimb/obuffers.h/obuffers_remextra.c
+++ /dev/null
@@ -1,17 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <errno.h>
-#include <limb/obuffers.h>
-
-int
-obuffers_remextra(buffer *b)
-{
-    int n = sizeof(obuffer_extras) / sizeof(obuffer_extras[0]);
-    for (int i = 0; i < n; ++i)
-        if (obuffer_extras[i].b == b) {
-            obuffer_extras[i].b = NULL;
-            return 1;
-        }
-    return (errno = ENOENT, 0);
-}
diff --git a/src/liblimb/obuffers.h/obuffers_remlog.c b/src/liblimb/obuffers.h/obuffers_remlog.c
deleted file mode 100644
index 32331d7..0000000
--- a/src/liblimb/obuffers.h/obuffers_remlog.c
+++ /dev/null
@@ -1,16 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
-
-extern buffer buffer_log_;
-
-int
-obuffers_remlog(void)
-{
-    int fd = buffer_log_.fd;
-    if (!obuffers_remextra(&buffer_log_))
-        return -1;
-    buffer_log_.fd = -1;
-    return fd;
-}
diff --git a/src/liblimb/obuffers.h/out_putmsg.c b/src/liblimb/obuffers.h/out_putmsg.c
deleted file mode 100644
index f75d4e0..0000000
--- a/src/liblimb/obuffers.h/out_putmsg.c
+++ /dev/null
@@ -1,13 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <limb/obuffers.h>
-
-obuffer obuffer_1_ = { buffer_1, OLVL_NORMAL };
-
-void
-out_putmsg(u8 level, const char * const *as, unsigned int n)
-{
-    obuffer_putmsg(obuffer_1, level, as, n);
-    extras_putmsg(level, as, n);
-}
diff --git a/src/liblimb/obuffers.h/out_putmsgdie.c b/src/liblimb/obuffers.h/out_putmsgdie.c
deleted file mode 100644
index 109bdf3..0000000
--- a/src/liblimb/obuffers.h/out_putmsgdie.c
+++ /dev/null
@@ -1,12 +0,0 @@
-/* This file is part of limb                           https://lila.oss/limb
- * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
-/* SPDX-License-Identifier: GPL-2.0-only */
-#include <unistd.h>
-#include <limb/obuffers.h>
-
-void
-out_putmsgdie(int exit, u8 level, const char * const *as, unsigned int n)
-{
-    out_putmsg(level, as, n);
-    _exit(exit);
-}
diff --git a/src/liblimb/output.h/list_matches.c b/src/liblimb/output.h/list_matches.c
index 430643b..792b729 100644
--- a/src/liblimb/output.h/list_matches.c
+++ b/src/liblimb/output.h/list_matches.c
@@ -4,10 +4,10 @@
 #include <limb/output.h>
 
 void
-list_matches(obuffer_putmsgfn putmsg, u8 level, const char *intro,
+list_matches(obuffer *obuf, u8 level, const char *intro,
              const char *prefix, const char *sep, const char *outro,
              const char *str, size_t slen, int first, const char **list)
 {
-    list_matches_full(putmsg, level, intro, prefix, sep, outro,
+    list_matches_full(obuf, level, intro, prefix, sep, outro,
                       str, slen, first, list, 0, sizeof(*list));
 }
diff --git a/src/liblimb/output.h/list_matches_full.c b/src/liblimb/output.h/list_matches_full.c
index 6957617..032bd08 100644
--- a/src/liblimb/output.h/list_matches_full.c
+++ b/src/liblimb/output.h/list_matches_full.c
@@ -4,17 +4,17 @@
 #include <limb/output.h>
 
 void
-list_matches_full(obuffer_putmsgfn putmsg, u8 level, const char *intro,
+list_matches_full(obuffer *obuf, u8 level, const char *intro,
                   const char *prefix, const char *sep, const char *outro,
                   const char *str, size_t slen, int first,
                   const void *list_, size_t offset, size_t llen)
 {
     const char *list = (const char *) list_ + first * llen;
     const char *el = * (const char **) (list + offset);
-    putmsg(level, outarray(intro, prefix, el), 3);
+    obuffer_putmsg(obuf, level, outarray(intro, prefix, el), 3);
 
     for (list += llen; (el = * (const char **) (list + offset)); list += llen)
         if (!strncmp(str, el, slen))
-            putmsg(level, outarray(sep, prefix, el), 3);
-    putmsg(level, outarray(outro, "\n", PUTMSG_FLUSH), 2);
+            obuffer_putmsg(obuf, level, outarray(sep, prefix, el), 3);
+    obuffer_putmsg(obuf, level, outarray(outro, "\n", PUTMSG_FLUSH), 3);
 }
diff --git a/src/liblimb/obuffers.h/err_putmsgdie.c b/src/liblimb/output.h/obuffer_putmsgdie.c
similarity index 62%
rename from src/liblimb/obuffers.h/err_putmsgdie.c
rename to src/liblimb/output.h/obuffer_putmsgdie.c
index dfe6af4..569aa01 100644
--- a/src/liblimb/obuffers.h/err_putmsgdie.c
+++ b/src/liblimb/output.h/obuffer_putmsgdie.c
@@ -2,11 +2,11 @@
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
 #include <unistd.h>
-#include <limb/obuffers.h>
+#include <limb/obuffer.h>
 
 void
-err_putmsgdie(int exit, u8 level, const char * const *as, unsigned int n)
+obuffer_putmsgdie(obuffer *obuf, u8 level, const char *str[], unsigned n, int exit)
 {
-    err_putmsg(level, as, n);
+    obuffer_putmsg(obuf, level, str, n);
     _exit(exit);
 }
diff --git a/src/liblimb/parseopt.h/parseopt_warn.c b/src/liblimb/parseopt.h/parseopt_warn.c
index ecad301..ef3bf29 100644
--- a/src/liblimb/parseopt.h/parseopt_warn.c
+++ b/src/liblimb/parseopt.h/parseopt_warn.c
@@ -22,7 +22,7 @@ parseopt_warn(const char **argv, const struct option *options,
                 if (ctx->idx >= 0) {
                     const char *s = argv[ctx->cur] + ctx->off;
                     size_t l = byte_chr(s, strlen(s), '=');
-                    list_matches_full(err_putmsg, OLVL_NORMAL, "did you mean ",
+                    list_matches_full(obuffer_2, OLVL_NORMAL, "did you mean ",
                                       "--", " or ", " ?", s, l, ctx->idx, options,
                                       offsetof(struct option, longopt), sizeof(*options));
                 }
diff --git a/src/liblimb/term.h/ask_password.c b/src/liblimb/term.h/ask_password.c
index fa2fbd2..3c9d812 100644
--- a/src/liblimb/term.h/ask_password.c
+++ b/src/liblimb/term.h/ask_password.c
@@ -23,13 +23,13 @@ ask_password(char *dst, size_t dlen, const char *prompt)
     /* prompt: if prefixed with '>' then use stderr instead of stdout.
      * Also a leading '\\' is skipped, to allow to start a prompt with a '>'
      * still on stderr */
-    obuffer_putmsgfn putmsg = err_putmsg;
+    obuffer *b = obuffer_2;
     if (prompt) {
         if (*prompt == '>')
-            putmsg = out_putmsg;
+            b = obuffer_1;
         if (*prompt == '>' || *prompt == '\\')
             ++prompt;
-        putmsg(OLVL_NORMAL, (const char *[]) { prompt, PUTMSG_FLUSH }, 2);
+        obuffer_putmsg(b, OLVL_NORMAL, (const char *[]) { prompt, PUTMSG_FLUSH }, 2);
     }
 
     size_t plen = 0;
@@ -43,7 +43,7 @@ ask_password(char *dst, size_t dlen, const char *prompt)
     }
 
     /* if we put up a prompt, add a newline */
-    if (prompt) putmsg(OLVL_NORMAL, (const char *[]) { "\n", PUTMSG_FLUSH }, 2);
+    if (prompt) obuffer_putmsg(b, OLVL_NORMAL, (const char *[]) { "\n", PUTMSG_FLUSH }, 2);
     /* restore echo if we did indeed turn it off */
     if (echo) {
         int e = errno;
diff --git a/src/mkrabintables/err_putmsg.o b/src/mkrabintables/err_putmsg.o
deleted file mode 120000
index f93752d..0000000
--- a/src/mkrabintables/err_putmsg.o
+++ /dev/null
@@ -1 +0,0 @@
-liblimb/obuffers.h/err_putmsg.o
\ No newline at end of file
diff --git a/src/mkrabintables/err_putmsgdie.o b/src/mkrabintables/err_putmsgdie.o
deleted file mode 120000
index 2b17e24..0000000
--- a/src/mkrabintables/err_putmsgdie.o
+++ /dev/null
@@ -1 +0,0 @@
-liblimb/obuffers.h/err_putmsgdie.o
\ No newline at end of file
diff --git a/src/mkrabintables/extras_putmsg.o b/src/mkrabintables/extras_putmsg.o
deleted file mode 120000
index f76b7c9..0000000
--- a/src/mkrabintables/extras_putmsg.o
+++ /dev/null
@@ -1 +0,0 @@
-liblimb/obuffers.h/extras_putmsg.o
\ No newline at end of file
diff --git a/src/mkrabintables/obuffer_1_2.o b/src/mkrabintables/obuffer_1_2.o
new file mode 120000
index 0000000..6d99128
--- /dev/null
+++ b/src/mkrabintables/obuffer_1_2.o
@@ -0,0 +1 @@
+liblimb/obuffer.h/obuffer_1_2.o
\ No newline at end of file
diff --git a/src/mkrabintables/obuffer_putmsgdie.o b/src/mkrabintables/obuffer_putmsgdie.o
new file mode 120000
index 0000000..c83e3db
--- /dev/null
+++ b/src/mkrabintables/obuffer_putmsgdie.o
@@ -0,0 +1 @@
+liblimb/output.h/obuffer_putmsgdie.o
\ No newline at end of file
diff --git a/src/mkrabintables/obuffers_putmsg.o b/src/mkrabintables/obuffers_putmsg.o
deleted file mode 120000
index e2f5e77..0000000
--- a/src/mkrabintables/obuffers_putmsg.o
+++ /dev/null
@@ -1 +0,0 @@
-liblimb/obuffer.h/obuffers_putmsg.o
\ No newline at end of file