Depurar código en bash-script
Para un administrador de sistemas, es necesario el uso de scripts de shell para realizar su trabajo más eficientemente. Vemos como depurar el código bash-script en nuestra terminal.
Sumario
- Que es un script
- Depuración del código en bash-script
- Depuración del código completo
- Modificando la primera linea del script
- Indicando la depuración en la orden de ejecución
- La opción -e
- La opción -v
- Depuración por partes del código
- Ejecución paso a paso
Que es un script
Los scripts de shell son una herramienta de ejecución de comandos reutilizables, que se guardan en un fichero de texto ejecutable. Para crear este fichero ejecutable puedes utilizar cualquier editor de texto. Estos prácticos scripts hacen de los sistemas GNU/Linux uno de los sistemas operativos más flexibles de los disponibles.
Los scripts comienzan con el «shebang», una expresión compuesta por un conjunto de caracteres, una almohadilla y un final de exclamación «#!» y a continuación la ruta absoluta a intérprete de comandos que se debe llamar, por ejemplo «#!/bin/bash».

Es muy recomendable añadir comentarios al código para explicarlo, lo que nos ayudará en las modificaciones que precise así como también ayudará a comprenderlo a terceros.
No obstante, cuando estamos «picando código», no siempre salen las cosas como esperamos y tenemos que determinar que es lo que falla en la ejecución del script. Cuando nos encontramos en esta situación, nos viene muy bien tener opciones de depuración del código.
Para esta tarea podemos usar las opciones de depuración del shell que nos permiten hacerlo, tanto de manera parcial o analizando todo el código.
Lo más habitual es ejecutar el script con la opción «-x», es decir, en modo de depuración. Nos mostrará en la salida estandar cada comando y sus argumentos, después de que los comandos se hayan expandido pero antes de que se ejecuten. En la salida no veremos los comentarios añadidos al código.
Depuración del código en bash-script
Como hemos comentado, podemos depurar todo el código del programa a la vez o por partes. Vemos como hacerlo.
Depuración del código completo.
Tanto en bash como en sh, la depuración es muy importante ya que su tipado débil, puede provocar errores difíciles de encontrar, como por ejemplo el valor o el nombre de una variable mal introducida.
Para ello tenemos la opción «set -u». Colocando esta sentencia al principio del script, el intérprete nos devolverá un error cuando se llame a una variable a la que no hayamos asignado un valor. Veamos.
Tenemos un script llamado «variable.sh» al que hemos dado permisos de ejecución con «chmod +x variable.sh», con el siguiente contenido.
#!/bin/bash val=1 echo $(expr $val + 1)

Al ejecutarlo nos da la salida
javier@debian:~/Pruebas$ ./variable.sh 2 javier@debian:~/Pruebas$

Modificamos el código equivocándonos en la variable
#!/bin/bash val=1 echo $(expr $var + 1)

Y en la salida, bash no nos dice el error
javier@debian:~/Pruebas$ ./variable.sh 1 javier@debian:~/Pruebas$

Si añadimos «set -u» al código
#!/bin/bash set -u val=1 echo $(expr $var + 1)

javier@debian:~/Pruebas$ ./variable.sh ./variable.sh: línea 4: var: variable sin asignar javier@debian:~/Pruebas$

