3 puntos por GN⁺ 2024-12-14 | 1 comentarios | Compartir por WhatsApp
  • La experiencia percibida de cómo funciona una terminal surge de la combinación del sistema operativo, el shell, el emulador de terminal y también de las convenciones de cada programa; y esas convenciones son más consistentes de lo que parece
  • POSIX trata principalmente la interacción entre el emulador de terminal, el sistema operativo y el shell, además de algunas utilidades clave, pero no define hasta el detalle el comportamiento de programas como htop
  • Se repiten patrones como Ctrl-C, q, Ctrl-D, los 16 colores ANSI, los atajos de readline, desactivar colores al enviar salida por pipe y el significado de - como stdin/stdout
  • Comportamientos que parecen leyes naturales en realidad deben ser implementados por el programa o por la biblioteca de entrada; ejemplos de eso son salir de ipython con Ctrl-D y cancelar una búsqueda con Ctrl-C en prompt-toolkit
  • Al crear un nuevo programa de terminal o al intentar entender cómo funciona una herramienta, suele ser más práctico partir de las expectativas por defecto y sus excepciones que de la idea de que “todo es posible”

Los cuatro actores que forman el comportamiento de la terminal

  • Lo que ocurre en una terminal puede verse, en general, como el resultado conjunto de cuatro actores
    • el sistema operativo
    • el shell
    • el emulador de terminal
    • el programa que se está ejecutando en ese momento, como top, vim o cat
  • En combinaciones como bash, GNOME Terminal y Linux, los tres primeros elementos son relativamente conocidos y parte de su comportamiento está estandarizado por POSIX
  • Los programas individuales pueden parecer capaces de hacer cualquier cosa, pero en la experiencia real de uso suelen comportarse de maneras bastante consistentes

Hay pocos estándares, pero sí hay convenciones

  • Casi no existen estándares reales que definan de forma integral el comportamiento de los programas de terminal
  • Hay dos referencias cercanas
    • POSIX: trata sobre todo la interacción entre el emulador de terminal, el sistema operativo y el shell, y también define parte del comportamiento de algunas utilidades esenciales como cp
    • command line interface guidelines: una guía sobre convenciones de interfaces de línea de comandos
  • Estas reglas no son prescripciones obligatorias que todo autor de programas deba seguir, sino más bien convenciones técnicas observadas durante 20 años usando terminales
  • Hay muchas excepciones, y a menudo esas excepciones tienen buenas razones

Algunas reglas deben implementarse directamente en el programa

  • La ubicación de los archivos de configuración, la salida de --help y la práctica de escribir la salida normal en stdout y los errores en stderr son responsabilidades bastante claras del programa
  • En cambio, algunos comportamientos parecen naturales, pero en realidad deben ser manejados directamente por el programa o por una biblioteca de entrada
  • Un ejemplo representativo es salir de un REPL con Ctrl-D
    • cat no necesita una implementación aparte porque, en cooked mode, el sistema operativo devuelve EOF
    • ipython implementa el manejo de Ctrl-D con código de prompt-toolkit
  • Entender esta responsabilidad de implementación hace que sorprenda menos que los detalles del comportamiento varíen un poco de un programa a otro

Regla 1: los programas no interactivos terminan con Ctrl-C

  • Los programas no interactivos normalmente terminan cuando se presiona Ctrl-C
  • Esto se debe a que, si el programa no configura un manejador para la señal SIGINT, por defecto termina con Ctrl-C
  • Esta regla no se aplica igual a programas interactivos como python3, bc o less
    • En un programa interactivo, Ctrl-C puede interrumpir la tarea en curso en lugar de cerrar el programa
    • Algunos ejemplos son una búsqueda en less o código Python en ejecución dentro de python3
  • La biblioteca de manejo de entrada prompt-toolkit, usada por ipython, tiene código para cancelar búsquedas con Ctrl-C

Regla 2: las TUI suelen cerrarse con q

  • Los programas TUI como less o htop normalmente se cierran al presionar q
  • Esta regla no aplica a programas donde salir con q no resulta natural
    • tmux
    • editores de texto

Regla 3: los REPL suelen salir con Ctrl-D en una línea vacía

  • Los REPL como python3 o ed normalmente terminan al presionar Ctrl-D en una línea vacía
  • Si se ejecuta un programa como cat en cooked mode, el sistema operativo devuelve EOF cuando se presiona Ctrl-D en una línea vacía
  • sqlite3, python3, fish y bash, entre otros, en realidad no usan cooked mode, pero implementan este atajo para imitar el comportamiento por defecto
  • Se pueden ver ejemplos de implementación en dos lugares
  • El REPL de Erlang es una excepción: no termina con Ctrl-D

