Como dice el título, fmtroff es una versión más del
viejo fmt que encontrará en la mayoría de sistemas tipo
Unix:
Se trata de una utilidad para formatear párrafos en texto
plano. El nombre que elegí para mi versión puede
confundir, lo primero que cabe aclarar es que
fmtroff NO es un parser para roff, elegí un nombre distinto para evitar
sugerir que pretende ser un reemplazo a otras versiones y el sufijo
‘roff’ porque mi versión, además de reunir y mejorar algunas
características de otras versiones, también incluye alguna innovación
para hacer más fácil y fiable trabajar con archivos roff (para editar
mis novelas usé groff, la
versión de GNU).
Descargar el fichero fuente
(fmtroff.c)
Probado en OpenBSD y Linux. Espero que lo encuentre
útil.
Documentación
Para compilarlo basta con:
$ cc fmtroff.c -o fmtroff
Algún día tal vez escriba una página de manual. Por
ahora, al que esté familiarizado con fmt le bastará la
ayuda que fmtroff imprime (opción ‘-h’), donde
ya se aprecian algunas diferencias:
$ fmtroff -h
Usage: fmtroff [-bhlnp] [-w width] [file ...]
-b break sentences with a new line
-h print this help
-m try to skip mail headers and quoted text
-n format also lines beginning with a dot character
-o lowercase letters can begin a sentence (you may need this with
man pages)
-p indent the whole paragraph copying the first line indentation
-w set maximum line width (default 72 columns)
Diferencias que no se deducen de esta ayuda y es importante tener
en cuenta:
- Excepto por el sangrado, ya sea sólo
en la primera línea o en el párrafo completo al usar la opción ‘-p’
(ver segundo ítem), fmtroff siempre colapsa los
espacios, dejando uno entre palabras y dos entre
oraciones. En otras versiones, este comportamiento está
implementado como opción y no está habilitado por defecto.
- Al usar la opción ‘-p’, el párrafo se sangra
completo, copiando exactamente la misma cadena de
la sangría de la primera línea, respetando espacios, tabulaciones y
su orden.
Para llamarlo desde Vi o Vim incluya una línea como la siguiente
en ~/.nexrc o ~/.vimrc:
map v !}fmtroff -np^M
Cronología de cambios
- 17 de julio de 2026. Restablecer
explícitamente los estados de análisis global dinámicos
(header = false, nested = false) justo antes
de llamar a filecopy() dentro del bucle para evitar que
los estados se filtren entre archivos.
- 17 de julio de 2026. Semántica de tipo
modernizada usando en variables booleanas explícitas
(true/false).
- 17 de julio de 2026. Reemplacé
atoi() por strtoul() de POSIX dentro de
main().
- 17 de julio de 2026. Eliminé la variable de
tamaño fijo size_t n = 0 de
is_initial(). La reemplazé por indexación
relativa con punteros negativos (por ejemplo, r[-1 +
qcount]), eliminando el riesgo de lecturas fuera de los
límites.
- 17 de julio de 2026. Cambié el tipo de
retorno de cquote_count() a ssize_t y añadí
un casting explícito a ssize_t dentro del bloque de
seguimiento de oraciones de separate_sentences().
- 17 de julio de 2026. Reescribí
cquote_count() para usar un contador con signo
(ssize_t), evitando que i-- se desborde hasta
SIZE_MAX.
- 17 de julio de 2026. Actualizaron todas las
asignaciones de memoria dinámica para escalar según
sizeof(wchar_t) en lugar de
sizeof(wint_t).
- 3 de marzo de 2026. Corrección del conteo
de columnas en función wrap(), arrancaba de cero en
lugar de uno.
- 1 de septiembre de 2025. Se me había
escapado detectar el espacio precedente en el tercer caso de
detección de abreviaturas (is_initial()).
-
20 de agosto de 2025. Ingo Schwarze, del
equipo de OpenBSD me hizo notar que la notación 0x2e
al definir una constante es considerda del tipo int
(ver C11 6.4.4.1, Integer
constant). Yo estaba convirtiendo
int a wchar_t, lo que no tiene
sentido. Para definirlas como constantes de
characteres la notación que corresponde es L'.' (ver
C11 6.4.4.4, Character constant).
- 19 de agosto de 2025. Volví a implementar
la simplificación del 17 de agosto. No se trataba de un
problema con los compiladores sino de un fallo mío, se me había
escapado acabar con valor nulo las matrices constantes de
caracteres. Esto añadía basura en la cadena confundiendo
a funciones como wcsspn() que retornaban valores
equivocados. Agradezco a Otto Moerbeek del equipo de
OpenBSD por hacérmelo notar.
- 18 de agosto de 2025. Lamentablemente tuve
que revertir el cambio anterior. El cambio introdujo un
bug en OpenBSD no reproducible en Linux. En la entrada
del 5 de Junio mencioné que compilando en Linux el comportamiento
del ejecutable era diferente con respecto a hacerlo en
OpenBSD. Hoy instalé GCC (GNU cc) en OpenBSD y corroboré
que GCC interpreta el código de distinta manera a como lo hace
Clang.
- 17 de agosto de 2025. Separé la detección
de abreviaturas a una función aparte para ordenar un poco el
código.
- 10 de agosto de 2025. Revertí un cambio que
provocaba un error de memoria en Linux.
- 8 de agosto de 2025. Más mejoras en
reconocimiento de abreviaturas.
- 2 de agosto de 2025. Reconocer abreviaturas
incluso si están entre comillas o paréntesis.
- 2 de agosto de 2025. Pequeña modificación
en el reconocimiento de abreviaturas. No sólo una
mayúscula, una letra minúscula precedida de espacio (o signo de
acotación) y seguida de punto también es seguramente una
abreviatura.
- 5 de junio de 2025. Últimamente, usando
Linux, encontré dos bugs que no se reproducían en
OpenBSD.
- En la función cquote_count() (ex
left_quote()), no limitar el loop (regresivo)
producía un segfault cuando un símbolo de acotación
seguido por espacios se encontraba al principio de la línea.
- Me di cuenta de que en Linux, para que la función
wcsspn() retorne los valores esperados es necesario
declarar como constantes las matrices utilizadas como argumentos
(en este caso end_of_sentence[], oquote[] y
cquote[]). Para lograr el mismo efecto en
OpenBSD hay que declararlas como estáticas. Supongo
que esto se debe a que los compiladores gcc y
clang interpretan el código de diferente manera.
- 5 de junio de 2025. Cambié el nombre de la
función left_quote() por uno más apropiado:
cquote_count(). También simplifiqué el nombre
de las matrices cl_quote[] and op_quote[],
ahora cquote[] y oquote[]
respectivamente.
- 28 de enero de 2025. Quité un par de
abreviaturas que había introducido por equivocación.
- 6 de enero de 2025. En modo "troff", además
de las líneas que comienzan con punto o apóstrofe, ignorar también
las que empiezan con barra inclinada invertida.
- 26 de octubre de 2024. Reconocer barra
invertida ('\') como caracter de inicio de
oración. Esto permite usar etiquetas tanto de roff como
de LaTeX al inicio de la oración (ej. \fI o
\emph{).
- 2 de octubre de 2024. Decidí revertir los
cambios del 10 y el 22 de agosto desde que complicaban el código
sólo para cubrir casos aislados.
- 26 de septiembre de 2024. Ayer noté que al
ejecutar fmtroff en texto chino muchos caracteres se
borraban acortando la cadena. Esto sucedía en Linux,
como yo normalmente uso OpenBSD (y tampoco suelo escribir en chino
;-)) no lo había notado. Después de investigar, me di
cuenta de que la causa estaba en collapse_whitespace(),
donde usaba isblank() en lugar de
iswblank(). Pensaba que en este caso no era
necesario usar la versión de caracteres anchos de la función, pero
estaba equivocado.
- 22 de agosto de 2024. Antes de salvar la
variable explicada en el item anterior, comprobar que la palabra
contiene sólo letras para asegurarse de que es un nombre de
persona.
- 10 de agosto de 2024. Salvar en una
variable si la presente palabra empieza con mayúsculas (lo que
sugiere que es un nombre) para reconocer si la que sigue es una
inicial.
- 9 de mayo de 2024. Simplifiqué un poco el
código.
- 7 de mayo de 2024. Agregué la opción
‘-m’ que, activada, indica a fmtroff que
evite dar formato a los encabezados de correo electrónico y al
texto acotado (agregué esta opción sólo por ser coherente con el
uso original de fmt(1)).
- 7 de mayo de 2024. Cambié la opción
‘-t’ por ‘-b’ y la opción ‘-l’ por
‘-o’, para ganar compatibilidad con la versión de
fmt(1) de OpenBSD.
- 6 de mayo de 2024. Ahora fmtroff
evita formatear el código anidado de tablas, gráficos y ecuaciones
(tbl(1), pic(1),
eqn(1)). Gracias Victor <vico arroba
tuta punto io> por indicármelo.
- 7 de noviembre de 2023 (bug). Corregido un
error creado en las últimas modificaciones.
- 8 de octubre de 2023. Una vez más reescribí
todo el código ahora usando funciones de las bibliotecas de C y de
caracteres anchos.
- 1 de octubre de 2023. Reescribí todo el
código usando punteros en lugar de matrices de longitud
variable.
- 8 de septiembre de 2023. A excepción de
‘nueva línea’ y ‘tabulador’, fmtroff limpia los llamados caracteres
de control. Hoy leyendo una interesante discusión en las
listas de correo de groff me enteré de que con roff se puede
utilizar el caracter leader o SOH (ASCII 0x01) en
tablas o índices, de ahí que modifiqué el código para que, en modo
‘troff’ (opción ‘-t’), fmtroff preserve este
caracter.
- 24 de agosto de 2023 (bug). Hoy descubrí
que es necesario reservar memoria antes de entrar al loop
que lee el fichero (o cadena de entrada). Sin esto
llamar fmtroff desde un editor de texto (nvi o vim) en un fichero
vacío produce un segfault.
- 28 de junio de 2023. Mejoré el
reconocimiento de iniciales para que ignore paréntesis o comillas y
le agregué soporte de mayúsculas no ASCII.
- 25 de abril de 2023 (bug). Pasaron años sin
usar iso-latin, por esto no fue hasta ayer que, mientras tecleaba
algún texto en la consola de OpenBSD, me di cuenta de que fmtroff
se colgaba al encontrar caracteres iso-latin. Tomó sólo
modificar un condicional para corregir el error, lo que significa
que ahora la limitación de UTF-8 sólo se aplica a caracteres
multibyte.
- 6 de abril de 2023 (bug). En modo troff o
siempre que la opción ‘-n’ no se utilice, tratar las
líneas que comienzan con ‘'’ (comilla simple) igual que
las que comienzan con punto, ya que aquellas también son utilizadas
en macros de troff.
- 5 de abril de 2023. No agregar doble
espacio luego de puntos suspensivos cuando éstos se hallan al
principio de la línea.
- 28 de marzo de 2023. Nueva opción
‘-l’, permite que las oraciones comiencen con minúscula
(útil con páginas de manual donde comenzar la oración con un nombre
de comando es recurrente).
- 25 de marzo de 2023 (bug). Ignorar palabra
cuando su número de caracteres supera el límite de columnas (ej
URLs).