Si utilizamos «set +u», se revierte el efecto.
Pero el error puede que no esté en las variables. Con la opción «-x» (xtrace), depuraremos todo el código. Tenemos dos opciones.
Modificando la primera linea del script
Para utilizar este método, tienes que modificar la primera linea del script, el «shebang», con la opción de depuración. La opción «-x» ordena imprimir, por la salida estandar, cada una de las instrucciones que se ejecutan en el script, permitiéndonos seguir, de forma sencilla, la ejecución del script.
#!/bin/bash -x
Podemos utilizar las opciones adicionales siguientes:
- -e Indica al intérprete que cancele la ejecución del programa nada más ejecutar una orden que devuelva error (es decir, un valor distinto de 0).
- -v (verbose) Imprime por la salida estandar todas las órdenes que se ejecutan. Así veremos cual falla.
Lo vemos con un ejemplo sencillo. Creamos un script al que llamamos «script.sh», una calculadora sencilla y le damos permisos de ejecución.
nano script.sh
Copia y pega el siguiente código
#/bin/bash #calculadora clear sum=1 i="y" echo "Vamos a realizar una operacion aritmetica echo echo sleep 2 echo "Introduce la primera cifra" read n1 echo "Introduce la segunda cifra" read n2 while [ $i = "y" ] do echo "1.Sumar" echo "2.Restar" echo "3.Multiplicar" echo "4.Dividir" echo "Introduce el numero de operacion deseada (1 al 4)" read ch case $ch in 1)sum=$(expr $n1 + $n2 echo " Resultado de la suma ="$sum;; 2)sum=$(expr $n1 - $n2) echo " Resultado de la resta ="$sum;; 3)sum=$(expr $n1 \* $n2) echo " Resultado de la multiplicacion ="$sum;; 4)sum=$(expr $n1 / $n2) echo " Resultado de la division ="$sum;; *)echo "Tu seleccion es incorrecta";; esac echo "Quieres continuar (y/n) ?" read i if [ $i != "y" ] then echo "Hemos terminado" $USER exit fi done

Damos permiso de ejecución
chmod +x script.sh
Al ejecutarlo nos da un error. Faltan unas comillas dobles en la linea 21.
./script.sh: línea 21: error sintáctico cerca del elemento inesperado `(' ./script.sh: línea 21: ` echo "Introduce el numero de operacion deseada (1 al 4)"'

Si añadimos la opción «-x» al shebang
#!/bin/bash -x #calculadora ...

Fíjate que en la primera línea he incluido la opción -x. Si ahora ejecutas el script con «bash script.sh», verás lo siguiente,
+ sum=1 + i=y ./script.sh: línea 21: error sintáctico cerca del elemento inesperado `(' ./script.sh: línea 21: ` echo "Introduce el numero de operacion deseada (1 al 4)"'

Antes de ejecutarse la primera línea del script la imprime. De esta manera sabemos exactamente lo que ha ejecutado. Para distinguirlo incluye un «+» al comienzo de la línea.
Indicando la depuración en la orden de ejecución
Otra forma de ejecutar la depuración es indicando la opción en la misma orden de ejecución del script, en vez de incluirla en el código. Lo haremos con la siguiente sintaxis y el mismo resultado
bash -x script.sh
Y obtenemos la misma salida que antes
+ sum=1 + i=y script.sh: línea 21: error sintáctico cerca del elemento inesperado `(' script.sh: línea 21: ` echo "Introduce el numero de operacion deseada (1 al 4)"'
La opción -e
Disponemos de dos opciones que hemos comentado antes «-e» y «-v». La opción «-e» conseguirá que cuando detecte un error, es decir, devuelva un valor distinto de «0», trunque la ejecución y salga. Lo que nos permitirá determinar donde esta el fallo. Lo vemos con un ejemplo, primero sin la opción y luego con ella. Creamos un fichero con el siguiente contenido. Es un script que nos dirá si el argumento es numérico, alfabético o alfanumérico.
nano numero.sh
#!/bin/bash if test $# -ne 1 then echo "debe haber un argumento" exit 1 fi numero=$(echo $1 | grep "^[0-9][0-9]*$") letra=$(echo $1 | grep "^[a-zA-Z][a-zA-Z]*$") if [ $numero ] then echo $1 es un valor numérico" elif [ $letra ] then echo "$1 es una valor alfabético" else echo "$1 es un valor alfanumérico" exit 2 fi exit 0