Regla 4: no pasar de los 16 colores básicos

  • En general, los programas de terminal no usan mucho colores fuera de los 16 colores ANSI básicos
  • Si se especifican colores hex directamente, hay mucha probabilidad de que choquen con el color de fondo del usuario
    • Por ejemplo, texto #EEEEEE puede ser casi invisible sobre un fondo blanco, pero verse bien sobre uno oscuro
  • Si se usan los 16 colores base, es más probable que el usuario ya los haya configurado en su emulador de terminal para que combinen bien con el fondo
  • Este enfoque también reduce las suposiciones sobre los colores que admite el emulador de terminal
  • Hay excepciones, como los editores de texto
    • Helix usa por defecto un fondo morado en lugar de un color ANSI básico
    • Helix no se considera un programa “central”, y se asume que quien no esté conforme con la paleta puede cambiar el tema

Regla 5: cierto soporte para atajos de readline

  • Casi todos los programas soportan en alguna medida los atajos de readline cuando eso tiene sentido
  • Muchos programas no usan readline directamente, pero imitan atajos al estilo emacs/readline
  • No todos los programas lo imitan exactamente de la misma forma
    • atuin parece usar Ctrl-A como prefijo, así que Ctrl-A no mueve el cursor al inicio de la línea
  • Estos programas implementan sus propios búferes de cortar y pegar, por lo que se puede borrar una línea con Ctrl-U y luego pegarla con Ctrl-Y
  • También hay excepciones
    • programas como git, cat o nc no ofrecen edición de línea más allá de backspace, Ctrl-W y Ctrl-U
    • los editores de texto tienen cada uno su propio modo de edición
  • Este tema se trata más a fondo en entering text in the terminal is complicated

Regla 5.1: Ctrl-W borra la última palabra

  • Fuera de los editores de texto, no he visto programas donde Ctrl-W no borre la última palabra
  • En cooked mode, el sistema operativo por defecto borra la última palabra con Ctrl-W y elimina toda la línea con Ctrl-U
  • Muchos programas imitan este comportamiento por defecto
  • No se me ocurren excepciones fuera de los editores de texto

Regla 6: desactivar colores al escribir a un pipe

  • La mayoría de los programas desactivan los colores cuando la salida va a un pipe
  • Ejemplos
    • rg blah resalta blah cuando la salida va a la terminal, pero desactiva el resaltado cuando la salida se envía a un pipe o a un archivo
    • ls --color=auto usa colores al escribir a la terminal y no los usa al escribir a un pipe
  • El formato también puede cambiar según si la salida va o no a una terminal
    • ls organiza los archivos en columnas cuando escribe a una terminal
    • ripgrep agrupa las coincidencias junto con su encabezado
  • Si se quiere forzar la visualización de colores, se puede usar unbuffer para hacer que la salida del programa parezca venir de un tty
unbuffer rg blah |  less -R
  • Algunos programas permiten forzar los colores con la bandera --color
    • El ejemplo anterior también se puede hacer con rg --color=always | less -R

Regla 7: - significa stdin o stdout

  • Normalmente, si se pasa - en lugar de un nombre de archivo, el programa lee de stdin o escribe en stdout según corresponda
  • Si quieres formatear código Python del portapapeles con black y luego volver a copiarlo, puedes ejecutar esto
pbpaste | black - | pbcopy
  • pbpaste es un programa de Mac; en Linux se puede hacer algo parecido con xclip
  • Parece que la mayoría de los programas implementan esta convención cuando tiene sentido, aunque puede haber muchas excepciones

Por qué estas reglas tardan tanto en aprenderse

  • Estas reglas no suelen aprenderse de forma explícita de una sola vez, sino a través de varias etapas
    • Primero se aprende que existe una regla, como “Ctrl-C termina un programa”
    • Luego se descubren excepciones, como que find sí termina pero less no
    • Después se interioriza sin pensarlo demasiado el patrón de que en programas no interactivos sí termina, mientras que en los interactivos puede limitarse a interrumpir la tarea actual
    • Solo más tarde se logra formular eso como una regla explícita
  • Mucha de la comprensión sobre la terminal sigue estando en la etapa de reconocimiento inconsciente de patrones
  • Poner estas reglas por escrito puede ayudar a que otras personas aprendan un poco más rápido cómo funciona la terminal

