Decodificando GNU Coreutils (2018)
(maizure.org)- Proyecto de explicación de largo plazo que recorre la estructura del código fuente de GNU coreutils 8.3 utilidad por utilidad, pensado para programadores principiantes que quieren aprender diseño de herramientas de línea de comandos
- No es documentación de uso, sino material de apoyo para leer el código fuente, y cubre junto con cada comando su espacio de nombres, flujo de ejecución, decisiones de diseño, algoritmos y el linaje de UNIX y de los primeros Coreutils
- Como coreutils tiene muchos programas pequeños en C con un solo propósito, hay que leer dentro de su contexto histórico patrones de código toscos desde una mirada moderna como variables globales, macros,
gotoy funciones largas - El flujo común sigue inicialización, análisis de opciones, procesamiento de entrada, ejecución de llamadas al sistema y manejo de fallos, y aparecen repetidamente
getopt_long(),ftsy helpers de gnulib - Antes de contribuir o modificar algo, primero hay que verificar si se puede reproducir con herramientas existentes, si rompe la compatibilidad hacia atrás y cuánto se desvía de POSIX
Alcance del proyecto de explicación de GNU coreutils
- Es un proyecto de largo plazo que explica todas las utilidades de GNU coreutils 8.3
- El público objetivo son programadores principiantes que quieren examinar el diseño de utilidades de línea de comandos
- Funciona mejor como material de contexto al leer el source code de la utilidad que te interese
- Este material no es un manual para usuarios; para el uso real hay que consultar las man pages
- El proyecto está compuesto por varias etapas
- Fase 1: se completaron las páginas de espacio de nombres y panorama de ejecución de cada utilidad
- Fase 2: se completaron las principales decisiones de diseño y algoritmos, el linaje de UNIX y de los primeros Coreutils, recorridos del código fuente más útiles y visualizaciones de la evolución del código fuente
- Fase indefinida: el recorrido línea por línea del código de cada utilidad sigue siendo una tarea de largo plazo
- Se ofrece un GitHub repo para recopilar notas línea por línea
Páginas de explicación por utilidad
- Cada nombre de comando enlaza a una página detallada que decodifica esa utilidad
- Cada página incluye discusión, código fuente y walkthrough
- Las utilidades en negritas son entradas ampliadas en la Fase 2
- La lista incluye comandos de GNU coreutils como
arch,base64,cat,chmod,cp,date,dd,df,ls,rm,sort,tail,tr,wc,yes, entre otros
Supuestos que conviene conocer antes de leer el código
- Muchas utilidades de coreutils tienen una historia de casi 30 años, y su código ha sido modificado por muchas personas durante mucho tiempo
- La mayoría son programas pequeños orientados a un solo propósito
- En general son programas de un solo archivo fuente
- No es código diseñado para sobrevivir mucho tiempo o expandirse más allá de su función
- Se ven patrones como variables globales, macros,
gotoy funciones largas conswitch/bucles anidados
- El conocimiento de POSIX es importante para entender el código
- Conviene empezar por Utility Syntax Guidelines
- POSIX define la interoperabilidad entre entrada y salida, pero deja la forma real de trabajar a cada implementación
- GNU coreutils puede no seguir POSIX de forma estricta, pero conceptos como bits de permisos, uid/gid, variables de entorno y estados de salida están profundamente incorporados
- Por temas de portabilidad, coreutils depende mucho de gnulib
- Casi todas las utilidades incluyen funciones de gnulib diseñadas para resolver problemas comunes en distintos sistemas
- GNU coreutils se ejecuta suponiendo el apoyo de un shell como
bash,zshoksh- El shell ejecuta utilidades con fork/clone, pasa argumentos, configura el entorno, redirige entrada y salida con pipes y conserva el valor de salida
- GNU coreutils surgió originalmente de tres paquetes: utilidades de shell, de texto y de archivos, y las utilidades del mismo tipo comparten muchos patrones de diseño
Patrones básicos de diseño
- La mayoría de las utilidades CLI tienen en común tres etapas
- Una etapa de configuración que prepara flags, opciones y localización
- Una etapa de parsing de argumentos que lee la entrada y define los parámetros de ejecución
- Una etapa de procesamiento y ejecución que prepara la entrada y la pasa a una o más llamadas al sistema
- Durante la ejecución pueden verificarse restricciones y producirse fallos en varios puntos
- Diferentes estados de salida pueden insinuar dónde ocurrió el problema
EXIT_FAILUREse usa con frecuencia como estado de fallo general
- Después de un fallo se da retroalimentación al usuario
- Las variantes de utilidades se dividen en tres grupos
- Trivial utilities: definen unas pocas líneas de macros, luego incluyen el código fuente de otra utilidad y con macros fuerzan cierto control de flujo. Ejemplos:
arch,dir,vdir - Wrapper utilities: después de la configuración y del parsing de opciones, pasan directamente las opciones de línea de comandos como argumentos de llamadas al sistema. El resultado de la llamada al sistema se vuelve el resultado de la utilidad y hay poco procesamiento propio. Ejemplos:
link,whoami,hostid,logname - Full utilities: tienen configuración, parsing de opciones y argumentos, procesamiento de datos de entrada y ejecución de varias llamadas al sistema; la mayoría de las utilidades cae aquí
- Trivial utilities: definen unas pocas líneas de macros, luego incluyen el código fuente de otra utilidad y con macros fuerzan cierto control de flujo. Ejemplos:
Inicialización común y parsing de opciones
- Todas las utilidades tienen un breve procedimiento de inicialización al inicio de
main()initialize_main (&argc, &argv);set_program_name (argv[0]);setlocale (LC_ALL, "");bindtextdomain (PACKAGE, LOCALEDIR);textdomain (PACKAGE);atexit (close_stdout);
- Este código de preprocesamiento se encarga de tareas de gestión como internacionalización y registro de comportamiento al salir, y no afecta el comportamiento central de cada utilidad
- La familia de herramientas Getopt cumple un papel central en el parsing de opciones de línea de comandos
- Las opciones cortas usan el prefijo
-y las largas usan-- - Las opciones cortas se definen como cadena y las largas usan struct
- En la cadena de opciones cortas, si no hay argumento se escribe solo el carácter; para argumento obligatorio se agrega
:, y para argumento opcional:: - La cadena de opciones cortas
Lln:s:tdekillsignifica queL,lytno tienen argumento, mientras quenyssí lo requieren - En todas las utilidades se usa
getopt_long()para devolver la siguiente opción optindapunta a la posición del siguiente argumento dentro del arregloargv[]optarges un puntero a carácter que apunta al valor del argumento de una opción
- Las opciones cortas usan el prefijo
Recorrido del sistema de archivos y helpers para llamadas al sistema
- En sistemas tipo Unix suele existir soporte para la biblioteca fts, que facilita el recorrido del sistema de archivos
fts_open()oxfts_open()crea una estructuraFTSa partir de una ruta- Los nodos de archivo o directorio del árbol se representan con la estructura
FTSENT - Al llamar
fts_read()se genera unFTSENT, y ese proceso constituye el recorrido del árbol - El campo
FTSENT->fts_infose usa con frecuencia para describir la entrada y decidir cómo procesarla
- Además de lo que entrega libc, coreutils usa muchos wrappers de llamadas al sistema y helpers
- En la escritura, además del wrapper de la llamada al sistema
write()y funciones de libc comofwrite(), aparecen funciones no estándar comofull_write()- full_write() sigue reintentando la escritura mientras no ocurra un fallo duro
- safe_write() vuelve a intentar la llamada al sistema
write()incluso si hubo una interrupción - También hay helpers de escritura usados solo por una utilidad concreta, como
iwrite()deddycwrite()desplit
Funciones comunes y modismos de código
- Todas las utilidades usan al menos tres funciones:
main(),usage()y_() usage()muestra ayuda con parámetros de entrada, significado y sintaxis correcta_()es una macro definida ensystem.hque conecta cadenas simples con la función Native Language Support de GNU gettext.h- Si una cadena se muestra al usuario, por lo general está envuelta con esta macro
- En la mayoría de las utilidades no triviales se repiten líneas de código comunes
#include "system.h"define macros dependientes del sistema, variables y funciones utilitarias no estándarPROGRAM_NAMEdefine el nombre oficial de la utilidad y se usa al consultar la versiónAUTHORSdefine a los autores de la utilidad y se usa al consultar la versiónemit_try_help()muestra una sugerencia de ayuda después de una salida por falloemit_ancillary_info (PROGRAM_NAME)muestra información adicional de ayuda común después de la salida específica del comandoinitialize_main(&argc, &argv)maneja la expansión interna de comodines en VMS y se elimina en la mayoría de los demás sistemas operativosatexit(close_stdout)registra una función para vaciar y cerrar streams con buffer al terminar el programa
- También aparecen modismos de C que pueden resultar extraños para principiantes
!!es una doble operación unaria NOT que se usa para forzar la conversión de un valor a booleanodo { ... } while (0)es un patrón no iterativo que envuelve macros de varias sentencias para que la tokenización sea segura después de la sustitución del preprocesador
Forma de mantenimiento y revisión antes de contribuir
- Un proyecto activo como coreutils suele evolucionar siguiendo tres flujos de mantenimiento
- Cambios de todo el proyecto son cambios grandes que afectan la estructura y dependencias de todas las utilidades
- En 1995 se añadió Native Language Support mediante el proyecto GNU gettext project, y se introdujo la macro
_()en la mayoría de las líneas de salida de texto - En 1996 se amplió el soporte de internacionalización y se agregaron varias líneas de inicialización a
main() - En 1995 se añadió una breve descripción del propósito de la utilidad a la salida de uso
- En 2003 se agregó soporte para comodines de VMS, visible en la función
initialize_main() - En 2016 la macro
die()reemplazó la mayoría de las funcionesexit()yerror()para evitar advertencias del compilador en rutas de fallo
- En 1995 se añadió Native Language Support mediante el proyecto GNU gettext project, y se introdujo la macro
- Cambios por utilidad se dividen en correcciones de bugs, nuevas funciones y optimizaciones
- Mantenimiento anual incluye como mínimo actualizar el año de copyright de todas las utilidades
- También hay cambios administrativos, como actualizar la dirección de la FSF, que no afectan la ejecución
- Conviene que los contribuidores revisen primero la GNU project page, las contribution guidelines, las rejected features y los mailing list archives
- Antes de escribir código hay tres preguntas que conviene verificar
- Si la misma función puede reproducirse con herramientas existentes
- Si la contribución rompe la compatibilidad hacia atrás
- Si el comportamiento propuesto se aparta mucho de POSIX
- Si no hay certeza, se recomienda consultar a la comunidad en la mailing list
Detalles interesantes
- La utilidad más corta es
false, con 2 líneas, empatada conarch,diryvdir - La utilidad ejecutable independiente más corta es
true, con 80 líneas, y su primera versión está muy cerca de un programa mínimo en C - La utilidad más larga es
ls, con 5308 líneas - Muchas utilidades se remontan al Research UNIX de los años 70, y algunas incluso a Multics
- El ancestro conceptual más antiguo es el comando
LISTFde CTSS, hacia 1963, que después se acortó alsLISTFfue una de las 18 utilidades originales del paper de diseño de 1962
- La sintaxis particular de
ddrecuerda al job control language de OS/360 de inicios de los años 60 sortes la única utilidad que aprovecha multithreadingfmtmuestra optimización de líneas y párrafos usando costo de funciónyesimplementa salida de alto rendimiento usando un buffer de memoria alineado a páginadfusa metadatos del dispositivo ydurevisa todos los archivos, por esodfes más rápidocksumtiene dos puntos de entrada: uno para ejecución normal y otro para generar la tabla CRC-32echono tiene condición de fallo- El diseño de
testyexprdifiere mucho de la estructura típica de las utilidades suoriginalmente se mantenía en coreutils/shellutils
Implementaciones que vale la pena revisar
- shuf y shred usan el ISAAC cipher) para números aleatorios
- shuf usa Reservoir sampling
- sum usa checksums heredados de System V y BSD
- expr usa evaluación de expresiones asociativa por la izquierda
- shred usa Secure overwrite
- cksum calcula CRC-32 checksum
- sort usa una variante de Merge sort
- factor es una utilidad donde vale la pena mirar elementos de implementación matemática
Aviso sobre apoyo
- No está configurado para recibir apoyo individual
- Si quieres compartir tiempo o dinero, puedes contribuir a la Free Software Foundation
1 comentarios
Opiniones de Hacker News
Si dicen que “muchas de estas utilidades tienen casi 30 años y, durante ese tiempo, mucha gente las modificó”, pero aun así “no fueron diseñadas para extenderse más allá de una larga vida útil o de su función”, me gustaría ver ejemplos de programas pensados para una larga vida útil según el autor
Me da curiosidad tanto si son programas que sobrevivieron 30 años como si son programas que él cree que durarán otros 30
Para probar un pequeño lenguaje de programación que hice, armé un framework de pruebas con bash y coreutils. Al principio me incomodaba no usar un lenguaje “de verdad”, pero en la práctica funcionó muy bien e incluso ejecutaba en paralelo
Lo único que no pude probar fue el
argv[0]del programa, y por más combinaciones que intenté no pude producir exactamente el comportamiento que quería, así que envié a coreutils una solicitud de función paraenvjunto con un parche: https://lists.gnu.org/archive/html/coreutils/2023-08/msg0006...Parece que lo van a incorporar, así que sería una función nueva entrando en un programa antiguo
Las prácticas que enumera el autor son cosas que, en programas grandes, normalmente se critican porque perjudican la mantenibilidad a medida que crece la escala
Materiales que vale la pena consultar:
Cómo prueba GNU coreutils: https://www.pixelbeat.org/docs/coreutils-testing.html
Exploración de cada comando de coreutils: https://ratfactor.com/slackware/pkgblog/coreutils
Procesamiento de texto en la línea de comandos con GNU Coreutils: https://learnbyexample.github.io/cli_text_processing_coreuti... — mi ebook, que cubre más de 20 herramientas de procesamiento de texto
Artículos relacionados:
Decoded: GNU Coreutils (2018) - https://news.ycombinator.com/item?id=29871037 - enero de 2022, 7 comentarios
Decoded: GNU coreutils (2019) - https://news.ycombinator.com/item?id=26411908 - marzo de 2021, 38 comentarios
Decoded: GNU Coreutils - https://news.ycombinator.com/item?id=20328650 - julio de 2019, 55 comentarios
Dato curioso: si instalas coreutils con Homebrew en macOS, como macOS ya tiene
od(1), elodde coreutils se instala comogod(1)Si el autor está leyendo, hay al menos un error. La descripción breve de la página de shred[0] en realidad es la descripción de
csplit[1], y debería ser algo como “sobrescribe el contenido de un archivo para ocultarlo y, opcionalmente, lo elimina”[0]: https://maizure.org/projects/decoded-gnu-coreutils/shred.htm...
[1]: https://maizure.org/projects/decoded-gnu-coreutils/csplit.ht...
No sabía que esto existía; está genial. Incluso algo tan simple como
yesresulta bastante interesante para ver cómo se escribe el código base de una utilidad que escribe en la salida estándarhttps://maizure.org/projects/decoded-gnu-coreutils/yes.html
Por ejemplo, está bueno tener un ejemplo mínimo del que se pueda aprender cómo escribir salida muy rápido. Como el programa es tan corto, la cantidad de líneas no importa mucho; si el autor sabe cómo hacerlo, me parece mejor que lo haga lo más rápido posible para que no estorbe
Como lista de utilidades básicas, es bastante divertida. Aunque uso UNIX desde hace muchísimo tiempo, nunca había oído hablar de cosas como
shred,shufofactorMe dan ganas de ejecutar una vez
y ver cuánto aguanta antes de matarse a sí misma. Claro, en una máquina virtual o en un equipo que se pueda volver a flashear fácilmente
dd, pero terminó hasta el final y fue bastante anticlimáticoEsperaba que se estrellara o al menos que se cortara la conexión, pero el kernel,
sshdybashseguían en memoria, así que volví a un prompt donde no se podía hacer nada bienETXTBUSY), así queshredno puede romperse a sí mismo ni romperfindMe gusta que
/bin/trueen realidad pueda fallar y devolver false. Técnicamente, eso hace más robusta la llamada a “Not/bin/false”: https://github.com/coreutils/coreutils/blob/master/src/true....Claro, sé que es un caso que casi nunca va a ocurrir, pero igual da risa
truees que se volvió muchísimo más complejo de lo necesarioPrimero, un ejercicio:
trueempezó literalmente así. Bastante zenEl primer cambio fue por temas legales. Todo el código tenía que tener un aviso de copyright, y los archivos vacíos también lo necesitaban. Así que un archivo que no tenía nada pasó a ser un archivo que solo tenía un aviso de copyright, lo que plantea la pregunta: “¿se puede poner copyright sobre la nada?”. AT&T lo intentó
Luego alguien dijo que los programas debían estar bien definidos y no depender de un comportamiento accidental de Unix, lo cual en ese momento era bastante razonable. Así que
truepor fin tuvo código, y ese código eraexit 0Después alguien dijo que las utilidades del sistema debían escribirse en C en vez de shell para que fueran más rápidas, y OpenBSD todavía tiene un buen ejemplo de esa forma: http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr....
En algún momento intervino la burocracia de GNU y dijo que todos los programas debían soportar la bandera
-h, y luego que todos los programas debían soportar locales, así que eltruede GNU actual, sorprendentemente, tiene 80 líneas: https://github.com/coreutils/coreutils/blob/master/src/true....Se podría decir que está bien, pero para un programa que por definición “no hace nada y tiene éxito”, es mucho código
http://trillian.mit.edu/~jc/humor/ATT_Copyright_true.html
trueno parece verse afectado, a menos que el comentario sea confusoSi eres un programador principiante que quiere desarrollar intuición sobre cómo aplicar de forma útil estructuras de datos y algoritmos, ¿hay algo aquí que convenga mirar en particular?
Por ejemplo, usar
copy_file_range()en lugar deread()/write()para evitar copiar datos al espacio de usuario. Está más cerca de la ingeniería de software que de la ciencia de la computaciónTal vez me esté perdiendo la idea de este sitio, pero ¿no existen ya páginas man o páginas info para cada uno?