4 puntos por GN⁺ 2023-09-10 | 1 comentarios | Compartir por WhatsApp
  • 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, goto y 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(), fts y 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, goto y funciones largas con switch/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, zsh o ksh
    • 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_FAILURE se 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í

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:t de kill significa que L, l y t no tienen argumento, mientras que n y s sí lo requieren
    • En todas las utilidades se usa getopt_long() para devolver la siguiente opción
    • optind apunta a la posición del siguiente argumento dentro del arreglo argv[]
    • optarg es un puntero a carácter que apunta al valor del argumento de una opción

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() o xfts_open() crea una estructura FTS a partir de una ruta
    • Los nodos de archivo o directorio del árbol se representan con la estructura FTSENT
    • Al llamar fts_read() se genera un FTSENT, y ese proceso constituye el recorrido del árbol
    • El campo FTSENT->fts_info se 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 como fwrite(), aparecen funciones no estándar como full_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() de dd y cwrite() de split

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 en system.h que 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ándar
    • PROGRAM_NAME define el nombre oficial de la utilidad y se usa al consultar la versión
    • AUTHORS define a los autores de la utilidad y se usa al consultar la versión
    • emit_try_help() muestra una sugerencia de ayuda después de una salida por fallo
    • emit_ancillary_info (PROGRAM_NAME) muestra información adicional de ayuda común después de la salida específica del comando
    • initialize_main(&argc, &argv) maneja la expansión interna de comodines en VMS y se elimina en la mayoría de los demás sistemas operativos
    • atexit(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 booleano
    • do { ... } 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 funciones exit() y error() para evitar advertencias del compilador en rutas de fallo
  • Cambios por utilidad se dividen en correcciones de bugs, nuevas funciones y optimizaciones
    • join, sort y uniq eran vulnerables a ataques por overflow hasta un patch de 2016
    • A df se le agregó la opción --output en 2013
    • yes mejoró su rendimiento con mejor buffering
  • 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 con arch, dir y vdir
  • 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 LISTF de CTSS, hacia 1963, que después se acortó a ls
  • La sintaxis particular de dd recuerda al job control language de OS/360 de inicios de los años 60
  • sort es la única utilidad que aprovecha multithreading
  • fmt muestra optimización de líneas y párrafos usando costo de función
  • yes implementa salida de alto rendimiento usando un buffer de memoria alineado a página
  • df usa metadatos del dispositivo y du revisa todos los archivos, por eso df es más rápido
  • cksum tiene dos puntos de entrada: uno para ejecución normal y otro para generar la tabla CRC-32
  • echo no tiene condición de fallo
  • El diseño de test y expr difiere mucho de la estructura típica de las utilidades
  • su originalmente se mantenía en coreutils/shellutils

Implementaciones que vale la pena revisar

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

 
GN⁺ 2023-09-10
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

    • El problema es cuál es exactamente su función. Me pregunto si habrá algún límite para lo que se puede hacer con estas herramientas
      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 para env junto 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
    • Creo que lo dicho aquí debería entenderse como “hecho para ser desarrollado activamente durante mucho tiempo”
      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
    • Probablemente se refería a una “larga vida de desarrollo”. Es decir, utilidades que prácticamente se escriben una vez y se usan durante mucho tiempo, lo cual también es una ventaja del enfoque de “hacer una sola cosa y hacerla bien”
    • Curiosamente, interpreté “long life” como un programa que se ejecuta continuamente. Lo entendí en contraste con herramientas que tienen una entrada clara, una ejecución breve y una salida clara
    • ¿Emacs?
  • 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), el od de coreutils se instala como god(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 yes resulta bastante interesante para ver cómo se escribe el código base de una utilidad que escribe en la salida estándar
    https://maizure.org/projects/decoded-gnu-coreutils/yes.html

    • Mi punto es que incluso una utilidad simple merece estar bien escrita y optimizada
      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
    • Una implementación en Go hecha por diversión sería así:
      package main
      
      import "flag"
      
      func main() {  
      yes := flag.String("m", "y", "message")  
      flag.Parse()  
      for {  
      println(*yes)  
      }  
      }  
      
  • 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, shuf o factor
    Me dan ganas de ejecutar una vez

    sudo find / -type f -exec shred {} \;  
    

    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

    • Probé algo parecido con dd, pero terminó hasta el final y fue bastante anticlimático
      Esperaba que se estrellara o al menos que se cortara la conexión, pero el kernel, sshd y bash seguían en memoria, así que volví a un prompt donde no se podía hacer nada bien
    • En Linux probablemente es muy posible que llegue hasta el final. No se puede escribir sobre los ejecutables y bibliotecas que están corriendo en ese momento (ETXTBUSY), así que shred no puede romperse a sí mismo ni romper find
  • Me gusta que /bin/true en 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

    • Otro punto interesante del comando true es que se volvió muchísimo más complejo de lo necesario
      Primero, un ejercicio:
      touch mytrue  
      chmod u+x mytrue  
      ./mytrue  
      echo "error code for mytrue is $?"  
      
      true empezó literalmente así. Bastante zen
      El 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 true por fin tuvo código, y ese código era exit 0
      Despué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 el true de 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
    • ¿No pasa eso solo cuando se ejecuta con opciones? Ejecutar simplemente true no parece verse afectado, a menos que el comentario sea confuso
  • Si 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?

    • La mayor parte de lo ingenioso en coreutils no está tanto en las estructuras de datos y algoritmos, sino en cómo se acopla eficazmente con el kernel
      Por ejemplo, usar copy_file_range() en lugar de read()/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ón
    • Tal vez lo estoy entendiendo mal, pero no veo qué relación tienen las estructuras de datos y algoritmos con las herramientas de línea de comandos
  • Tal vez me esté perdiendo la idea de este sitio, pero ¿no existen ya páginas man o páginas info para cada uno?

    • No es información de uso; este sitio explica en detalle cómo funcionan internamente los programas