====== 01 - Scripts con bash ====== La terminal repite trabajo: teclear de nuevo la misma secuencia de comandos es lento y propenso a erratas. Un **script** guarda esa secuencia en un fichero y la ejecuta en orden con un solo comando: el mismo resultado cada vez, y el fichero documenta cómo se consigue. **Bash** es la **shell** habitual de los sistemas Unix: el intérprete que lee y ejecuta comandos línea a línea. Un script de bash es eso mismo —una secuencia de comandos— guardada en un fichero. ===== 1. De comandos a script ===== La primera línea de un script es el **shebang**: indica con qué intérprete se ejecuta. #!/usr/bin/env bash Se guarda con extensión ''.sh'' y necesita permiso de ejecución: $ chmod +x saludo.sh # solo la primera vez $ ./saludo.sh Los comentarios (''#'') no se ejecutan: explican el qué y el porqué. Los scripts se leen tanto como se escriben. Cualquier secuencia de comandos cabe en un script. Un ejemplo cotidiano: automatizar compilar y ejecutar un programa: #!/usr/bin/env bash echo "Compilando..." javac -d bin src/Main.java echo "Ejecutando..." java -cp bin Main * ''javac -d bin'' compila a la carpeta ''bin''. * ''java -cp bin Main'' ejecuta la clase con ''bin'' en el *classpath*. * ''echo'' escribe en la salida estándar: los mensajes cuentan lo que el script hace. ===== 2. Variables ===== Un script suele tener valores que cambian: se guardan en **variables** y se asignan una vez, al principio. FUENTE="datos" * La asignación no lleva espacios alrededor del ''='': no es una comparación, es una asignación. * Para usarla, ''$FUENTE'', y siempre entre **comillas dobles**: así un valor con espacios no se rompe. * Por convención, en **MAYÚSCULAS**: distingue la configuración de las demás líneas. La **sustitución de comandos**, ''$( )'', mete en una variable la salida de un comando: la shell lo ejecuta cada vez que corre el script. HOY=$(date +%F) DESTINO="copias/copia-$HOY.tgz" echo "La copia de hoy irá a $DESTINO" ===== 3. Tuberías y redirecciones ===== La shell **conecta comandos**: la salida de uno puede alimentar al siguiente o terminar en un fichero. ^ Sintaxis ^ Efecto ^ | ''comando > fichero'' | escribe la salida en el fichero; si existe, lo sobreescribe | | ''comando >> fichero'' | la añade al final del fichero | | ''comando 2> fichero'' | escribe ahí la salida de **error** | | ''comando < fichero'' | lee la entrada del fichero | | ''comando1 \| comando2'' | **tubería**: la salida de uno es la entrada del otro | $ ls -l > listado.txt $ find . -name "*.bak" 2> errores.txt # los avisos de find, en un fichero $ history | grep docker $ ps aux | grep java * Los errores van por otra vía, el **error estándar**: por eso el ''2>''. Un fallo silencioso en la salida normal pasaría desapercibido. * Cada herramienta hace una cosa y la hace bien —''grep'' busca, ''wc'' cuenta, ''sort'' ordena—; la tubería las compone. ===== 4. Errores y códigos de salida ===== Cada comando termina con un **código de salida**: ''0'' si tuvo éxito, distinto de ''0'' si falló. En la terminal no se ve, pero es el contrato con el que comandos y scripts se comunican. $ cat noexiste.txt cat: noexiste.txt: No such file or directory $ echo $? # el código del último comando 1 * ''$?'': el código de salida del último comando. * ''exit n'': termina el script con ese código. ''set -e'' al principio del script detiene la ejecución en cuanto un comando falla. Fallar pronto: nada de continuar sobre un estado a medias. #!/usr/bin/env bash set -e ===== 5. Condicionales y bucles ===== ''if'' ejecuta un bloque si la condición se cumple. Los corchetes son un comando de test: necesitan espacios alrededor y las variables entre comillas. if [ -f "$FUENTE" ]; then tar -czf copia.tgz "$FUENTE" else echo "No existe $FUENTE" exit 1 fi ^ Test ^ Comprueba ^ | ''-f'' | que existe y es fichero | | ''-d'' | que existe y es directorio | | ''-z'' | que la cadena está vacía | | ''='' | que dos cadenas son iguales | | ''-eq'' | que dos números son iguales | ''for'' recorre una lista; con la sustitución de comandos, la lista puede ser la salida de ''find'': for f in $(find . -name "*.txt"); do echo "Texto: $f" done ''while'' repite mientras la condición tenga éxito; con ''read'', recorre líneas de un fichero: while read -r linea; do echo "$linea" done < ficheros.txt ===== 6. Funciones ===== Una **función** agrupa una tarea bajo un nombre: se define una vez y el script la llama cuando la necesita. copia() { echo "▶ Copiando..." tar -czf "$DESTINO" "$FUENTE" } limpia() { echo "▶ Limpiando..." rm -f "$FUENTE"/*.tmp } * Una tarea, una función: el cuerpo del script se lee como la lista de tareas que hace. * El nombre elegido dice la tarea —''copia'', ''limpia''—, no el comando que la resuelve. ===== 7. Subcomandos con case ===== El script admite un argumento que elige la tarea: ''$1'' es el primero (''$0'', el propio nombre del script). El ''case'' compara el argumento con cada caso y ejecuta el bloque correspondiente: case "$1" in copia) copia ;; limpia) limpia ;; *) echo "Uso: $0 {copia|limpia}" exit 1 ;; esac * '';;'' cierra cada caso. * ''*)'' es el resto: sin argumento o con uno erróneo, el script enseña su uso y termina con error. * Es el patrón de cualquier herramienta de línea de comandos: ''git commit'', ''docker compose up''... Un comando, una tarea. @startuml start :./tareas.sh "$1"; switch ($1) case (copia) :copia; case (limpia) :limpia; case (*) :uso; :exit 1; endswitch stop @enduml ===== 8. Componerlo todo ===== Todo junto, el esqueleto de un script con varias tareas: #!/usr/bin/env bash set -e # Configuración FUENTE="datos" HOY=$(date +%F) DESTINO="copias/copia-$HOY.tgz" # Tareas copia() { echo "▶ Copiando..." tar -czf "$DESTINO" "$FUENTE" } limpia() { echo "▶ Limpiando..." rm -f "$FUENTE"/*.tmp } # Despacho case "$1" in copia) copia ;; limpia) limpia ;; *) echo "Uso: $0 {copia|limpia}" exit 1 ;; esac El esqueleto es siempre el mismo: configuración, tareas, despacho. Cambiar de tarea solo toca las funciones y el ''case''; la forma del script no cambia.