Le damos permisos de ejecución con «chmod +x numero.sh».
Al ejecutar el script con la opción «-x» sola, obtenemos la siguiente salida
bash -x numero.sh 123
javier@debian:~/Pruebas$ bash -x numero.sh 123 + test 1 -ne 1 ++ echo 123 ++ grep '^[0-9][0-9]*$' + numero=123 ++ echo 123 ++ grep '^[a-zA-Z][a-zA-Z]*$' + letra= numero.sh: línea 13: EOF inesperado mientras se buscaba un `"' coincidente numero.sh: línea 17: error sintáctico: no se esperaba el final del fichero javier@debian:~/Pruebas$
Ahora vemos las diferencias al ejecutar el script con las opciónes «-xe».
bash -xe numero.sh 123
javier@debian:~/Pruebas$ bash -xe numero.sh 123 + test 1 -ne 1 ++ echo 123 ++ grep '^[0-9][0-9]*$' + numero=123 ++ echo 123 ++ grep '^[a-zA-Z][a-zA-Z]*$' + letra= javier@debian:~/Pruebas$

Como ves al ejecutar la linea «echo $1 es un valor numérico» y faltar las comillas dobles iniciales, termina la ejecución del script. Ya hemos localizado el error.
Las opciones las puedes utilizar juntas o separadas.
bash -x -e script.sh #o bash -xe script.sh #o bash -ex script.sh
El resultado será el mismo.
La opción -v
La opción «-x», como hemos visto, expande los valores, pero si utilizamos la opción «-v», nos imprimirá todas las líneas en el orden que las vaya leyendo, sin expandirlas.
Si ejecutamos el último ejemplo que hemos visto «bash -xv numero.sh 123», el resultado será el siguiente,
javier@debian:~/Pruebas$ bash -xv numero.sh 123 #!/bin/bash if test $# -ne 1 then echo "debe haber un argumento" exit 1 fi + test 1 -ne 1 numero=$(echo $1 | grep "^[0-9][0-9]*$") ++ echo 123 ++ grep '^[0-9][0-9]*$' + numero=123 letra=$(echo $1 | grep "^[a-zA-Z][a-zA-Z]*$") ++ echo 123 ++ grep '^[a-zA-Z][a-zA-Z]*$' + letra= if [ $numero ] then echo $1 es un valor numérico" elif [ $letra ] then echo "$1 es una valor alfabético" else echo "$1 es un valor alfanumérico" exit 2 fi exit 0 numero.sh: línea 13: EOF inesperado mientras se buscaba un «"» coincidente numero.sh: línea 17: error sintáctico: no se esperaba el final del archivo javier@debian:~/Pruebas$

De esta forma podemos repasar todas las lineas de código del script y ver como es tratada por el intérprete.
Depuración por partes del código
Todo esto vendrá muy bien cuando tenemos poco código que depurar, pero también podemos depurarlo por partes, estableciendo unos cortes. Para ello utilizamos la herramienta «set» con las opciones «-x», para iniciar el corte y «+x» para terminarlo. Lo vemos con un ejemplo. Modificamos el código para incluir el corte,
#!/bin/bash if test $# -ne 1 then echo "debe haber un argumento" exit 1 else echo "El argumento es correcto" fi set -x # Iniciamos el corte numero=$(echo $1 | grep "^[0-9][0-9]*$") letra=$(echo $1 | grep "^[a-zA-Z][a-zA-Z]*$") if [ $numero ] then echo $1 es un valor numérico" elif [ $letra ] then echo "$1 es una valor alfabético" else echo "$1 es un valor alfanumérico" exit 2 fi set +x # cerramos el corte exit 0
Si ahora ejecutas el script con «bash numero.sh» encontrarás el siguiente resultado,
javier@debian:~/Pruebas$ bash numero.sh 123 El argumento es correcto ++ echo 123 ++ grep '^[0-9][0-9]*$' + numero=123 ++ echo 123 ++ grep '^[a-zA-Z][a-zA-Z]*$' + letra= numero.sh: línea 16: EOF inesperado mientras se buscaba un `"' coincidente numero.sh: línea 21: error sintáctico: no se esperaba el final del fichero javier@debian:~/Pruebas$

Se ejecuta el programa y, al llegar al corte y encontrar el error, sale del programa. Y nos indica el error y la linea donde esta.
El interprete imprimirá todas las instrucciones entre «set -x» y «set +x».
También podemos usar «set -v» para hacer un «echo» por la salida estandar con todas las órdenes que se ejecutan.
javier@debian:~/Pruebas$ var=3 ; set -v ; echo $var ; echo $var var=3 ; set -v ; echo $var ; echo $var 3 3 javier@debian:~/Pruebas$

