1. Un método
La tentación ante un error es cambiar algo al azar y volver a probar. A veces funciona, pero sin entender por qué, y el error suele volver. Un método ordenado es más rápido a la larga:
- Reproducir. Encontrar una forma fiable de provocar el error: qué argumentos, qué datos, qué directorio. Un error que no se puede reproducir no se puede confirmar como resuelto. reproducir
- Leer el mensaje. Completo, con el número de línea si lo tiene. Muchas veces ya dice qué pasó. leer
- Observar. Mirar qué hace realmente el script, con la traza o con mensajes, en lugar de suponerlo. observar
- Aislar. Reducir el problema hasta la línea, el dato o la condición exacta que lo provoca. aislar
- Corregir y verificar. Hacer un cambio que se entiende, y comprobar con el caso del paso 1 que el error desapareció y que no apareció otro. corregir
El paso que más se saltea es el tercero, y es el que más tiempo ahorra. La diferencia entre lo que uno cree que hace el script y lo que realmente hace es, casi siempre, el error.
2. Antes de ejecutar: bash -n y ShellCheck
Las dos herramientas del tema 4 encuentran muchos errores sin ejecutar nada, y conviene usarlas siempre primero:
| Herramienta | Encuentra | Ejemplo |
|---|---|---|
bash -n script | Errores de sintaxis: bloques sin cerrar, comillas sin pareja. | syntax error: unexpected end of file |
shellcheck script | Errores que la sintaxis permite: variables sin comillas, nombres mal escritos, construcciones frágiles. | SC2154: destion is referenced but not assigned |
Si ShellCheck señala algo en la línea donde aparece el error, empieza por ahí: buena parte de los errores de Bash son exactamente los que ShellCheck conoce. Los que quedan, los de lógica, son los que necesitan la traza.
3. La traza: bash -x
Ejecutado con bash -x, un script muestra en la salida de errores cada comando que ejecuta, precedido de +, después de todas las expansiones del tema 3: variables reemplazadas, comodines expandidos, sustituciones resueltas. Es lo que el comando realmente recibe.
$ bash -x contar-lineas notas
+ set -euo pipefail
+ dir=notas
+ total=0
+ archivos=0
+ find notas -name '*.txt'
+ read -r f
++ wc -l
+ n=3
+ total=3
+ archivos=1
+ read -r f
…
+ echo '0 archivos, 0 líneas'
0 archivos, 0 líneas
Las líneas con + son la traza; la última, sin él, es la salida normal del script. Como la traza va a la salida de errores, se puede separar de la salida: bash -x script 2> traza.txt.
Si el script está instalado como comando, en lugar de buscar su ruta para pasársela a bash, se puede escribir bash -x "$(command -v contar-lineas)" notas. Y la opción funciona también combinada con las demás: bash -xv, o en la línea shebang, aunque como vimos en el tema 2 eso no funciona con env.
4. Leer la traza
La traza tiene algunas convenciones que conviene conocer para leerla con soltura:
| Lo que se ve | Significa |
|---|---|
+ comando | Un comando del nivel principal. |
++ comando | Un comando dentro de una sustitución $( ); +++, dentro de una sustitución anidada. |
'mi nota.txt' | Un argumento con espacios o caracteres especiales, entre comillas simples para mostrar dónde empieza y termina. |
+ wc -l mi nota.txt | Sin comillas: son dos argumentos separados, mi y nota.txt. |
+ n=3 | Una asignación, con el valor ya calculado. |
+ [[ -f notas ]] | Una prueba, con los valores reales; si después no aparece el cuerpo del if, fue falsa. |
$ bash -xc 'f="mi nota.txt"; wc -l $f'
+ f='mi nota.txt'
+ wc -l mi nota.txt
wc: mi: No such file or directory
wc: nota.txt: No such file or directory
La asignación muestra el valor entre comillas: es uno solo. El wc, en cambio, recibe dos palabras. La traza responde de un vistazo la pregunta que el mensaje de error dejaba abierta: ¿por qué wc busca un archivo llamado mi?
5. Trazar solo un tramo: set -x y set +x
En un script largo, la traza completa es abrumadora. set -x activa la traza desde ese punto, y set +x la desactiva, de modo que se puede observar solo la parte sospechosa:
preparar_entorno
set -x # traza desde aquí…
calcular_totales "$dir"
set +x # …hasta aquí
generar_informe
La línea set +x aparece ella misma en la traza, como + set +x. Si molesta, se puede ocultar con { set +x; } 2>/dev/null. Y dentro de una función, el tramo puede ser la función entera: set -x al comienzo y set +x antes de cada return.
6. Una traza más informativa: PS4
El + que precede a cada línea de la traza es el valor de la variable PS4, y se puede cambiar. Como se expande en cada línea, puede incluir variables que muestren dónde se está:
export PS4='+ ${BASH_SOURCE##*/}:${LINENO} ${FUNCNAME[0]:-main} [sub ${BASH_SUBSHELL}] '
| Variable | Muestra |
|---|---|
${BASH_SOURCE##*/} | El archivo, útil cuando hay bibliotecas. |
${LINENO} | El número de línea. |
${FUNCNAME[0]:-main} | La función en curso, o main fuera de ellas. |
${BASH_SUBSHELL} | El nivel de subshell: 0 en el script, 1 o más dentro de una tubería o de ( ). |
El primer carácter de PS4 se repite para indicar la profundidad de las sustituciones, por eso conviene que siga siendo +. Las comillas simples son esenciales: las variables deben expandirse en cada línea de la traza, no al asignar PS4. Exportarlo hace que valga también para bash -x:
$ PS4='+ ${BASH_SOURCE##*/}:${LINENO} [sub ${BASH_SUBSHELL}] ' bash -x contar-lineas notas
+ contar-lineas:4 [sub 0] dir=notas
+ contar-lineas:5 [sub 0] total=0
+ contar-lineas:6 [sub 0] archivos=0
+ contar-lineas:8 [sub 0] find notas -name '*.txt'
+ contar-lineas:8 [sub 1] read -r f
++ contar-lineas:9 [sub 2] wc -l
+ contar-lineas:9 [sub 1] n=3
+ contar-lineas:10 [sub 1] total=3
+ contar-lineas:11 [sub 1] archivos=1
…
+ contar-lineas:14 [sub 0] echo '0 archivos, 0 líneas'
Con este PS4, la causa del error del ejemplo salta a la vista, como veremos en la sección 12.
7. La traza en un archivo
La traza se mezcla con los mensajes del script en la salida de errores. Para separarla, Bash permite enviarla a otro descriptor con la variable BASH_XTRACEFD:
exec 9> /tmp/traza.log # abrir el archivo en el descriptor 9
BASH_XTRACEFD=9 # la traza va al descriptor 9
set -x
Es especialmente útil para depurar un script que corre desde cron o como servicio, donde no hay pantalla: se agregan estas líneas, se espera a la próxima ejecución y se lee el archivo con calma. Hay que recordar quitarlas después, por lo que dice la sección 10 del tema 25: la traza muestra los valores de todas las variables, contraseñas incluidas.
Una opción emparentada es set -v (bash -v), que muestra cada línea tal como está escrita, antes de expandirla. Es menos útil que -x para encontrar errores, pero ayuda a seguir el recorrido por un script largo, y combinadas, -xv, muestran cada línea escrita y luego cómo quedó.
8. Mensajes de depuración
A veces la traza es demasiado: solo interesa el valor de unas pocas variables en un par de lugares. Un mensaje con declare -p, del tema 5, a la salida de errores, muestra el valor exacto, con sus espacios y caracteres invisibles:
declare -p origen destino opciones >&2
declare -- origen="/home/ana/proyecto "
declare -- destino="/respaldos"
declare -a opciones=([0]="-a" [1]="--delete")
El espacio al final de origen, invisible con echo, aquí se ve. Para no tener que agregar y quitar estos mensajes, se puede dejar la depuración preparada en el script, activable con una variable de entorno:
if [[ -n "${DEPURAR:-}" ]]; then
export PS4='+ ${BASH_SOURCE##*/}:${LINENO} ${FUNCNAME[0]:-main} '
set -x
fi
Así, DEPURAR=1 respaldo ~/docs ejecuta el script con la traza informativa, sin modificarlo. La biblioteca de registro del tema 25 ofrece algo similar a menor escala: los mensajes log_debug, que solo aparecen con -v.
9. trap ERR y trap DEBUG
El trap … ERR de los temas 23 y 24 es, en sí mismo, una herramienta de depuración: dice qué comando falló y en qué línea, sin necesidad de traza. Conviene tenerlo siempre en los scripts con set -e.
La pseudoseñal DEBUG va más lejos: se produce antes de cada comando. Con ella se puede construir una ejecución paso a paso rudimentaria, que muestra cada comando y espera Enter antes de ejecutarlo:
trap 'echo "[línea $LINENO] $BASH_COMMAND" >&2; read -rp "Enter para seguir… " < /dev/tty' DEBUG
Es poco elegante, pero en un tramo confuso permite detenerse en cada comando y, desde otra terminal, mirar el estado de los archivos. Para algo más completo existe bashdb, un depurador para Bash con puntos de ruptura y ejecución paso a paso al estilo de los depuradores de otros lenguajes, y algunos editores lo integran.
10. Aislar el problema
Cuando la traza no basta para ver la causa, hay que reducir el problema. Algunas técnicas:
- Probar la línea sospechosa en la terminal, con los mismos valores que muestra la traza. Si falla igual, el problema está en esa línea; si no, está en el contexto: el directorio, las variables de entorno, las opciones activas.
- Reducir los datos. Si el script falla con un directorio de mil archivos, probar con diez, luego con uno. El archivo que provoca el fallo suele tener algo especial: un espacio, un salto de línea, un guion inicial, una tilde.
- Dividir el script por la mitad. Agregar un
exiten medio y ver si el error ya ocurrió. Si no, el problema está en la segunda mitad; si sí, en la primera. Repetir hasta encontrar la línea. - Comparar con un caso que funciona. «Funciona en mi equipo y no en el servidor» casi siempre se explica por una diferencia: versión de Bash,
PATH, configuración regional, permisos, un programa que falta.bash --version,envycommand -ven los dos lados suelen encontrarla. - Explicárselo a alguien, o por escrito, línea por línea. Muchas veces el error aparece en el momento de describir lo que el código «debería» hacer.
11. Los rastros de los errores típicos
Muchos errores dejan un rastro característico. Reconocerlo ahorra la búsqueda:
| Síntoma | Causa probable | Cómo confirmarlo |
|---|---|---|
| Un comando recibe un nombre de archivo partido en dos | Una variable sin comillas (tema 3). | bash -x: el argumento aparece sin comillas simples. |
$'\r': command not found, o rutas con $'\r' | Finales de línea de Windows (tema 2). | cat -A script muestra ^M$ al final de las líneas. |
| Una variable vale lo de antes después de un bucle | El bucle corrió en una subshell, por una tubería (tema 16). | BASH_SUBSHELL en PS4. |
| El script termina sin mensaje, a la mitad | set -e y un comando que «falló» en silencio, como grep (tema 23). | trap … ERR, o bash -x: la última línea de la traza es la que falló. |
Funciona a mano, falla en cron | PATH reducido, variables de entorno ausentes, otro directorio actual (tema 34). | Registrar env y pwd al comienzo del script. |
Un número da value too great for base | Un cero a la izquierda, leído como octal (tema 7). | La traza muestra el valor con el cero. |
sort o grep dan resultados distintos en otra máquina | Otra configuración regional. | locale en las dos; LC_ALL=C para fijarla (tema 5). |
| Una variable parece vacía sin motivo | Nombre mal escrito, o asignada dentro de $( ) (tema 19). | ShellCheck SC2154; set -u. |
12. Un caso completo
Este script debería contar los archivos .txt de un directorio y el total de sus líneas. Con un directorio que tiene dos archivos, de 3 y 2 líneas, muestra:
#!/usr/bin/env bash
set -euo pipefail
dir="$1"
total=0
archivos=0
find "$dir" -name '*.txt' | while read -r f; do
n=$(wc -l < "$f")
total=$(( total + n ))
archivos=$(( archivos + 1 ))
done
echo "$archivos archivos, $total líneas"
$ contar-lineas notas
0 archivos, 0 líneas
Siguiendo el método:
- Reproducir: el error ocurre siempre, con cualquier directorio. No hay mensaje que leer: el script termina bien, con un resultado incorrecto.
- Antes de ejecutar:
bash -nno encuentra nada y ShellCheck tampoco señala nada decisivo. Es un error de lógica. - Observar: la traza simple de la sección 3 muestra algo desconcertante:
total=3, despuéstotal=5, y al final se muestra 0. El bucle calcula bien, pero el valor se pierde. - Aislar: con el
PS4de la sección 6, las líneas del bucle aparecen con[sub 1], y elechofinal con[sub 0]. El bucle corre en una subshell, porque está al final de una tubería, y sus asignaciones no llegan al script. Es el problema que vimos en el tema 16. - Corregir: conectar
findal bucle con una sustitución de procesos, de modo que el bucle corra en la shell principal. Y de paso, separar los nombres con nulos, para que funcione con cualquier nombre de archivo.
#!/usr/bin/env bash
set -euo pipefail
dir="$1"
total=0
archivos=0
while IFS= read -r -d '' f; do
n=$(wc -l < "$f")
total=$(( total + n ))
archivos=$(( archivos + 1 ))
done < <(find "$dir" -name '*.txt' -print0)
echo "$archivos archivos, $total líneas"
$ contar-lineas notas
2 archivos, 5 líneas
La verificación usa el mismo caso que reproducía el error, y el resultado coincide con lo que se sabía de antemano: 3 + 2 líneas en dos archivos. Sin la traza, este error invita a sospechar de wc, de la aritmética o de find, que funcionaban perfectamente. Observar antes de suponer llevó directo a la causa.
13. Errores frecuentes
| Al depurar… | Qué pasa | Mejor |
|---|---|---|
| Cambiar cosas al azar hasta que funcione | No se sabe por qué funciona, y el error vuelve. | Observar con la traza y entender la causa. |
PS4="+ $LINENO " con comillas dobles | Muestra siempre el mismo número de línea. | Comillas simples. |
echo "$var" para inspeccionar | No se ven espacios finales ni caracteres invisibles. | declare -p var >&2 |
Mensajes de depuración con echo a la salida estándar | Se mezclan con los datos y cambian el resultado. | A la salida de errores. |
Dejar set -x o la traza a archivo | Llena registros y expone secretos. | Activarla con DEPURAR, y quitar lo provisorio. |
| Probar la corrección con otro caso | No se sabe si el error original desapareció. | Verificar con el caso que lo reproducía. |
14. Resumen
- El método: reproducir, leer el mensaje, observar, aislar, corregir y verificar con el mismo caso.
bash -ny ShellCheck encuentran muchos errores sin ejecutar nada.bash -xmuestra cada comando después de las expansiones, en la salida de errores;++indica una sustitución, y las comillas simples, dónde empieza y termina cada argumento.set -xyset +xlimitan la traza a un tramo.PS4define el prefijo de la traza; conBASH_SOURCE,LINENO,FUNCNAMEyBASH_SUBSHELL, muestra dónde ocurre cada línea.BASH_XTRACEFDenvía la traza a un archivo aparte;set -vmuestra las líneas sin expandir.declare -p var >&2muestra el valor exacto; una variable comoDEPURARpermite activar la traza sin modificar el script.trap … ERRdice dónde falló;trap … DEBUGpermite avanzar paso a paso.- Para aislar: probar la línea en la terminal, reducir los datos, dividir el script, comparar con un caso que funciona.
- Muchos errores tienen un rastro característico: argumentos partidos,
^M, subshells,set -esilencioso,cron, octal, configuración regional.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué es importante reproducir un error antes de intentar corregirlo?
Porque sin una forma fiable de provocarlo, no se puede observar qué lo causa ni confirmar que la corrección lo resolvió.
2. ¿Qué muestra bash -x en cada línea de la traza?
El comando que se va a ejecutar, después de todas las expansiones: con las variables reemplazadas, los comodines expandidos y las sustituciones resueltas.
3. En la traza, ¿qué diferencia hay entre + wc -l 'mi nota.txt' y + wc -l mi nota.txt?
En la primera, wc recibe un solo argumento con un espacio. En la segunda, recibe dos argumentos, mi y nota.txt: típicamente, una variable sin comillas.
4. ¿Qué indica ++ al comienzo de una línea de la traza?
Que el comando se ejecuta dentro de una sustitución de comandos. Cada nivel de anidamiento agrega un +.
5. ¿Por qué PS4 debe asignarse con comillas simples?
Para que variables como $LINENO se expandan en cada línea de la traza. Con comillas dobles se expandirían una sola vez, al asignar PS4, y mostrarían siempre el mismo valor.
6. ¿Qué revela ${BASH_SUBSHELL} en la traza?
En qué nivel de subshell se ejecuta cada comando. Un bucle al final de una tubería aparece con un nivel mayor que 0, lo que explica que sus variables se pierdan.
7. ¿Para qué sirve BASH_XTRACEFD?
Para enviar la traza a otro descriptor, por ejemplo a un archivo abierto con exec 9> traza.log, separándola de los mensajes del script.
8. ¿Por qué declare -p var es mejor que echo "$var" para inspeccionar una variable?
Porque muestra el valor entre comillas, con los espacios del final y los caracteres invisibles visibles, y además indica si es un array y cuáles son sus elementos.
9. ¿Cómo se deja la depuración preparada en un script sin que esté siempre activa?
Activando la traza solo si una variable de entorno tiene valor: [[ -n "${DEPURAR:-}" ]] && set -x. Así, DEPURAR=1 script la activa sin modificar el archivo.
10. Un script funciona a mano y falla en cron. ¿Qué conviene registrar para averiguar por qué?
El entorno con env, el directorio actual con pwd y, si hace falta, la traza en un archivo con BASH_XTRACEFD. Las causas típicas son un PATH reducido, variables ausentes y otro directorio de trabajo.
Para practicar
- Crea el script
contar-lineascon el error, un directorio con algunos.txt, y reproduce el caso de la sección 12 con la traza simple y con elPS4informativo. - Agrega a tu
~/.bashrcunPS4informativo exportado, para que valga en todas tus trazas. - Ejecuta con
bash -xuno de los scripts de temas anteriores y localiza en la traza cada sustitución de comandos. - Agrega a un script la activación de la traza con
DEPURAR, enviándola a un archivo conBASH_XTRACEFD. - Prueba la ejecución paso a paso con
trap … DEBUGen un script de cinco o seis líneas. - Crea un script con finales de línea de Windows, como en el tema 2, ejecútalo, y diagnostícalo con
cat -A.