1 comentarios

 
GN⁺ 2024-12-14
Opiniones en Hacker News
  • Además, los programas de terminal deben respetar los colores predeterminados de primer plano/fondo del usuario y no cambiarlos sin motivo.
    Aunque usen colores, deben seguir siendo legibles sin importar cuáles sean el fondo/primer plano predeterminados y la paleta de colores de la terminal; si se transmite significado solo con color, se pierde al copiar/pegar o en terminales sin soporte de color.
    Los emojis y los caracteres no ASCII innecesarios también deben usarse con cuidado, y en una salida estándar que va haciendo scroll conviene no borrar información importante como advertencias mostradas temporalmente o nombres de archivo.
    Si quieren usar muchos colores llamativos y animaciones, como mínimo deben ofrecer una opción de configuración fácil para desactivarlos.

    • Hace unos años, en Docker, sufrí un bug muy molesto en el que, cuando un script imprimía un emoji en la salida estándar, la terminal interactiva moría e incluso el contenedor se cerraba.
      No sé si se corrigió la causa raíz, pero terminé haciendo PRs de workaround en varios proyectos open source para que renunciaran un poco a su intención artística original: https://github.com/docker-archive/toolbox/issues/695
    • Hacer que los colores sean legibles sin importar cuáles sean el fondo/primer plano predeterminados y la paleta de colores de la terminal es, en la práctica, casi imposible.
      Sin embargo, sí se pueden ofrecer colores personalizados.
    • Como señala el texto original, estas reglas no son prescriptivas, sino observaciones técnicas.
      Son más bien una recopilación de cosas que, en general, se puede asumir que todos los programas de terminal ya siguen.
    • Hay un intento de estandarizar esto: https://bixense.com/clicolors/
    • Los emojis y los caracteres no ASCII innecesarios merecen más énfasis.
      Hoy algunos autores crean herramientas suponiendo que el usuario usa fuentes parcheadas, lo que puede perjudicar mucho la usabilidad.
  • Buen artículo.
    Como menciona que estas convenciones son difíciles de aprender, también recomiendo Command Line Interface Guidelines, mi referencia favorita al pensar en CLI: https://clig.dev
    Incluye las reglas del texto original, como salir con Ctrl-C, aceptar - en la entrada estándar y desactivar colores en pipes, además de mucho más.

    • Ese recurso ya aparece referenciado justo después del índice, al principio del artículo.
    • También vale la pena recomendar las lecturas adicionales de CLIG: https://clig.dev/#further-reading
      Incluyen materiales de POSIX, GNU y Unix, la guía de Heroku CLI y la guía de apps CLI de 12 factores.
    • También está esto: https://usage.jdx.dev/
  • El punto de que “la salida normal debe ir a stdout y los errores a stderr” es realmente importante.
    La salida estándar debería ser solo para los datos que se le pidió al programa generar. Si el usuario pidió datos JSON, en la salida estándar debe estar exactamente ese objeto JSON y nada más, jamás.
    El nombre error estándar en realidad está algo mal puesto; se parece más a un “stream estándar de usuario” al que se envían todos los mensajes que el usuario debe leer en la terminal. No solo los mensajes de error, sino también los mensajes de estado y la salida detallada pertenecen ahí.
    Así la salida del programa se puede pasar por pipe sin problemas a otros programas, y la información que no es datos puede seguir yendo a la terminal o redirigirse a otro lugar.
    Sería bueno que un programa pudiera crear fácilmente nuevos descriptores de archivo para conexiones de terminal con propósitos específicos, y documentar sus números en el manual, como con los códigos de salida. Por compatibilidad, el valor predeterminado podría enviarse a stdout o stderr, y pienso experimentar un poco con esto.

  • Un par de agregados: si es la primera vez que oyes hablar de los atajos readline/emacs Ctrl-E y Ctrl-W, conviene saber que la mayoría de las fuentes de entrada de macOS usan esos atajos.
    Si estás en macOS, puedes probar Ctrl-E, Ctrl-W y Ctrl-U ahora mismo en la barra de direcciones del navegador.
    Si usas un programa de línea de comandos que no soporta edición de línea, puedes instalar rlwrap y ejecutar el REPL debajo de él. Por ejemplo, Standard ML of New Jersey tiene REPL, pero no funciones de edición de línea, así que se puede complementar con rlwrap smlnj.
    Sobre “no usar más de 16 colores”, yo iría más allá y diría “no usen más de 8 colores, o al menos hagan que los colores sean configurables”. Paletas populares como Solarized y Base 16 predeterminada a veces usan los colores “brillantes” para varios tonos de gris, de modo que el verde brillante que el autor tenía en mente puede ser en realidad el mismo gris que el texto normal.

    • Si presionas Ctrl-W en la barra de direcciones de macOS, en la mayoría de los navegadores que he usado se cierra la pestaña actual.
      Mi emulador de terminal, Alacritty, también hace eso, y la mayoría de los exploradores de archivos con pestañas se comportan igual. Según recuerdo, Windows Explorer ahora también, aunque hace bastante que no uso Windows.
  • La convención de salir de un REPL con Ctrl-D siempre me hace tropezar en GHCi
    Normalmente presiono Ctrl-D, me desconcierto porque no pasa nada, luego recuerdo que en GHCi no funciona y ejecuto :q
    Entonces Ctrl-D inserta un carácter invisible al inicio de la entrada, aparece el error “lexical error at character '\EOT'”, y tengo que escribir :q de nuevo sin ese prefijo invisible para que GHCi se cierre

    • Si intentas salir de vi en un teclado de un país desconocido, te parecerá aún más difícil
    • Yo siempre cierro GHCi con Ctrl-D
      Ni siquiera sé bien cuál es la otra forma; ¿hay alguna configuración que cambie este comportamiento?
    • ¿Estás usando Windows?
  • Algo que falta es el comportamiento de tratar ~ como el directorio home
    Esto parece ser una funcionalidad del shell, no de la API POSIX. En Go, os.ReadFile("~/.bashrc") no funciona, pero en el shell echo ~/~/~ se expande a algo como /home/jrockway/~/~
    yourprogram ~/path/to/file siempre funciona, pero en un REPL que pregunta por un nombre de archivo puede que no. En la práctica, mucho software lo soporta, así que creo que podría entrar en “lo que hacen la mayoría de los programas TUI”

    • La expansión de tilde es parte de la expansión de palabras del shell: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06_01
      En la expansión de palabras, la expansión de tilde es el primer paso, seguido por la división de campos, la expansión de nombres de ruta y la eliminación de comillas
      La especificación relacionada del shell está en https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06, y la especificación completa puede verse en https://pubs.opengroup.org/onlinepubs/9799919799/
    • Creo que esto debería seguir siendo una función del shell
      Si una herramienta agrega expansión de tilde, tiene que conocer el directorio home del usuario actual, lo que complica la implementación, y puede terminar con implementaciones incompletas en las que solo funciona ~ pero no ~name
      En un shell POSIX, ~name se expande al directorio home de ese usuario, así que hay que consultar el directorio home de usuarios arbitrarios, incluidos los usuarios del sistema. Dependiendo de la configuración de NSS, eso puede ser mucho más complejo que leer /etc/passwd
      En una TUI interactiva puede ser útil implementar la expansión de tilde, pero en ese caso debería ser tan completa como la del shell. Es mejor que los programas CLI no hagan este tipo de expansiones mágicas
    • Si vas a poner esto en tu propio REPL, de verdad tienes que tomarte en serio la documentación, el escape y el manejo de comillas
      Surgen problemas como qué hacer si existe un archivo llamado "~", qué hacer con "~username", cómo escaparlo o citarlo, qué pasa si $HOME no existe o no coincide con el home real, y qué hacer en Windows
      Un dato menos conocido: en ssh, si escribes "~." después de un salto de línea, el cliente cierra la conexión a la fuerza y el servidor no puede hacer nada, así que conviene no incorporarlo al flujo de trabajo
    • Un caso en el que este problema siempre me molesta es algo como ./myprogram --config=~/.config/myprogram
      Claro que muchos parsers de flags permiten usar la forma que el shell sí expande, ./myprogram --config ~/.config/myprogram, así que lo saben o la soportan por casualidad
      Pero por memoria muscular siempre termino escribiendo =. Creo que es una costumbre que me quedó de una biblioteca de parsing de flags en un trabajo anterior, y por la misma razón también uso siempre --underscores_like_this en lugar de --hyphens-like-normal-people
    • Buen punto
      En especial, como .. se maneja a nivel de la API del SO, es fácil esperar que ~ también funcione así en POSIX
  • La razón principal por la que prefiero tanto la CLI frente a la GUI, y a veces frente a la TUI, es que se siente mucho más consistente
    Hay convenciones, y seguir todas las convenciones en una CLI es mucho más fácil que diseñar una buena GUI. Por eso, el resultado tiende a ser de mayor calidad
    Pienso mucho en cómo llevar esa propiedad a la GUI, pero hasta ahora las mejores respuestas son algo como “mucho esfuerzo” o “bajar las expectativas”

    • Creo que una gran razón es que, salvo en macOS, las convenciones de GUI no están fuertemente implementadas en el SO ni son fáciles de adoptar
      Los otros sistemas operativos no lograron consolidar una tecla meta para atajos de GUI, así que hay muchos más conflictos como ctrl-c
      Los valores predeterminados del framework de aplicaciones sobre el que se basan las apps nativas de macOS ofrecen muchas operaciones comunes de teclado. Si usas la clase estándar de barra de menú y los comandos estándar, Command O, N, Q, X, C, V, etc. vienen casi gratis; y si usas campos de texto estándar, también obtienes automáticamente navegación con Command/Option/Fn + flechas
      Windows también es bastante bueno en este aspecto, pero que la tecla modificadora predeterminada sea Ctrl puede dificultar el uso de apps basadas en terminal, y varios frameworks de UI internos del SO parecen tener valores predeterminados distintos
      En el mundo Linux, parece que haría falta que alguien diseñara un framework de aplicaciones completo, que las distribuciones lo estandarizaran y que las apps se portaran a él. El gestor de ventanas no quiere dictar el comportamiento dentro de una ventana; los toolkits de GUI no quieren definir valores predeterminados para todo el SO; y los entornos de escritorio y las distribuciones tampoco quieren encargarse de imponer un framework de UI ni de portar aplicaciones
  • Los programas no deberían agregar archivos al directorio home, y deberían respetar configuraciones como XDG_CONFIG_HOME

    • De acuerdo, pero este artículo, como el autor intenta dejar claro, es más descriptivo que prescriptivo
      No intenta convencer sobre cómo deberían comportarse las aplicaciones, sino enumerar comportamientos que se ven comúnmente en ellas
  • Una excepción a la regla de que “Ctrl-W debe borrar la última palabra” es mysql(1).
    mysql está enlazado con editline en vez de readline, así que, por defecto, Ctrl-W no borra la última palabra, sino todo hasta el inicio de la línea.
    Durante un tiempo tuve que usarlo, y de verdad me volvía loco que desaparecieran consultas enteras que había escrito con tanto cuidado.

    • Podría ser peor.
      En la universidad tuve la mala suerte de probar sqlplus para conectarme a una base de datos Oracle, y no solo no funcionaban atajos de terminal como Ctrl-W, sino que tampoco podía mover el cursor hacia atrás dentro de la línea ni había navegación por el historial para ir a comandos anteriores.
      Con solo cometer un error de tipeo tenía que volver a escribir todo desde el principio, y si ingresaba un solo / podía ejecutar tal cual el último comando, pero eso no ayudaba a corregir errores.
      Tampoco se podía limpiar la pantalla con Ctrl-L; en su lugar había que ejecutar clear scr manualmente. Supongo que usar simplemente clear como comando era demasiado obvio.
  • Quisiera agregar que los procesos de larga ejecución deberían volver a leer la configuración cuando reciben SIGHUP.

    • Esta no es una lista de “cosas que deberían hacerse”, sino de “cosas que generalmente son así”.
      La diferencia es grande y, lamentablemente, este punto no es lo bastante universal como para entrar en la lista original.
    • En sentido estricto, es muy probable que esto quede fuera del alcance de este artículo.
      Los procesos de larga ejecución que reaccionan a SIGHUP normalmente se ejecutan desacoplados y sin una TTY de control, así que llamarlos “programas de terminal” resulta algo forzado.
    • ¿No es difícil implementar este comportamiento en la práctica?
      Hay que tener cuidado de que la aplicación no almacene accidentalmente en caché no solo el estado de la configuración, sino también el estado derivado de esa configuración; de lo contrario, quedan desincronizados.
      En una app simple puede ser fácil, pero la mayoría de las apps que necesitan un archivo de configuración no son tan simples.