Es previsible que sepamos por donde buscar el error, en cuyo caso deberemos ejecutar la sentencia justo antes de que comience el código problemático y revertir el efecto cuando ya sepamos que ha pasado.
Y si ejecutas «set -x», te mostrará la orden ejecutada con las sustituciones hechas
javier@debian:~/Pruebas$ var=3 ; set -x ; echo $var ; echo $var var=3 ; set -x ; echo $var ; echo $var + echo 3 3 + echo 3 3 javier@debian:~/Pruebas$

Con estas herramientas de depuración encontrarémos y evitarémos errores en nuestro código, y nos facilitará la vida.
Ejecución paso a paso
Otra opción disponible es emular la ejecución paso a paso disponible para otros lenguajes con el argumento «DEBUG» con «trap». Para ello creamos una librería de funciones con tres de ellas, y las llamamos desde nuestro script. Crea el fichero «libreria-depuracion.sh» con el siguiente contenido:
nano libreria-depuracion.sh
Copia y pega el siguiente código
# debugger.sh: funciones para depuración de código debug() { [ $__untrap -eq 1 ] && return echo "Línea $1: $BASH_COMMAND" while true; do read -p "> " comm [ -z "$comm" ] || [ "$comm" = "n" ] && return [ "$comm" = "c" ] && __untrap=1 && return eval $comm done } breakpoint() { __untrap=0 trap 'debug $LINENO' DEBUG } continue { trap '' DEBUG }

Dale permisos de ejecución con «chmod +x libreria-depuracion.sh»
Solo necesitaremos llamar a la libreria y añadir la sentencia «breakpoint» donde queramos crear un punto de ruptura y que empiece la ejecución paso a paso, y «continue» donde queramos que la ejecución prosiga de forma normal.
Y modificamos el contenido de «numero.sh»
#!/bin/bash . libreria-depuracion.sh if test $# -ne 1 then echo "debe haber un argumento" exit 1 else echo "El argumento es correcto" fi source ./libreria-depuracion.sh breakpoint numero=$(echo $1 | grep "^[0-9][0-9]*$") letra=$(echo $1 | grep "^[a-zA-Z][a-zA-Z]*$") if [ $numero ] then echo "$1 es un valor numérico" elif [ $letra ] then echo "$1 es una valor alfabético" else echo "$1 es un valor alfanumérico" exit 2 fi exit 0

Al ejecutarlo, obtenemos la salida
javier@debian:~/Pruebas$ bash numero.sh 123 bash numero.sh 123 + bash numero.sh 123 libreria-depuracion.sh: línea 18: continue: solo tiene significado en un bucle `for', `while', o `until' libreria-depuracion.sh: línea 20: error sintáctico cerca del elemento inesperado `}' libreria-depuracion.sh: línea 20: `}' El argumento es correcto ./libreria-depuracion.sh: línea 18: continue: solo tiene significado en un bucle `for', `while', o `until' ./libreria-depuracion.sh: línea 20: error sintáctico cerca del elemento inesperado `}' ./libreria-depuracion.sh: línea 20: `}' Línea 15: numero=$(echo $1 | grep "^[0-9][0-9]*$") > Línea 16: letra=$(echo $1 | grep "^[a-zA-Z][a-zA-Z]*$") > numero.sh: línea 22: EOF inesperado mientras se buscaba un `"' coincidente numero.sh: línea 26: error sintáctico: no se esperaba el final del fichero javier@debian:~/Pruebas$

Al pararse la ejecución, podemos:
- Presionar «Enter» o escribir «n» para ejecutar la línea y avanzar a la siguiente.
- Escribir «c» para que la ejecución continúe sin más paradas hasta el próximo breakpoint.
- Evaluar cualquier órden, sin avanzar.
Tienes más información sobre el uso de librerías aquí.
Si tienes algún comentario que hacer sobre este artículo, al pie del post tienes un formulario para hacerlo.
Si quieres contactar conmigo por cualquier otro asunto relacionado con el sitio, en la página de contacto, tienes un formulario más adecuado.
Y para suscribirte y recibir las novedades publicadas, tienes un enlace en pie de la página o desde aquí mismo.