Skip to content

~/bda/m00 $ cat 03-bash-scripting.md

  • Big Data Aplicado
  • M00
  • lección 03/05

Bash scripting

De comandos repetidos a scripts comprobables.

Un script convierte una secuencia de órdenes en un procedimiento repetible. Este bloque ocupa dos sesiones y avanza desde decisiones y repeticiones sencillas hasta funciones, archivos, registros y revisión estática. Trabaja en el mismo contenedor bda-m00-debian y desde /bda-m00, la carpeta local compartida preparada en 01. Los ejemplos elementales no necesitan cotizaciones: primero entenderemos qué hace Bash y después aplicaremos esas estructuras al script que continuará en 05.

Los ejemplos sin shebang pueden probarse en la terminal de Bash; cuando se indique un nombre de script, guarda su contenido completo con Nano. No pegues fragmentos que contengan exit directamente en tu sesión interactiva: terminarías esa shell. Conserva los archivos de las páginas anteriores y no sustituyas el capturador final de 05 si estás repasando después de terminar el proyecto.

  • Crear y ejecutar scripts Bash con shebang y permisos adecuados.
  • Recibir argumentos, usar variables y proteger valores con comillas.
  • Usar condiciones, bucles y funciones cuando aporten claridad.
  • Comunicar éxito o fallo con códigos de salida y logs.
  • Revisar un script con ShellCheck antes de entregarlo.

Recupera scripts/saludar.sh, creado y autorizado para ejecución en la página 01. No hace falta volver a escribirlo ni concederle el mismo permiso:

Terminal window
cat scripts/saludar.sh
./scripts/saludar.sh

La salida esperada es El entorno de M00 está listo. También puedes pedir directamente a Bash que lea el archivo:

Terminal window
bash scripts/saludar.sh

El mensaje debe ser el mismo, pero la forma de arrancar cambia. Con ./scripts/saludar.sh, el archivo necesita permiso de ejecución y su shebang selecciona el intérprete. Con bash scripts/saludar.sh, eliges Bash explícitamente: este necesita poder leer el archivo, pero el archivo no necesita permiso de ejecución. No sirve para saltarse los permisos de lectura. Mantendremos la ejecución directa en los scripts que se automatizarán después.

El saludo anterior funciona con varias shells y no permite apreciar la diferencia. Vamos a usar una característica propia de Bash: echo {1..3}. Bash expande ese rango antes de ejecutar echo, de modo que muestra 1 2 3. La shell sh del contenedor Debian, que utiliza Dash, no realiza esa expansión y muestra literalmente {1..3}.

Haz esta comparación dentro del contenedor Debian, desde /bda-m00; no desde la terminal del anfitrión, donde sh puede ser otra shell. Abre un archivo nuevo:

Terminal window
nano scripts/con_shebang.sh

Guarda estas dos líneas; #! debe ocupar el inicio de la primera línea, sin espacios ni líneas vacías delante:

con_shebang.sh

#!/usr/bin/env bash
echo {1..3}

/usr/bin/env bash busca Bash en el PATH. Ahora crea una copia y ábrela:

Terminal window
cp scripts/con_shebang.sh scripts/sin_shebang.sh
nano scripts/sin_shebang.sh

En la copia elimina solo la primera línea. El archivo debe quedar así:

Terminal window
echo {1..3}

Concede permiso de ejecución al propietario de ambos archivos:

Terminal window
chmod u+x scripts/con_shebang.sh scripts/sin_shebang.sh

Para que la comparación no dependa de nuestra shell interactiva, lanzaremos las dos órdenes desde sh. La opción -c le pide que ejecute el texto entre comillas como una orden:

Terminal window
sh -c './scripts/con_shebang.sh'
sh -c './scripts/sin_shebang.sh'
Archivo ejecutado desde sh Salida esperada en Debian Qué ha ocurrido
con_shebang.sh 1 2 3 El shebang ha seleccionado Bash, aunque la orden se lanzó desde sh.
sin_shebang.sh {1..3} No hay un intérprete indicado. Dash intenta interpretar el archivo por su cuenta y no expande el rango.

El segundo archivo no da un error, pero produce un resultado diferente. Un script sin shebang no siempre falla: algunas shells intentan interpretarlo cuando el sistema no lo reconoce como ejecutable. Si lo lanzaras directamente desde Bash, podría parecer que todo funciona. Precisamente por eso no debemos confiar en esa recuperación implícita.

Comprueba ahora qué sucede cuando eliges el intérprete expresamente:

Terminal window
bash scripts/sin_shebang.sh
sh scripts/con_shebang.sh

La primera orden muestra 1 2 3: has elegido Bash aunque el archivo no tenga shebang. La segunda muestra {1..3}: has elegido sh y este no cambia a Bash por encontrar esa primera línea; la trata como un comentario. No es lo mismo sh archivo.sh que sh -c './archivo.sh': en el primer caso sh lee el archivo; en el segundo lanza una orden de ejecución directa y el shebang sí puede seleccionar el intérprete.

La conclusión no es que «sin shebang siempre hay un error», sino que el mismo texto puede comportarse de forma distinta según quién lo interprete. Conservaremos el shebang en nuestros scripts Bash para declarar qué intérprete necesitan.

Una variable se asigna sin espacios alrededor de =. Al usarla, se escribe $nombre; las comillas dobles protegen su valor.

Terminal window
symbol="BTCUSDT"
echo "Símbolo: $symbol"

La salida es Símbolo: BTCUSDT. Las comillas dobles permiten sustituir $symbol; las simples muestran el texto literalmente. Comprueba la diferencia sin cambiar la variable:

Terminal window
echo "Símbolo: $symbol"
echo 'Símbolo: $symbol'

