Una Máquina a la que Había que Explicárselo Todo
Hoy, en cualquier editor, pulsar Intro simplemente funciona: el cursor baja a una línea nueva sin que haga falta nada más. Esa comodidad se apoya sobre una máquina bastante más tosca y bastante más antigua. Los 32 códigos más bajos de ASCII, del 0 al 31, no se pensaron para editores de texto: se diseñaron para teletipos, máquinas de escribir electromecánicas que recibían un flujo de bits por un cable y a las que había que indicarles, de forma explícita, cada movimiento físico que debían ejecutar. No existía pantalla, ni cursor, ni la idea de que "la siguiente línea" apareciera sola. Si querías mover el cabezal de impresión, mandabas un código que significara exactamente eso. Por eso un carácter de control no imprime ningún glifo: no es texto, es una orden dirigida a un aparato mecánico que ya no está en casi ningún puesto de trabajo, enviada por un canal que todavía sigue en pie.
Nuestro artículo hermano sobre los planos de Unicode, la normalización y el orden alfabético sigue al estándar más allá de esas 128 posiciones, hasta el espacio de códigos que salió de ellas. Este se queda dentro de los 32 primeros valores y añade un puñado de polizones más recientes de Unicode, y hace una pregunta más concreta: para qué se creó cada una de estas órdenes, y en qué rincones del software actual sigue asomando esa maquinaria antigua. Puedes comprobar cualquiera de los códigos de abajo pegándolos en nuestra herramienta de conversión ASCII y Unicode, que te da el valor numérico exacto detrás de un carácter pegado en vez de dejarte adivinarlo por cómo se ve.
Dos Movimientos Distintos Disfrazados de una Sola Tecla
El carro de una máquina de escribir hace dos trabajos mecánicos bien distintos cuando terminas una línea: desliza el papel hacia la izquierda hasta el margen, y gira el rodillo para que el siguiente golpe caiga una fila más abajo. En el papel, eso se lee como un solo gesto. En un teletipo eran dos órdenes separadas, porque los dos motores que hacían el trabajo eran distintos y ninguno implicaba al otro. El código 13, retorno de carro (CR), significaba "lleva el cabezal de vuelta a la columna uno": un movimiento horizontal que cambia dónde estás dentro de la fila actual sin tocar en absoluto la fila. El código 10, salto de línea (LF), significaba "avanza el papel una fila": un movimiento vertical que por sí solo nunca toca la posición horizontal. Un terminal que solo recibiera un LF imprimiría el siguiente carácter justo debajo de donde había quedado el anterior, dibujando el texto en escalera hacia abajo de la página, porque nadie le había dicho que volviera al margen.
No es un caso hipotético: los teletipos necesitaban de verdad los dos códigos juntos para reproducir lo que la palanca de retorno de una máquina de escribir mecánica hacía con un solo gesto de la mano. El software escrito para esas máquinas se limitaba a reenviar el par tal cual, y cuando los sistemas operativos empezaron a guardar texto en archivos en vez de mover un rodillo físico, esa pareja de códigos se quedó como convención, ya sin ninguna necesidad mecánica que la sostuviera.
Por Qué Tres Sistemas Acabaron con Tres Respuestas Distintas
En cuanto el final de línea pasó del hardware a los archivos guardados, cada familia de sistemas operativos se quedó con un pedazo distinto de aquella costumbre del teletipo. Windows, que hereda de MS-DOS y este a su vez de CP/M, conservó los dos códigos juntos como CRLF (los bytes 13 y luego 10) — el mismo par, sin tocar, de la época de los teletipos. Unix, nacido en un entorno de investigación que quería la representación más pequeña posible, eliminó directamente el CR y dejó que un LF suelto significara el final de línea entero, confiando en que el controlador del terminal añadiera por su cuenta el retorno visual al mostrarlo, sin guardarlo en el archivo. El Mac clásico, antes de Mac OS X, tomó el tercer camino y conservó solo el CR, descartando el LF — una decisión que no tenía nada que ver con las otras dos y todo que ver con cómo estaban construidos sus propios controladores de terminal. Cuando Apple reconstruyó Mac OS X sobre una base Unix, adoptó la convención de Unix, y los archivos con solo CR pasaron a ser, en la práctica, una curiosidad de archivos de hace décadas.
La consecuencia práctica de tener tres respuestas para una sola pregunta se nota en cuanto un archivo cruza de un sistema a otro sin traducirse. Un script editado en Windows y movido a un servidor de compilación Unix puede fallar con un error que señala justo la primera línea, porque tras el shebang #!/bin/bash queda un retorno de carro invisible que Unix interpreta como parte del propio nombre del comando, así que bash se convierte en un programa inexistente llamado bash^M. Una herramienta de diff que compare un archivo editado en Windows contra su original de Unix puede marcar como cambiada cada línea del documento, aunque el texto visible sea idéntico, porque los bytes del final de línea son distintos. La opción core.autocrlf de Git existe precisamente para traducir entre ambas convenciones al hacer checkout y al confirmar los cambios, siempre en local, y así un repositorio compartido no termine acumulando una mezcla de las dos.
TAB: Una Orden para Saltar, No un Número Fijo de Espacios
El código 9 es la tabulación horizontal, TAB. Igual que CR y LF, empezó siendo una instrucción para una máquina física: las máquinas de escribir con topes de tabulación dejaban al operador fijar columnas de antemano y, con una sola palanca, saltar el carro directo hasta la siguiente, en vez de golpear la barra espaciadora repetidas veces con la esperanza de que la cuenta saliera exacta. Ahí nace un detalle que confunde a quien piensa en la sangría como "un número de espacios": un tabulador es una sola instrucción, un solo byte, un solo punto de código, sin importar cuántas columnas ocupe finalmente en la pantalla. Cuánto salta nunca lo decide el propio carácter — depende de dónde estén fijados los topes de tabulación, ya sea en el terminal, en el editor o en la impresora que lo interprete.
Por eso la discusión entre tabuladores y espacios sigue viva entre programadores en lugar de estar zanjada hace tiempo: un archivo sangrado con tabuladores reales se muestra al ancho que cada visor tenga configurado para sus topes de tabulación, así que el mismo código fuente puede verse perfectamente alineado en un editor y visiblemente torcido en otro, mientras que un archivo sangrado con espacios literales se ve idéntico en todas partes, porque un espacio siempre ocupa exactamente una columna, en cualquier visor, sin depender de ninguna configuración. Mezclar ambos dentro del mismo bloque de código es el fallo clásico: una línea que parece alineada en el editor donde se escribió puede aparecer desplazada varias columnas en el editor de quien la revisa, con un ancho de tabulación distinto, solo porque dos reglas de representación distintas conviven en el mismo archivo sin que nadie se diera cuenta.
NUL: El Byte que Significa "Deja de Leer"
El código 0, NUL, no tiene forma impresa y es anterior incluso a la idea de "cadena vacía" en la mayoría de los contextos de programación — es un byte cuyo único trabajo es señalar ausencia o final, nunca representar un símbolo. Su papel más importante hoy vive dentro del lenguaje C, donde una cadena de texto no se guarda con un campo de longitud aparte, como sí ocurre en muchos lenguajes de más alto nivel; en cambio, la cadena es simplemente una tira de bytes en memoria, y la convención que le dice a cualquier función dónde acaba esa tira es un único byte NUL colocado justo después del último carácter real. Una función como strlen no sabe de antemano cuánto mide una cadena: empieza por el principio y va leyendo byte a byte, contando, hasta toparse con ese NUL y detenerse ahí.
Esa convención es eficiente, y también es la antecesora directa de toda una familia de fallos de seguridad. Si alguien consigue colar un byte NUL en medio de un dato que el programa esperaba que fuera un nombre de archivo o una ruta limpia, algunas capas del sistema seguirán leyendo después de ese byte esperando más caracteres, mientras que otras capas se detendrán en seco justo ahí y tratarán todo lo que viene después como si no existiera — las dos mitades del sistema en desacuerdo sobre dónde termina realmente la cadena. Ese desajuste, a veces llamado inyección de byte NUL, se ha usado para conseguir que una comprobación de validación mire una cadena mientras el código que finalmente abre el archivo mira una versión más corta y truncada de esos mismos bytes. La solución no pasa por un análisis más ingenioso, sino por rechazar de raíz cualquier byte NUL incrustado en un campo que no debería llevarlo, porque ni un nombre de archivo legítimo ni una cadena escrita por un usuario tienen ningún motivo honesto para contener uno.

