Dominando la creación de archivos con "Here-Documents" (here-docs) en BASH


Dominando la creación de archivos con "Here-Docs" en BASH.

En el desarrollo de scripts, a menudo necesitaremos crear archivos de configuración XML, JSON, o bien scripts auxiliares que contendrán múltiples líneas de texto. Lo básico es escribir un "echo" tras otro, completando toda la secuencia.
Pero existe un método que creo que es mucho más elegante y bonito, el cual consiste en utilizar un "Here-Document" (o "Here-Doc").

Automatizando la configuración: el poder de "sed".

Imaginemos que necesitamos insertar una configuración específica en el archivo de acciones de Thunar (uca.xml), y que en lugar de escribir el archivo manualmente, utilizamos este bloque de código basado en el comando sed:

sed -i "/<\/actions>/i\
<action>\\
    <icon>terminal</icon>\\
    <name>ThunExec</name>\\
    <command>$terminal_predef</command>\\
    <description>Ejecuta en terminal preferida</description>\\
    <patterns>*.sh;*.bash;*.zsh;*.fish</patterns>\\
    <appears-on-files>true</appears-on-files>\\
</action>" "$thun_uca"


¿Qué hace este bloque?

Es un "inyector" de código. Busca la etiqueta de cierre </actions> dentro del archivo, y justo antes de ella (i), inserta todo el bloque <action>...</action> que configuramos. Es una forma quirúrgica de añadir funciones a un archivo de configuraciones, sin borrar lo que allí ya existía.

  • La primera "i" (sed -i...) es una opción del comando sed que significa "in-place", la cual le dice al comando: "no me muestres el resultado por pantalla, editá directamente el archivo original". Si no pusieses esta "i", sed solo imprimiría los cambios en la terminal y el archivo no se modificaría.

  • La segunda i (la del medio), es el comando de inserción "insert". En la sintaxis de sed, cuando ponés una barra "/busqueda/i", le estás diciendo al comando: "en la línea en donde encuentres esta búsqueda, insertá lo que siga a continuación, pero justo antes de esa línea".


El desafío: ¿cómo crear el archivo inicial?

Para que el comando "sed" anterior funcione, el archivo "$thun_uca" debe existir previamente y contener al menos las etiquetas básicas. Para lograr esto, tenemos dos caminos.

1. La "millonada de echos vomitantes". 

Es la opción que utilizan muchos programadores. Es funcional, pero un poco más difícil de mantener, además de ser menos elegante:

if [[ ! -f "$thun_uca" ]];then
    echo '<?xml version="1.0" encoding="UTF-8"?>' > "$thun_uca"
    echo '<actions>' >> "$thun_uca"
    echo '</actions>' >> "$thun_uca"
fi


Aquí solo hay 3 "echos", pero... ¿podés imaginar 30 o 40 de ellos?
El problema aquí es que cada línea nueva requiere un comando echo y una redirección, lo que convierte un bloque simple en una sucesión de instrucciones que  redundan sobre la misma estructura que se repite una y otra vez. Si usáramos el método tradicional, el script se volvería una tarea potencialmente problemática, ya que habría que copiar/pegar líneas "como quien lava y no escurre" o estar repitiendo código de manera enfermiza, como la máquina que NO somos.

2. La forma más elegante: los "Here-Doc"

Esta es la técnica de la que habla este artículo. Es la forma más limpia, porque nos permite definir el "molde" del archivo de una sola vez, manteniendo la estructura XML intacta y legible. Un "Here-Doc" es una forma de decirle a BASH: "tomá todo lo que se escriba a continuación como una única unidad de texto y envialo todo junto al destino". Algo así como el Hypertransport de AMD (enviar más, de un solo golpe) frente al Hyper-Threading de Intel (enviar menos, de a "golpecitos").

if [[ ! -f "$thun_uca" ]];then
cat > "$thun_uca" << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<actions>
</actions>
EOF
fi


Aquí, el bloque <actions></actions> actúa como el contenedor necesario. Una vez que este bloque crea el archivo base, el comando sed que vimos antes entrará en juego para insertar la acción "ThunExec" justo en medio de esas dos etiquetas.

¿Cómo interpretar este código?

cat > "$thun_uca": El comando cat toma lo que recibe y lo escribe en el archivo indicado por la variable $thun_uca. El símbolo > asegura que, si el archivo existe, se sobrescriba.

<< 'EOF': acá ocurre la magia. Las llaves << inician el Here-Doc. La palabra EOF es simplemente un marcador que elegimos nosotros (podría ser cualquier palabra, por ejemplo "POCHO", pero EOF —End Of File— es el estándar).