La primera orden muestra el valor; la segunda, el texto $symbol. Las comillas dobles también conservan como un único argumento un valor que contenga espacios.

# inicia un comentario fuera de las comillas; no es una orden que se ejecute. La primera línea #! tiene la función especial de seleccionar el intérprete. Por ejemplo:

Terminal window
# Elegimos una carpeta del volumen, no una carpeta del sistema.
carpeta='data/raw'
echo "Carpeta: ${carpeta}"
echo "Longitud: ${#carpeta}"

La salida debe indicar data/raw y longitud 8. Las llaves delimitan el nombre de la variable y ${#carpeta} obtiene su longitud. Una asignación no lleva espacios alrededor del igual; carpeta y Carpeta serían nombres distintos. Utiliza comillas normales del código, no comillas tipográficas copiadas de un documento.

Los argumentos de un script se reciben mediante $1, $2 y así sucesivamente. Guarda este ejemplo en scripts/capturar_cotizacion.sh con nano; por ahora solo muestra entradas, todavía no escribe una captura:

capturar_cotizacion.sh

#!/usr/bin/env bash
symbol="${1:-}"
price="${2:-}"
echo "Símbolo: $symbol | Precio: $price"

${1:-} evita un error si falta el argumento y deja una cadena vacía. Prueba:

Terminal window
chmod u+x scripts/capturar_cotizacion.sh
./scripts/capturar_cotizacion.sh BTCUSDT 65000.50
./scripts/capturar_cotizacion.sh

La primera muestra Símbolo: BTCUSDT | Precio: 65000.50; la segunda muestra ambos campos vacíos. El script todavía no rechaza esa entrada: añadiremos las condiciones a continuación.

Para construir registros con formato controlado usaremos también printf. El primer argumento es una plantilla: %s inserta texto y \n termina una línea. Los valores posteriores ocupan los huecos en orden. Dentro del script anterior, puedes sustituir el mensaje con echo por:

Terminal window
printf 'Símbolo: %s | Precio: %s\n' "$symbol" "$price"

echo basta para mensajes sencillos; printf resulta útil al construir CSV y registros. Mantén fija la plantilla y pasa los datos como argumentos.

Variable de shell frente a variable de entorno

Section titled “Variable de shell frente a variable de entorno”

Una variable de la sesión no se transmite automáticamente a los programas que arrancas. export la incluye en el entorno de los procesos hijos. Prueba con una variable propia, sin cambiar HOME ni PATH:

Terminal window
mensaje_m00='Prueba de entorno'
bash -c 'echo "Sin exportar: ${mensaje_m00:-no disponible}"'
export mensaje_m00
bash -c 'echo "Exportada: $mensaje_m00"'
unset mensaje_m00

bash -c ejecuta el texto indicado en otra instancia de Bash. Antes de exportar debes ver no disponible; después, Prueba de entorno. unset retira nuestra variable de la sesión. ${variable:-texto} utiliza un valor alternativo cuando no está definida o está vacía, igual que ${1:-} deja vacíos los argumentos ausentes. No vuelques todo el entorno en un informe: puede contener información sensible.

if permite ejecutar unas órdenes solo cuando una condición se cumple. Empieza con una comparación cuyo resultado sea fácil de anticipar:

Terminal window
edad=20
if [[ "$edad" -ge 18 ]]; then
echo 'Puede participar en la actividad para adultos'
fi
echo 'Comprobación terminada'

[[ ... ]] evalúa la condición, then abre las órdenes condicionadas y fi cierra el bloque. -ge significa «mayor o igual» para números enteros. Con 20 verás ambos mensajes. Cambia solo la asignación a edad=16: verás únicamente Comprobación terminada. Lo que está después de fi no pertenece a la rama.

Cuando necesitas una respuesta también para el caso falso, añade else:

Terminal window
edad=16
if [[ "$edad" -ge 18 ]]; then
echo 'Mayor de edad'
else
echo 'Menor de edad'
fi

Debe mostrar Menor de edad. Prueba 18 y 25: ambos deben entrar en la primera rama. else no lleva una segunda condición: recoge el caso en que la primera no se cumple.

elif añade una condición que se comprueba solo si la anterior ha sido falsa:

Terminal window
primero=7
segundo=4
if [[ "$primero" -gt "$segundo" ]]; then
echo 'El primero es mayor'
elif [[ "$primero" -lt "$segundo" ]]; then
echo 'El segundo es mayor'
else
echo 'Los dos son iguales'
fi

El resultado es El primero es mayor. Prueba los pares 3/8 y 5/5 para recorrer las otras ramas. Bash selecciona una sola: no ejecuta las siguientes aunque después pudieran tener sentido por separado. Aquí los valores se asignan como enteros conocidos; una entrada escrita por una persona se valida antes de compararla, como haremos con read.

Qué estamos escribiendo: test, [ ... ] y [[ ... ]]

Section titled “Qué estamos escribiendo: test, [ ... ] y [[ ... ]]”

Encontrarás distintas formas de expresar pruebas en otros apuntes:

Terminal window
ruta='data/raw/notas-prueba.txt'
test -f "$ruta"
echo "$?"
[ -f "$ruta" ]
echo "$?"
[[ -f "$ruta" ]]
echo "$?"

Si existe el archivo de notas, los tres códigos deben ser 0. test y [ son órdenes para evaluar pruebas; [ necesita su argumento final ]. [[ ... ]] es una construcción de Bash con reglas propias. Usaremos esta última como forma principal, sin suponer que funciona en cualquier shell /bin/sh.

Los espacios de separación son necesarios: [ -f "$ruta" ] no es lo mismo que [-f "$ruta"]. Las comillas protegen valores con espacios. En las comparaciones de texto de este bloque escribiremos el texto literal entre comillas, evitando confundirlo con un patrón. No uses > para comparar números: puede ser redirección o comparación textual según el contexto; utiliza los operadores numéricos.

Archivos y directorios: pruebas habituales

Section titled “Archivos y directorios: pruebas habituales”

Una prueba responde una pregunta concreta; no todas significan «existe». Prepara un pequeño conjunto aislado de los datos del proyecto:

Terminal window
mkdir -p data/pruebas-bash/carpeta
cp -i data/raw/notas-prueba.txt data/pruebas-bash/contenido.txt
touch data/pruebas-bash/vacio.txt
ls -l data/pruebas-bash

Debes tener un directorio, una copia de las notas y un archivo vacío. Si repites y ya hay contenido en vacio.txt, touch no lo borra: compruébalo, no vacíes archivos para forzar una respuesta. No crees data/pruebas-bash/no-existe.txt, que será el caso inexistente.

Prueba Pregunta Ejemplo de condición
-e ¿Existe la ruta? [[ -e "$ruta" ]]
-f ¿Es un archivo normal? [[ -f "$ruta" ]]
-d ¿Es un directorio? [[ -d "$ruta" ]]
-s ¿Existe y tiene tamaño mayor que cero? [[ -s "$ruta" ]]
-r ¿El usuario actual puede leer? [[ -r "$ruta" ]]
-w ¿El usuario actual puede escribir? [[ -w "$ruta" ]]
-x ¿Puede ejecutar el archivo o atravesar el directorio? [[ -x "$ruta" ]]

Prueba primero el tipo, no los permisos:

Terminal window
ruta='data/pruebas-bash/contenido.txt'
if [[ -f "$ruta" ]]; then
echo 'Es un archivo normal'
elif [[ -d "$ruta" ]]; then
echo 'Es un directorio'
else
echo 'No es un archivo normal ni un directorio accesible'
fi

Con la ruta de contenido.txt verás el primer mensaje; con data/pruebas-bash/carpeta, el segundo; con la ruta inexistente, el tercero. Cambiar la variable permite probar la misma decisión, sin reescribir el programa.

Para distinguir un archivo con texto de otro vacío:

Terminal window
ruta='data/pruebas-bash/vacio.txt'
if [[ -s "$ruta" ]]; then
echo 'La ruta tiene contenido'
else
echo 'La ruta no existe o tiene tamaño cero'
fi

Debe mostrar el segundo mensaje si el archivo sigue vacío. -s no dice por sí sola que el contenido sea texto válido ni distingue tamaño cero de inexistencia. -e puede ser verdadera para un directorio mientras -f es falsa. Los permisos se evalúan para el usuario actual: como root puedes obtener resultados distintos de los de alumno-m00; se mantiene además la limitación de permisos del bind mount explicada en 01. No modifiques permisos del anfitrión para fabricar un resultado.

Prueba Significado
[[ -z "$texto" ]] La cadena tiene longitud cero.
[[ -n "$texto" ]] La cadena tiene algún carácter.
[[ "$texto" == "si" ]] El texto coincide exactamente con si.
[[ "$texto" != "si" ]] El texto es distinto de si.
Terminal window
nombre=''
if [[ -z "$nombre" ]]; then
echo 'No has indicado un nombre'
else
echo "Hola, $nombre"
fi

Verás el aviso. Prueba después nombre='Ana': debe aparecer el saludo. Con nombre=' ' hay un espacio y la cadena no está vacía. Esta prueba comprueba longitud, no elimina espacios ni decide que un nombre sea válido.

Terminal window
respuesta='si'
if [[ "$respuesta" == "si" ]]; then
echo 'Respuesta afirmativa'
else
echo 'Respuesta diferente de si'
fi

Prueba también SI y no: la comparación distingue mayúsculas y no son iguales a si. Más adelante resolveremos varias alternativas con case.

Operador Significado Ejemplo verdadero
-eq Igual a [[ 4 -eq 4 ]]
-ne Distinto de [[ 4 -ne 7 ]]
-lt Menor que [[ 4 -lt 7 ]]
-le Menor o igual que [[ 4 -le 4 ]]
-gt Mayor que [[ 7 -gt 4 ]]
-ge Mayor o igual que [[ 7 -ge 7 ]]

Reutiliza el ejemplo de primero y segundo para practicar cada caso. Son comparaciones de enteros; no convierten automáticamente una cadena como texto en un dato numérico válido. Los decimales con punto del proyecto se comprueban como formato mediante regex, no se comparan con estos operadores.

&& exige ambas condiciones; || admite al menos una; ! niega el resultado. Empieza por escribir las pruebas de forma separada y después combínalas:

Terminal window
ruta='data/pruebas-bash/contenido.txt'
if [[ -f "$ruta" && -r "$ruta" ]]; then
echo 'Es un archivo normal y se puede leer'
else
echo 'No se cumplen las dos condiciones'
fi

Debe mostrar el primer mensaje. Con la carpeta, -f será falsa aunque pueda leerse: no basta con cumplir una de las dos condiciones.

Terminal window
respuesta='S'
if [[ "$respuesta" == "s" || "$respuesta" == "S" ]]; then
echo 'Respuesta admitida'
fi
ruta='data/pruebas-bash/no-existe.txt'
if [[ ! -e "$ruta" ]]; then
echo 'La ruta no existe'
fi

Verás los dos mensajes. La negación afecta a la prueba que sigue, no crea ni elimina ninguna ruta. Las combinaciones se evalúan con cortocircuito: si la primera parte ya decide el resultado, no hace falta evaluar la segunda.

En la página 02 ya consultaste códigos con $?. Ahora el script decidirá su propio resultado con exit: 0 expresa éxito por convención y cualquier otro valor requiere interpretación. Consultar $? en la terminal no cambia el resultado del script; exit termina el script devolviendo el código indicado.

Con estas bases, vuelve a scripts/capturar_cotizacion.sh, la versión que solo mostraba argumentos. Añade la comprobación entre la asignación de argumentos y el mensaje final. >&2 envía el mensaje a errores y 64 sigue la convención sysexits para uso incorrecto. Son fragmentos para añadir al archivo, no para pegar en la terminal:

capturar_cotizacion.sh
if [[ -z "$symbol" ]]; then
printf 'Debes indicar un símbolo.\n' >&2
exit 64
fi

=~ compara con una expresión regular extendida. Solo ahora combinamos la estructura de if con los patrones de 02. Añade la validación del precio; el patrón se escribe sin comillas en este ejemplo:

capturar_cotizacion.sh
if [[ ! "$price" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
echo 'Precio inválido. Usa un decimal con punto.' >&2
exit 64
fi

Prueba el script con símbolo y precio válidos, luego con el símbolo vacío ("" como primer argumento) y después con un precio texto. Consulta echo "$?" inmediatamente después de cada ejecución: el caso válido debe devolver 0 y los rechazados 64. En el válido, el mensaje final es la última orden y termina correctamente.

Para validar también el símbolo, añade antes del mensaje final:

capturar_cotizacion.sh
if [[ ! "$symbol" =~ ^[A-Z0-9]{3,20}$ ]]; then
printf 'Símbolo inválido.\n' >&2
exit 64
fi

Añade esta condición al mismo script antes del mensaje final y prueba ahora btc/usdt con un precio válido: debe rechazarse con código 64 por el formato del símbolo.

Un bucle repite unas órdenes. En for, una variable toma sucesivamente cada elemento de una lista; el cuerpo situado entre do y done se ejecuta una vez por elemento:

Terminal window
for nombre in Ana Luis Marta; do
echo "Hola, $nombre"
done

Debes ver tres saludos, en ese orden. nombre vale primero Ana, después Luis y finalmente Marta. El nombre de la variable se escribe sin $ en la cabecera y con $ cuando quieres utilizar su valor.

Una lista puede contener elementos con espacios si proteges cada uno:

Terminal window
for tema in "Terminal Linux" "Permisos" "Bash básico"; do
echo "Tema: $tema"
done

También hay tres iteraciones, no cinco palabras separadas. Las comillas delimitan elementos completos.

Terminal window
for numero in {1..10}; do
echo "$numero"
done

La salida debe ser una línea por número: 1, 2, 3 y así hasta 10. {1..10} expande una secuencia de valores en Bash; no necesitas ejecutar seq ni acceder a archivos. Ambos extremos se incluyen.

Puedes indicar un salto como tercer valor:

Terminal window
for numero in {2..10..2}; do
echo "$numero"
done

Debe mostrar 2, 4, 6, 8 y 10. Son rangos escritos literalmente: {1..$limite} no sustituye un límite variable como quizá esperarías, porque la expansión de llaves sucede antes que la de variables. Para un límite variable usaremos un contador y una condición.

$(( ... )) calcula una expresión entera. No es lo mismo que $(comando), que ejecuta una orden y recoge su salida; utilizaremos esta segunda forma al trabajar con funciones y archivos.

Terminal window
primero=7
segundo=3
echo "Suma: $((primero + segundo))"
echo "Producto: $((primero * segundo))"
echo "Cociente entero: $((primero / segundo))"
echo "Resto: $((primero % segundo))"

Los resultados deben ser 10, 21, 2 y 1. La división es entera y % obtiene el resto. No dividas entre cero ni utilices esta aritmética para los precios decimales del proyecto. Trabajamos con enteros pequeños para evitar problemas de rango.

Un acumulador conserva el resultado parcial. Inicialízalo antes de entrar en el bucle:

Terminal window
suma=0
for numero in {1..10}; do
suma=$((suma + numero))
done
echo "Total: $suma"

La salida debe ser Total: 55. En cada vuelta se añade el número actual al resultado anterior; si pusieras suma=0 dentro del bucle, borrarías el resultado acumulado en cada iteración.

También puedes combinar repetición y condición para mostrar pares:

Terminal window
for numero in {1..10}; do
if [[ $((numero % 2)) -eq 0 ]]; then
echo "$numero"
fi
done

Debe producir los mismos cinco pares que el rango con salto. Esta vez se recorren diez valores y se muestran solo los que dejan resto cero al dividir entre dos.

while comprueba una condición antes de cada vuelta y repite mientras sea verdadera:

Terminal window
contador=1
while [[ "$contador" -le 10 ]]; do
echo "$contador"
contador=$((contador + 1))
done

Debes ver del 1 al 10. El contador empieza en 1, se compara con 10, se muestra y se incrementa. Cuando llega a 11, la condición es falsa y el bucle termina. Si empezara en 11, no habría ninguna iteración.

No omitas la actualización: si el contador se quedara en 1, la condición siempre sería verdadera. Para interrumpir un bucle accidentalmente infinito en la terminal, pulsa Ctrl + C. No vamos a provocar uno expresamente.

until repite mientras la condición sea falsa, es decir, hasta que se cumpla:

Terminal window
contador=1
until [[ "$contador" -gt 3 ]]; do
echo "$contador"
contador=$((contador + 1))
done

Muestra 1, 2 y 3. La condición de salida es «mayor que 3»; se detiene cuando se hace verdadera. Conserva while como referencia principal y compara sus condiciones antes de elegir una forma.

Detener o saltar una vuelta: break y continue

Section titled “Detener o saltar una vuelta: break y continue”

break termina el bucle más interno, pero no el script:

Terminal window
for numero in {1..10}; do
if [[ "$numero" -eq 4 ]]; then
break
fi
echo "$numero"
done
echo 'El programa continúa fuera del bucle'

Verás 1, 2, 3 y el mensaje final. No muestra 4 porque se comprueba antes del echo.

continue omite el resto de la vuelta actual y pasa a la siguiente:

Terminal window
for numero in {1..5}; do
if [[ "$numero" -eq 3 ]]; then
continue
fi
echo "$numero"
done

Muestra 1, 2, 4 y 5. exit, en cambio, terminaría el script completo. En un while, no coloques un continue que se salte la actualización necesaria del contador: podrías impedir que avance.

Ahora podemos recorrer los CSV sin mezclar por primera vez bucles, patrones y condiciones:

Terminal window
for csv_file in data/raw/*.csv; do
if [[ -f "$csv_file" ]]; then
echo "$csv_file"
fi
done

El patrón *.csv reutiliza los comodines de la página 01. Si no hay coincidencias, Bash normalmente conserva el patrón como texto; la comprobación -f evita tratarlo como archivo. do inicia el cuerpo y done termina el bucle. Debe mostrar todos los CSV preparados en la página 02, incluidos cotizaciones.csv y personas.csv; su número dependerá de qué ejercicios hayas realizado. En un directorio sin CSV no mostrará archivos.

Una función permite dar nombre a una operación y reutilizarla. Defínela antes de llamarla:

Terminal window
saludar_clase() {
echo 'Hola, clase'
}
saludar_clase
saludar_clase

Definirla no muestra nada; cada llamada ejecuta el cuerpo. Debes obtener dos saludos. Los paréntesis forman parte de la definición, pero no se escriben al llamarla.

Una función también recibe argumentos posicionales. local crea una variable limitada a la función y evita cambiar otra variable del mismo nombre fuera de ella:

Terminal window
saludar_persona() {
local nombre="${1:-visitante}"
echo "Hola, $nombre"
}
nombre='Grupo M00'
saludar_persona Ana
saludar_persona "Luis García"
saludar_persona
echo "Fuera de la función: $nombre"

Verás los saludos para Ana, Luis García y visitante, y después Fuera de la función: Grupo M00. $1 dentro de la función es su primer argumento, no necesariamente el primero del script. local no se utiliza como una asignación normal fuera de una función.

Salida, return y exit: tres cosas distintas

Section titled “Salida, return y exit: tres cosas distintas”

Una función puede escribir un dato por stdout. Para recoger esa salida se utiliza $(...), que ejecuta la orden y sustituye su salida de texto, retirando los saltos de línea finales:

Terminal window
sumar() {
local primero="$1"
local segundo="$2"
echo "$((primero + segundo))"
}
resultado="$(sumar 7 3)"
echo "Resultado: $resultado"

Debe mostrar Resultado: 10. Pasamos dos enteros conocidos; esta función sencilla no valida argumentos y no debes llamarla aún con texto o sin ambos valores.

return comunica el estado de una función y vuelve al punto donde se llamó. No devuelve un número calculado como dato:

Terminal window
es_par() {
local numero="$1"
if [[ $((numero % 2)) -eq 0 ]]; then
return 0
fi
return 1
}
if es_par 4; then
echo 'El número es par'
else
echo 'El número es impar'
fi
echo 'El programa sigue después de la función'

Muestra que 4 es par y después el mensaje final; con 5 debe elegir la otra rama. if puede comprobar el código de una orden o función directamente, sin escribir [[ ... ]]. return 5 comunicaría un estado no cero, no produciría el texto 5 por stdout. Usa códigos de estado pequeños y documentados; no los emplees para transportar sumas. exit terminaría el script, no solo la función.

Fecha y función de registro: combinar lo aprendido

Section titled “Fecha y función de registro: combinar lo aprendido”

Para obtener una fecha, date -u usa UTC y el argumento de formato fija sus campos. Reutilizaremos la sustitución $(...) para incorporarla a los registros. Prepara scripts/fecha-utc.sh, que necesitaremos en la página 04, con este contenido:

fecha-utc.sh

#!/usr/bin/env bash
date -u '+%Y-%m-%dT%H:%M:%SZ'

El formato combina año (%Y), mes (%m), día (%d), hora (%H), minuto (%M) y segundo (%S). Los guiones, los dos puntos, T y Z son texto literal. T separa fecha y hora; Z indica UTC, coherente aquí porque hemos utilizado -u.

Concédele ejecución y comprueba que produce una línea parecida a 2026-10-05T08:30:00Z y código 0:

Terminal window
chmod u+x scripts/fecha-utc.sh
./scripts/fecha-utc.sh
echo "$?"

Aplica ahora una función a los registros sin mezclarla todavía con la captura. Prepara scripts/registrar_ejecucion.sh. El archivo de registro se define antes de llamar a la función. "$*" reúne los argumentos de la función como un único mensaje:

registrar_ejecucion.sh

#!/usr/bin/env bash
log_file="/bda-m00/logs/ejecucion.log"
log() {
printf '[%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" >> "$log_file"
}
log 'Inicio de la prueba'
log 'Fin de la prueba'

Antes de usarla, log_file debe contener una ruta válida y su directorio debe existir. La carpeta logs ya está preparada. Prueba:

Terminal window
chmod u+x scripts/registrar_ejecucion.sh
./scripts/registrar_ejecucion.sh
cat logs/ejecucion.log

Cada ejecución añade dos líneas con fecha UTC, una de inicio y otra de fin. Si repites, no esperes dos líneas totales: >>, estudiado en la página 02, conserva las anteriores. No hace falta volver a explicar la redirección para usarla dentro de la función.

Al inicio de los scripts del módulo se usará:

Terminal window
set -euo pipefail
  • -e: detiene el script ante determinados fallos no controlados; tiene excepciones, por ejemplo las pruebas de un if.
  • -u: evita utilizar una variable no definida.
  • pipefail: una tubería falla si falla cualquiera de sus órdenes.

Estas opciones no sustituyen comprobaciones como if: ayudan a no continuar silenciosamente cuando aparece un error inesperado.

Para actualizar contadores hemos utilizado contador=$((contador + 1)). Conserva esa forma al aplicar estas opciones: una orden aritmética como ((contador++)) devuelve estado según el valor de la expresión y, empezando en cero, puede provocar una salida inesperada con -e. Incrementar un dato y comunicar éxito son cuestiones diferentes.

Si grep no selecciona líneas devuelve 1, por lo que un script con -e puede detenerse aunque no haya un error de lectura. Los resultados esperados que puedan ser falsos se comprueban mediante if. pipefail modifica el código final de una tubería; no revierte operaciones ya realizadas.

Comprueba también dependencias externas:

Terminal window
if ! command -v cut >/dev/null 2>&1; then
printf 'No se ha encontrado cut.\n' >&2
exit 69
fi

69 corresponde en sysexits a servicio o dependencia no disponible.

command -v busca una orden en PATH, la lista de directorios donde la shell busca ejecutables. Aquí comprobamos cut, ya utilizado en la página 02. /dev/null descarta la salida de esta comprobación concreta; el if proporciona el mensaje útil cuando no encuentra la herramienta. El ejemplo comprueba disponibilidad, no realiza una extracción de campos por sí mismo.

El entorno Debian del aula dispondrá de ShellCheck. Revisa el script antes de entregarlo:

Terminal window
shellcheck scripts/capturar_cotizacion.sh

ShellCheck revisa el código sin ejecutarlo. Una salida sin avisos no sustituye las pruebas manuales: hay que comprobar también argumentos válidos, argumentos inválidos, CSV y logs.

Bash puede comprobar la sintaxis sin ejecutar las órdenes del script:

Terminal window
bash -n scripts/capturar_cotizacion.sh
echo "$?"

Si la sintaxis es correcta, la primera orden no muestra nada y el código será 0. Si falta, por ejemplo, un fi, aparecerá un diagnóstico y un código distinto de 0. No alteres el script para provocar el error: basta con comprender qué comprueba esta herramienta. bash -n no valida los argumentos, la existencia de archivos ni el resultado de las órdenes. ShellCheck añade comprobaciones estáticas que una revisión de sintaxis no cubre.

Si el script no hace lo que esperas, usa la traza de Bash:

Terminal window
bash -x scripts/capturar_cotizacion.sh BTCUSDT 65000.50
echo "$?"

-x sí ejecuta el script. Muestra por la salida de errores las órdenes conforme se ejecutan, normalmente con un prefijo + y los valores ya sustituidos. En esta versión del script verás asignaciones como + symbol=BTCUSDT, las comprobaciones de los argumentos y el mensaje final; el código esperado es 0. Si lo ejecutas más adelante sobre el capturador completo, también escribirá CSV y registros: una traza no es una simulación.

La traza puede revelar argumentos y valores de variables. No la uses con contraseñas o tokens ni compartas una traza sin revisarla. El orden de trabajo es: comprobar sintaxis, revisar con ShellCheck y probar el comportamiento; utiliza la traza cuando necesites explicar un resultado inesperado.

7. Argumentos, entrada interactiva y lectura de líneas

Section titled “7. Argumentos, entrada interactiva y lectura de líneas”

Recuperamos del curso anterior tres situaciones diferentes: recibir argumentos al arrancar, preguntar a una persona y consumir texto por la entrada estándar. No se eligen por comodidad: un script programado con cron no debe esperar una respuesta del teclado.

$0 identifica cómo se invocó el script; $# es el número de argumentos y "$@" representa cada argumento por separado, conservando sus espacios. "$*", utilizado en nuestra función de log, los reúne en un solo texto: no es equivalente para recorrerlos.

Prepara scripts/argumentos.sh con Nano:

argumentos.sh

#!/usr/bin/env bash
echo "Script: $0"
echo "Cantidad: $#"
for argumento in "$@"; do
echo "Argumento: $argumento"
done

Concédele ejecución y prueba:

Terminal window
chmod u+x scripts/argumentos.sh
./scripts/argumentos.sh "notas de clase" permisos
./scripts/argumentos.sh

La primera ejecución debe indicar cantidad 2, con una línea para notas de clase y otra para permisos. La segunda debe indicar 0 y no entrar en el bucle. No recurras a for ... in $(ls) para recorrer archivos: los nombres con espacios pueden dividirse. Los patrones protegidos y el bucle de CSV ya estudiado evitan ese problema.

Las comparaciones ya explicadas permiten validar el número de argumentos: [[ $# -ne 2 ]] detecta una llamada que no tenga exactamente dos, antes de acceder a ellos. No hace falta volver a definir operadores numéricos ni pruebas de archivos para utilizarlos en un programa.

read guarda una línea de la entrada en una variable. -p muestra la pregunta y -r conserva las barras invertidas como datos. Prepara scripts/preguntar_nombre.sh:

preguntar_nombre.sh

#!/usr/bin/env bash
read -r -p 'Nombre: ' nombre
if [[ -z "$nombre" ]]; then
echo 'No has escrito un nombre.' >&2
exit 64
fi
echo "Hola, $nombre"

Ejecuta con bash scripts/preguntar_nombre.sh y escribe Ana: debe saludar y terminar con 0. Repite pulsando solo Intro: debe avisar por stderr y terminar con 64. Un espacio sigue sin ser una cadena vacía; no estamos validando nombres reales. Una pregunta no recibe respuestas por el hecho de ejecutarse en cron: estos diálogos se prueban solo en la terminal.

No sumes directamente cualquier texto recibido. Para empezar sin complicaciones de formato, admitiremos un solo dígito entre 0 y 9 por respuesta. Prepara scripts/sumar_interactivo.sh:

sumar_interactivo.sh

#!/usr/bin/env bash
read -r -p 'Primer número (0 a 9): ' primero
read -r -p 'Segundo número (0 a 9): ' segundo
if [[ ! "$primero" =~ ^[0-9]$ || ! "$segundo" =~ ^[0-9]$ ]]; then
echo 'Escribe un dígito del 0 al 9 en cada respuesta.' >&2
exit 64
fi
echo "Suma: $((primero + segundo))"

Prueba 4 y 5: debe mostrar Suma: 9. Con a o una respuesta vacía debe rechazar la entrada sin calcular. También rechazará 12 y 08, deliberadamente: el contrato del ejemplo es un solo dígito. Admitir otros formatos requiere acordar y validar sus reglas, no basta con quitar esta comprobación.

Reutiliza la entrada con read, ahora para seleccionar entre varias respuestas mediante case. Prepara scripts/confirmar.sh:

confirmar.sh

#!/usr/bin/env bash
read -r -p '¿Continuar con la prueba? (s/n): ' respuesta
case "$respuesta" in
s|S|si|SI) echo 'Respuesta afirmativa' ;;
n|N|no|NO) echo 'Respuesta negativa' ;;
*) echo 'Respuesta no reconocida' ;;
esac

En case, cada patrón termina con ), sus órdenes con ;; y la selección con esac. Los patrones son patrones de shell, no regex ERE. La barra | separa alternativas y * recoge el resto. Ejecuta con Bash y prueba s, NO y talvez: deben producir respectivamente respuesta afirmativa, negativa y no reconocida. Este ejemplo no modifica archivos ni realiza la operación que pregunta.

Ya has repetido con while sobre un contador. Ahora su condición será la propia lectura: read devuelve éxito al leer una línea y deja de hacerlo al llegar al fin de entrada. Prepara scripts/leer_lineas.sh:

leer_lineas.sh

#!/usr/bin/env bash
while IFS= read -r linea; do
echo "Línea: $linea"
done

IFS= evita recortar los espacios de los extremos de la línea para esta lectura; no cambia permanentemente el separador de toda la sesión. Prueba de dos formas equivalentes:

Terminal window
bash scripts/leer_lineas.sh < data/raw/notas-prueba.txt
cat data/raw/notas-prueba.txt | bash scripts/leer_lineas.sh

Ambas deben mostrar tres líneas con el prefijo Línea:. El script consume stdin: no necesita que sea una tubería ni un archivo concreto. Este ejemplo presupone líneas terminadas con salto de línea, como las notas preparadas; un formato que permita una última línea sin terminador requeriría tratar expresamente ese caso.

ID Actividad
BAS-1 Prepara con Nano dos archivos de texto dentro de data/raw, con dos y cuatro líneas. Escribe un script que reciba exactamente dos rutas, compruebe archivo y lectura y diga cuál tiene más líneas o si empatan. Prueba empate, ruta inexistente y número incorrecto de argumentos, conservando las entradas.
BAS-2 Escribe un script interactivo que pregunte un usuario y compruebe su existencia mediante id. Usa la cuenta alumno-m00 preparada en 01 y un nombre inexistente. No crees ni borres cuentas desde el script.
BAS-3 Adapta el lector de líneas para mostrar las notas en mayúsculas ASCII, conservando una línea por nota. Prueba tanto entrada redirigida como tubería y demuestra que el archivo original no cambia.
BAS-4 Amplía el ejemplo de confirmación para aceptar también Si, sí, Sí, SÍ y No, utilizando alternativas explícitas. Prueba una respuesta admitida de cada grupo y una desconocida. Explica por qué no programarías este diálogo con cron.

En BAS-2 se usa id nombre dentro de un if: un usuario inexistente es un resultado esperado, no motivo para continuar como si existiera. Puedes descartar su diagnóstico con las redirecciones conocidas y producir tu propio mensaje. Las soluciones se mantienen separadas de estos enunciados.

Escribe scripts/resumen_datos.sh para recibir un directorio, comprobar que existe y mostrar cada CSV situado directamente en él con su número de líneas (sin recorrer subdirectorios). Debe terminar con código 1 si el directorio no existe. Usa el bucle y las comprobaciones estudiados; si no hay CSV, no debe intentar leer el patrón literal. Reutiliza la lectura por entrada estándar de la página 02, wc -l < "$csv_file", y la sustitución de comandos de este bloque: puedes guardar el resultado con lines="$(wc -l < "$csv_file")".

Pruébalo con data/raw, una carpeta inexistente y un directorio vacío que prepares dentro del volumen. Conserva salida normal, errores y código de cada prueba en logs/. El primer caso debe mostrar al menos cotizaciones.csv; el inexistente, un error y código 1; el vacío, ningún archivo y código 0.

  1. Crea un script que reciba símbolo y precio como argumentos y muestre ambos valores.
  2. Añade una función log a un script existente y registra inicio y fin de ejecución.
  3. Revisa un script con ShellCheck y corrige los avisos que aparezcan. Si no aparecen, registra ese resultado y explica qué pruebas funcionales siguen siendo necesarias.
  4. Ejecuta dos veces el script de fecha UTC preparado en la teoría y conserva sus salidas y códigos en logs. Explica el significado de la Z final y comprueba el formato de ambas fechas.
  5. En una copia de fecha-utc.sh, guarda una ruta en una variable y muéstrala entre comillas dobles. Conserva el script original para la página siguiente.
  6. Crea un script que reciba una ruta de archivo, compruebe que existe como archivo normal y muestre un mensaje de éxito o un error con código no cero. Pruébalo con data/raw/notas-prueba.txt, con una ruta inexistente y sin argumentos.
  7. Añade una condición que rechace un símbolo vacío mediante un mensaje por stderr y un código de salida distinto de 0.
  8. Añade una segunda condición que compruebe el formato del precio con la regex usada en este documento.
  9. Usa un bucle for para mostrar los nombres de los CSV presentes en data/raw/, sin modificar ninguno.
  10. Combina una función log, set -euo pipefail y una comprobación con command -v en un script pequeño; conserva un log de éxito o de fallo y revísalo con ShellCheck.

Objetivo. Escribe scripts/copiar_documentos.sh para copiar los archivos .txt no vacíos de una carpeta a otra y dejar un informe de lo realizado, conservando intactos los originales. Trabaja desde /bda-m00.

Prepara los datos. Crea data/documentos y prepara con Nano los archivos siguientes. No sobrescribas archivos anteriores si repites el ejercicio: utiliza una carpeta de prueba nueva.

Archivo Contenido de prueba
notas.txt Dos líneas: Repasar los permisos y Practicar los bucles.
ideas de clase.txt Una línea: Preparar un informe de copia.
vacio.txt Archivo vacío, sin líneas ni espacios; puedes crearlo con touch.
inventario.csv Dos líneas: producto,unidades y teclado,3.

Cómo se utilizará. Este ejemplo muestra la llamada, no la solución:

Terminal window
./scripts/copiar_documentos.sh data/documentos entregas/copia-documentos

La primera ruta es el origen; la segunda, un destino que todavía no debe existir. Prepara previamente su carpeta superior entregas. Para repetir una copia válida, utiliza otro nombre de destino en vez de borrar el anterior.

Qué debe hacer el script.

  1. Exigir exactamente dos argumentos. Si faltan o sobran, mostrar ayuda de uso por stderr y terminar con 64.
  2. Comprobar que el origen es un directorio que permite listar y acceder a su contenido. Si no existe o no es accesible, explicar el problema y terminar con código no cero.
  3. Rechazar el destino si ya existe, tanto si es una carpeta como si es un archivo. No sobrescribir ni reutilizar su contenido.
  4. Crear el directorio de destino. Si no puede crearse o falla una copia, no mostrar un éxito final: terminar con código no cero y conservar el diagnóstico. No se exige deshacer las copias ya realizadas.
  5. Recorrer solo los archivos .txt no ocultos situados directamente en el origen, sin entrar en subcarpetas. Ignorar directorios que terminen en .txt y evitar tratar el comodín como un archivo cuando no haya coincidencias.
  6. Omitir los archivos vacíos y copiar los que tengan contenido, conservando sus nombres. El nombre con espacios debe funcionar. Ante un archivo no legible, informar del problema y terminar con código no cero.
  7. Contar los documentos copiados y los vacíos omitidos. No incluir el CSV ni otras extensiones en esos contadores.
  8. Crear informe.log dentro del destino. Debe incluir fecha UTC, una entrada por documento copiado y un resumen con ambos contadores. Puedes añadir también las omisiones al registro.
  9. Mostrar el resumen por pantalla y terminar con 0 si el proceso ha concluido sin fallos. Si no hay .txt, crear el destino y el informe con ambos contadores a cero; no es un error.

Pistas y posibles recursos. Reutiliza $#, $1, $2, if, for, funciones, contadores y continue. Las pruebas -d, -r, -x, -e, -f y -s permiten decidir qué rutas y archivos tratar. Recuerda proteger las variables con comillas y no recorrer la salida de ls. Actualiza los contadores mediante asignación aritmética si utilizas set -e, como se explica en este bloque. Combina cp, mkdir, date -u y redirecciones para realizar y registrar las operaciones.

Una ayuda adicional: muchas órdenes, como cp y mkdir, aceptan -- para indicar que han terminado las opciones; lo que sigue se interpreta como rutas aunque comience por guion. No significa «copiar recursivamente». Puedes consultar su ayuda antes de utilizarlo.

Comprobaciones.

Caso Qué debes comprobar
Datos preparados Dos documentos copiados, uno vacío omitido y CSV ignorado; informe y código 0.
Nombre con espacios El archivo mantiene su nombre y contenido en el destino.
Destino existente Se rechaza sin modificarlo ni copiar dentro de él.
Origen inexistente Diagnóstico por stderr y código no cero.
Cero, uno o tres argumentos Ayuda de uso y código 64.
Origen vacío o sin .txt Informe con ambos contadores a cero y código 0.
Conservación Los cuatro originales mantienen sus nombres y contenido.

Entrega como evidencia de refuerzo tu script, el informe y una nota con las pruebas realizadas. Revísalo con ShellCheck; no basta con obtener un informe correcto en el primer caso. Las pruebas de denegación de permisos requieren un usuario ordinario y dependen del bind mount, como se explicó en 01: no cambies permisos de archivos personales para provocarlas.

Relación con resultados de aprendizaje y criterios de evaluación

Section titled “Relación con resultados de aprendizaje y criterios de evaluación”
RA / CE Evidencia
RA0.c) Script con parámetros, validación, códigos de salida, log y revisión ShellCheck.