Los Caracteres Invisibles que No Son Códigos de Control en Absoluto
Todo lo anterior vive dentro del rango C0, de U+0000 a U+001F, más el DEL en U+007F. Merece la pena marcar bien esa frontera, porque existe una segunda familia de problemas, sin relación con la primera, que en conversaciones informales se mete en el mismo saco de "caracteres de control" y en realidad queda fuera de ese rango por completo — son caracteres Unicode corrientes, clasificados formalmente como espacios o marcas de formato, que simplemente se muestran como nada o como algo indistinguible de un espacio normal. Confundir ambos grupos lleva a la solución equivocada: no se pueden eliminar de la misma forma que se elimina un CR suelto, porque cualquier herramienta que filtre "caracteres de control" comprobando solo el rango C0 dejará pasar cada uno de ellos sin tocarlos.
- U+00A0 NO-BREAK SPACE (espacio de no separación). Se ve exactamente igual que un espacio normal, pero le indica explícitamente al algoritmo de ajuste de línea que no debe partir la línea justo ahí — el comportamiento que quieres entre un número y su unidad, para que "10 km" nunca quede partido en dos renglones. Se convierte en un fallo en cuanto aparece donde no debía, casi siempre pegado desde un procesador de textos o una página web que lo usaba para maquetar espacios: una búsqueda de un espacio normal no lo encuentra, un analizador de CSV que separa campos por espacios puede tratarlo como delimitador o no según la biblioteca que use, y una búsqueda en diccionario o una comparación exacta contra lo que escribió un usuario falla en silencio, porque "café" escrito con un espacio normal y "café" pegado con un espacio de no separación son, byte a byte, dos cadenas distintas que se imprimen de forma idéntica.
- U+200B ZERO WIDTH SPACE (espacio de ancho cero). Se muestra como literalmente nada — sin ancho, sin glifo, invisible en cualquier tipografía normal. Existe para marcar un punto de corte de palabra válido dentro de una tira larga sin espacios, como una URL o una palabra compuesta en una escritura que no separa palabras con espacios, de modo que un navegador sepa dónde puede partir la línea sin insertar ninguna marca visible. Suelto donde no toca, es la pesadilla de cualquier herramienta de diff: dos líneas de código fuente que se ven pixel por pixel idénticas en cualquier editor pueden diferir por un espacio de ancho cero invisible, así que una revisión de código no muestra ningún cambio visible mientras la herramienta de diff insiste en que la línea se editó, y un script de compilación que compara el contenido de un archivo para generar una clave de caché obtiene un hash distinto para lo que en pantalla parece exactamente el mismo archivo.
- U+FEFF, leído como marca de orden de bytes. Este único punto de código cumple dos funciones distintas según el contexto. Al principio de un archivo, una secuencia concreta de bytes decodificada de cierta forma indica qué orden de bytes usa un archivo UTF-16, o simplemente marca por convención que el archivo es UTF-8 — eso es la marca de orden de bytes, o BOM, y algunos editores y herramientas de exportación la añaden de forma automática. Leída por un analizador que no la espera, esa secuencia inicial aparece como un puñado de bytes de más, invisibles, pegados justo al principio de la primera línea del archivo. Un analizador de JSON al que se le pasa un archivo que empieza con un BOM seguido de
{puede rechazar el documento entero por mal formado, porque el primer carácter que ve no es la llave de apertura que esperaba — son tres bytes que no reconoce como sintaxis JSON válida. Un script de shell con un BOM al principio puede fallar la comprobación del shebang de la misma manera que lo haría un retorno de carro suelto, porque la búsqueda del intérprete no empieza donde parece empezar el primer carácter visible del script.
Cómo Encontrarlos: Lee el Código, No Confíes en Cómo se Ve
Ninguno de los caracteres anteriores se anuncia visualmente — precisamente por eso causan problemas. La manera fiable de confirmarlo repite el mismo principio que sirve para los desajustes de codificación: hay que apartar la vista del texto renderizado e ir directamente a los números que hay debajo. En la línea de comandos, od -An -tx1 mi_archivo.txt | head -20 o xxd mi_archivo.txt | head -20 vuelca los bytes en bruto del archivo en hexadecimal, sin importar cómo decida mostrarlos un editor concreto, y una tanda de pares 0d 0a donde esperabas un simple 0a te avisa al instante de que se colaron finales de línea CRLF en un archivo que una herramienta Unix espera ver solo con LF. Grep tiene un truco más discreto para lo mismo: grep -U -c $'\r' nombre_de_archivo cuenta las líneas que contienen un retorno de carro literal sin dejar que el terminal lo reinterprete antes.
Para caracteres más allá, dentro de Unicode — un espacio de no separación, un espacio de ancho cero, un BOM colado — el volcado en bytes sigue funcionando pero se vuelve más difícil de leer a simple vista en cuanto el archivo tiene algo de texto fuera del rango ASCII, porque esos caracteres ocupan dos o tres bytes en UTF-8 en lugar de uno. Pegar el texto sospechoso directamente en nuestra herramienta de conversión ASCII y Unicode se salta ese paso: informa del punto de código exacto detrás de cada carácter que pegues, así que un espacio que resulta ser U+00A0 en vez del U+0020 normal se ve al instante, sin tener que descifrar hexadecimal a mano. Vale la pena aplicar esa misma costumbre a cualquier carácter que parezca del todo corriente pero se comporte de forma rara: una búsqueda que no encuentra nada, un diff que no se calla, un analizador que rechaza una entrada que a simple vista parece correcta. Si en cambio el problema es que una palabra entera se convierte en otras letras distintas, en lugar de que aparezca un hueco invisible donde no debería haber ninguno, ese es un fallo distinto con su propio mecanismo — el análisis completo está en por qué tu ñ se convierte en ñ y cómo arreglarlo.
Una Nota Sobre el Rango C1, y una Referencia Rápida
Unicode reserva un segundo bloque de códigos de control, mucho menos visto, entre U+0080 y U+009F, llamado los controles C1. La propia Unicode Character Database deja ese bloque sin nombre formal; los nombres que suelen aparecer junto a cada código C1 en la mayoría de las referencias — cosas como NEXT LINE o PARTIAL LINE FORWARD — vienen de la ISO 6429 (equivalente a la ECMA-48), no de la autoridad de nomenclatura propia de Unicode. En la práctica, este rango casi nunca aparece en un archivo de texto normal; es mucho más probable que sean bytes mal interpretados de otra codificación que una secuencia de control puesta a propósito, algo que conviene tener presente si un volcado en hexadecimal saca a la luz un valor de ese rango sin que nadie lo esperara.
- NUL (0) — marca el final de una cadena en C; conviene eliminarlo si aparece en campos que no deberían llevarlo.
- TAB (9) — un solo carácter; su ancho depende de los topes de tabulación configurados en cada visor, no del propio byte.
- LF (10) — el final de línea completo en Unix y en los archivos modernos de Mac OS X.
- CR (13) — el final de línea completo en los archivos del Mac clásico anterior a OS X; va emparejado con LF como CRLF en Windows.
- ESC (27) — introduce una secuencia de escape más larga en los códigos de control de terminal, no es una orden completa por sí solo.
- DEL (127) — queda fuera del bloque C0 por numeración, pero se agrupa con él por comportamiento: sin glifo, y con sus siete bits puestos a uno sin excepción — perforar las siete posiciones en la cinta de papel era, en aquella época, la única manera de anular un error.
- U+00A0, U+200B, U+FEFF — caracteres Unicode corrientes, no controles C0, que se muestran como nada o como un espacio normal y rompen diffs, analizadores y búsquedas precisamente porque parece que no ha pasado nada.
Cada uno de estos es un byte, o dos, que se puede rastrear a propósito en cuanto dejas de fiarte de cómo se ve el texto en pantalla y empiezas a leer lo que realmente contiene.