Importante: al envolver EOF en comillas simples ('EOF'), le estamos dando una instrucción vital al shell: "no interpretes nada de lo que hay adentro, sé literal". Esto evita que BASH busque variables con $ para expandirlas o intente ejecutar comandos dentro del texto, en lugar de tratar a todo como texto plano.

El final del bloque: el EOF que aparece al final, solo y sin sangrías, le avisa a BASH: "hasta acá llegó el archivo".

Reglas de oro para estudiantes.

El delimitador final debe estar solo: nunca pongas espacios o tabulaciones antes del EOF de cierre, porque si lo hacés, BASH no reconocerá el fin del bloque y el script se quedará "colgado" esperando el cierre.

Literalidad: gracias a las comillas simples ('EOF'), podés copiar y pegar cualquier fragmento de código (HTML, XML, JSON, Python) dentro del bloque sin miedo a que los caracteres especiales (como $ o ") rompan el flujo de tu script.

Orden: esta técnica es la forma más "limpia" de gestionar configuraciones. Mantiene el contenido del archivo de destino visualmente idéntico a cómo debería quedar en el medio de almacenamiento, facilitando mucho la edición futura.

Nota importante sobre la sangría (indentación, tabulación) dentro del "Here-Doc".

Indentar, tabular, sangrar el texto al programar es "mandatory!", al igual que comentar lo no obvio.
Es posible que al escribir tu código dentro del Here-Doc quieras usar tabulaciones o espacios para que el XML se vea ordenado. Podés hacerlo sin problemas, pero con una advertencia: el delimitador de cierre (el EOF, el POCHO) debe estar pegado contra el margen izquierdo de la pantalla. Si ponés espacios antes del EOF final, BASH pensará que esos espacios son parte del contenido que debe buscar, no los encontrará, y tu script se quedará "colgado" esperando el cierre.

El truco avanzado: si necesitás que el código dentro del bloque también tenga sangría visual y que el EOF final pueda estar alineado con ella, podés usar <<-EOF (agregando un guión después de los signos "menor que"). Ese mismo guión será el que le indique a BASH que ignore todas las tabulaciones iniciales de cada línea.

Así es cómo quedaría el código, aplicando el truco del guión:

if [[ ! -f "$thun_uca" ]];then
    cat > "$thun_uca" <<- 'EOF'
        <?xml version="1.0" encoding="UTF-8"?>
        <actions>
        </actions>
    EOF
fi


Como podés ver, tanto el bloque interior como el EOF de cierre pueden estar perfectamente indentados, y BASH, gracias a ese guión, ignorará esas tabulaciones iniciales al procesar el archivo. Esto hace que el código quede en consonancia con los "case", "if", etc, que ya poseas en tu script.

NOTA EXTREMADAMENTE IMPORTANTE: para que esto funcione, la sangría dentro del bloque debe hacerse con tabulaciones reales ([Tab]) y no con espacios ([Space]), ya que <<- está diseñado específicamente para ignorar caracteres de tabulación, no "espacios puestos con la barra espaciadora", al inicio de cada línea incluida en el bloque.

De la estructura clásica a la potencia del "cortocircuito" en BASH


De la estructura clásica a la potencia del "cortocircuito" en BASH.


En BASH, existen muchas formas de tomar decisiones.
En este artículo, vamos a hacer un breve "viaje" desde la estructura más básica del "if" hasta una técnica avanzada y compacta que verás en los scripts que más experiencia acumulada encierran: el operador de "cortocircuito".

1. Punto de partida: el "if" básico.


Todo estudiante comienza por aquí. Es la forma más legible y segura de ejecutar algo si se necesita verificar el cumplimiento de una condición.

# Si el archivo existe, notificarlo.
if [[ -f "$archivo" ]];then
    echo "El archivo existe."
fi


2. Agregando alternativas: "else" y "elif".

En cuanto el script comienza a crecer, es necesario empezar a manejar el "qué pasa si no", y aquí es en donde entran en escena los actores "else" (si no) y "elif" (y si).

if [[ -f "$archivo" ]];then
    echo "Es un archivo regular."
elif [[ -d "$archivo" ]];then
    echo "Es un directorio."
else
    echo "No se encontró nada con ese nombre."
fi


3. El salto al minimalismo: operadores && y { }

A veces, podemos juzgar que un bloque "if" de 5 líneas para una tarea muy pequeña está ocupando "demasiado espacio visual", y es este tipo de situación el origen del código compacto, utilizando, entre otras herramientas, al operador "&&" (AND).

En BASH, el "&&" le dice al sistema: "ejecutá lo que sigue SOLO SI la operación anterior terminó con éxito".

Si solo querés ejecutar un comando, la cuestión es simple:

[[ -f "$archivo" ]] && echo "Existe."


¿Te vas dando cuenta de cómo funciona esto?

Ahora... ¿qué pasa si querés ejecutar varios comandos en una sola línea?
Para esto mismo es que están las llaves "{}", las cuales se pueden utilizar en combinación con "&&" (AND - "si esto es cierto, hacé lo siguiente") y "||" (OR - "si lo anterior falló, hacé esto").

[[ -f "$archivo" ]] && { echo "Es un archivo regular."; exit; }
[[ -d "$archivo" ]] && { echo "Es un directorio."; exit; }
echo "No se encontró nada con ese nombre."


Este código es equivalente al "if" completo (if/else/elif/fi) de más arriba
Son 3 líneas en comparación con 7: menos de la mitad,

Y podemos llevarlo al extremo "oneliner", que es una técnica que yo usaba mucho cuando programaba en BASIC, hace muchos años atrás:

[[ -f "$archivo" ]] && { echo "Es un archivo regular."; exit; };[[ -d "$archivo" ]] && { echo "Es un directorio."; exit; };echo "No se encontró nada con ese nombre."

Una sola línea. ¿Qué me contás?

Reglas de oro para usar las llaves.

  • Espacios obligatorios: siempre debe haber un espacio después de { y antes de }
  • El punto y coma final: el último comando dentro de las llaves debe terminar en punto y coma antes de cerrar la llave, para que BASH sepa que el bloque terminó. Esto no es como lo grotesco de Java, al que hay que estar avisándole en cada línea que la línea se terminó, poniendo SIEMPRE ";". Esto en BASH significa "se terminó la línea", pero NO ES OBLIGATORIO, y de hecho, se usa extremadamente poco.


¿Cuándo utilizar cada estilo?

Debes utilizar el "if/elif/else/fi" tradicional cuando se trate de una lógica compleja o estés escribiendo un script que otros (o vos mismo en el futuro) necesiten leer con total claridad y sin andar interpretando "código barroco".

Te recomiendo que, si vas a utilizar este método de "&&", "||" y llaves "{ ...; }", lo hagas ante una validación rápida y corta. Ahorra mucho espacio y, una vez que te acostumbrás a leerlo, hace que tu código se vea mucho más dinámico y fluido.

Simplificando tu código BASH: de bloques "if" a soluciones minimalistas.


Simplificando tu código BASH: de bloques "if" a soluciones minimalistas.


Cuando estamos aprendiendo a programar en "shell", tendemos a usar estructuras largas para tareas simples. 
Vamos a ver cómo transformar un bloque de código común en una solución que si hablamos de programas de código muy extenso, puede llegar a ser mucho más profesional, y también de cómo gestionar archivos de configuración de forma más "limpia".

El desafío de las variables vacías.

Imaginemos que nuestro script necesita saber el nombre del usuario. Si por alguna razón no logra saberlo, queremos que guarde un mensaje "por omisión". Normalmente, escribiríamos algo así, lo cual estaría muy bien:

if [[ -z "$usuario" ]];then
    usuario="Desconocido"
fi


Esto funcionará, pero BASH tiene un operador de expansión conocido como "parámetro por defecto" que hace exactamente lo mismo en una sola línea, llenando él mismo el contenido de la variable sin tener que dar tanta vuelta nosotros a nivel código, y sin pedirle a él que de tanta vuelta interna:

usuario="${usuario:-Desconocido}"


¿Cómo se interpreta esto?

Las llaves "${ }" le dicen a BASH que preste atención al estado de la variable.
El símbolo ":-" actúa como una especie de "plan B" en caso de que la variable "usuario", por algún motivo, esté vacía o no haya sido inicializada, y entonces poder utilizar el texto que sigue.
Si ya tiene un valor, no toca nada. Es ideal para configurar valores iniciales de manera automatizada, sin tener que "llenar" el script de condiciones o igualaciones.
Va otro ejemplo, para terminar de redondear el tema.

Supongamos que tu script permite elegir un color de fondo, pero que si el usuario no elige ninguno, el script tenga que utilizar uno obligatoriamente.

La forma más típica:

if [[ -z "$col_fondo" ]];then
    col_fondo="azul"
fi


La forma que estamos estudiando en este artículo:

col_fondo="${col_fondo:-azul}"

En este caso, el "plan B" asegura que el script siempre tenga un color para referenciarse y no fallar, sin importar si el usuario lo omitió deliberadamente o se olvidó de configurarlo con anterioridad a la ejecución de dicha línea.

El operador "!" en BASH: ejecutar comandos del historial sin "copieypegue"

El operador "!" en BASH: ejecutar comandos del historial sin tener que buscar/copiar/pegar.

Origen de este artículo.

Estaba trabajando en la terminal, ejecuté "history | grep /usr/" y obtuve una lista de comandos anteriores con sus IDs. Me pregunté por primera vez si era posible ejecutar el comando número "475" sin tener que copiarlo manualmente, reescribirlo o buscarlo con flechas arriba/abajo interminablemente (como hago casi siempre, por comodidad).

La solución está en el operador "!", pero su sintaxis no es para nada intuitiva: al contrario, confunde, porque históricamente significa "negación". Dicho sea de paso... ¿a quién se le ocurrió usar ese símbolo y no otro más apropiado? Rarezas del "si pasa, pasa", del mundo de la programación.

¿Qué es "!"?

"!", es el operador de expansión de historial en BASH. Permite referenciar y ejecutar comandos previos de múltiples formas. Viene heredado de shells antiguos (csh, tcsh) y su diseño prioriza brevedad sobre claridad.

Varios ejemplos para su utilización.

1. Ejecución por número de ID.

!475

esto ejecuta directamente el comando que está en la línea 475 del historial:

history | grep /usr/
430 sudo cp /home/entropia/conf/Commodore_64-Pantalla_BN.png /usr/share/backgrounds/
475 ls -l /usr/share/backgrounds/
499 history | grep /usr/


Al tipear "!475" [Enter], el resultado será la ejecución de "ls -l /usr/share/backgrounds/" inmediatamente.

2. Ejecución del "último comando".

Ejecutar "!!", repite el último comando ejecutado. Puede ser útil cuando olvidaste poner "sudo" delante de la orden, cosa que me pasa a menudo.
En este caso, estoy intentando ejecutar GParted, herramienta que necesida privilegios de root debido a las tareas que resuelve:

gparted
Root privileges are required for running gparted.
GParted 1.8.0
configuration --enable-libparted-dmraid
libparted 3.6
Se requieren privilegios de root para ejecutar GParted


Entonces, como olvidé poner "sudo", ahora hago esto, y mirá la diferencia:

[entropia@void-entropia ~]$ sudo !!
sudo gparted
Contraseña: 


El "sudo gparted" lo devolvió la terminal: no tuve que ponerlo yo.
Imaginate olvidar "sudo" en una línea sumamente extensa... el tiempo que ahorra es de importancia.

3. Ejecución del comando anterior que empezaba con "cierta palabra".

!ls

Ejecuta el comando más reciente que empezó con "ls".

!sudo

Ejecuta el comando más reciente que empezó con "sudo".

4. Previsualización previa a la ejecución.

!475:p

El modificador ":p" (print) muestra el comando sin ejecutarlo. Luego, aparece en el historial y podés presionar [↑][Enter] para ejecutarlo si es el correcto.

Ejemplo basado en mi caso.

  1. Tipeás "!475:p"...
  2. Se muestra "ls -l /usr/share/backgrounds/"
  3. Pulsás [↑]
  4. Aparece "ls -l /usr/share/backgrounds/"
  5. Presionás [Enter] para ejecutar esa orden.


5. Buscar por contenido (pero no solo "al inicio").

!?backgrounds

Ejecuta el comando más reciente que contenga a la palabra "backgrounds" en cualquier parte.

!?/usr/

ejecuta el comando más reciente que contenga "/usr/" en cualquier lugar de la orden.

6. referencia a argumentos del último comando.

Supongamos que "hace un rato" hemos hecho "ls /usr/share/backgrounds/" y ahora queremos copiar algo allí. Entonces, podemos hacer esto:

cp Imagen.png !$

¿Por qué?

Porque "!$", expande el último argumento del comando anterior.
El resultado, sería, entonces: "cp imagen.png /usr/share/backgrounds/"

7. "Bonus track".

Te dejo otros modificadores de argumentos (ya no tan explicados) para que busques más información si te interesó esto de BASH, lo cual espero que así haya sido.
No sigo detallando porque se haría interminable el artículo, y además, porque si esto que te expliqué antes ya era algo inusual, lo que te comentaré a continuación, lo será aún más.

!$   : último argumento del comando anterior.
!^   : primer argumento del comando anterior.
!*   : todos los argumentos del comando anterior.
!-2 : ejecutar el comando que está 2 posiciones atrás en el historial.
!-1 : equivale a "!!" (el comando inmediatamente anterior).

ls /usr/share/backgrounds/
# Como te equivocaste de directorio, entonces...
^backgrounds^fonts
# Ejecuta: ls /usr/share/fonts/

8. "Bonus subtrack."

¿Sabés que se puede editar el archivo ".bashrc" con finalidades bien interesantes?
Por ejemplo, para evitar duplicados en el historial:

HISTCONTROL=ignoredups:erasedups

Reflexión final.

El operador "!" no es intuitivo hoy, porque, entre otras cosas, viene de una época en donde la brevedad era crítica. Pero una vez que entiendas su lógica

!   + número : ejecutar por ID
!   + palabra : ejecutar por inicio
!? + palabra : ejecutar por contenido

comprenderás que es una herramienta poderosa para evitar copiar/pegar y trabajar más eficientemente en terminal y consola.

La IA y los casos de borde, o "el estrés de tener que pensar como una máquina para poder controlarla".

 
La IA y los casos de borde, o "el estrés de tener que pensar como una máquina para poder controlarla".

Hay una experiencia que quienes somos programadores y a vez también usuarios avanzados de IA conocemos bien, pero de la que casi nadie habla: el "debug infinito desde el primer prompt".
No me refiero a errores obvios: me refiero a lo que pasa cuando trabajás "en el borde": entornos "poco mainstream", tecnologías muy maduras pero no prolíficas, configuraciones que existen desde hace décadas, que tienen documentación sólida, pero que no son "el caso de uso que entrenó al modelo".
Un ejemplo concreto: pedirle a un LLM ayuda para configurar un firewall en Windows, es una solicitud que seguramente posea una tasa de error cercana a cero. Pedirle que escriba un "frontend" básico para "nftables" o peor aún, para "iptables" bajo Runit, en distribuciones Linux "raras" como Crux, Void, Artix o Alpine, ya es otra historia: no porque sea intrínseca y necesariamente más difícil, sino porque esas distribuciones son "de nicho", deliberadamente minimalistas, con sistemas de init alternativos a "systemd", mantenidas por comunidades y no por corporaciones. Existe excelente documentación sobre éstas, pero la misma está dispersa en Internet, y los LLMs aprenden de lo que abunda, no de lo que existe.
Y es entonces cuando la IA no nos dirá que no sabe.
La IA generará código con confianza.
Y lo que generará parecerá correcto, aunque sin serlo.
Y cuando falle, el siguiente movimiento automático e irreflexivo del modelo será sospechar de tu entorno, de tu "configuración particular", de la reinstalación de un determinado paquete, pero nunca de sí mismo, hasta que se lo hagas ver y le pidas que se enfoque, mientras se van malgastando los "tokens" y se va diluyendo el contexto inicial: la charla se irá tornando arborescente y jamás volverá a bajar a la raíz, lugar desde el cual nunca debió haberse desviado.
El resultado es un tipo de estrés muy específico: llegás a cada sesión ya en modo escéptico, controlador, en "modo militar" y listo para discutir y ver todo en blanco y negro.
Empezás a pensar como una máquina para poder controlar a la máquina, no como un ser humano que viene de manera confiada a utilizar a su asistente digital.
No se trata de colaboración ni de ayuda "limpia": ya es supervisión constante.
Y esto tiene un costo real en tiempo, en salud anímica, en "cabeza".
Lo que me llama la atención es la diferencia de percepción entre quienes operamos así y la mayoría de los usuarios, que tienen experiencias mucho más fluidas porque sus casos de uso están en el centro del entrenamiento, no en el borde. Para ellos la IA "funciona genial." Y tienen razón; pero esa experiencia mayoritaria termina definiendo el relato público sobre qué tan confiable es la herramienta, cuando los casos de borde que manejamos otros dicen exacta, medible, justificada y demostrablemente lo contrario.
Los que vivimos en el borde sabemos otra cosa. Y generalmente, sabemos bastante más también.
¿También vos operás en ese espacio?
¿Cómo lo gestionás?

Publicado en el espacio personal de LinkedIn de Hugo Napoli:
Ver publicación
Flag Counter Visitas previas a la existencia de este contador: 3433

Artículos aleatorios

    Páginas: