¿Cómo detectar un error en un script de Linux Bash?

13

Hice el siguiente script:

# !/bin/bash

# OUTPUT-COLORING
red='\e[0;31m'
green='\e[0;32m'
NC='\e[0m' # No Color

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    cd $1
    if [ $? = 0 ]
            then
                    echo -e "${green}$1${NC}"
            else
                    echo -e "${red}$1${NC}"
    fi
}

# EXE
directoryExists "~/foobar"
directoryExists "/www/html/drupal"

El script funciona, pero además de mis ecos, también está la salida cuando

cd $1

falla en la ejecución.

testscripts//test_labo3: line 11: cd: ~/foobar: No such file or directory

¿Es posible atrapar esto?

Thomas De Wilde
fuente
Solo para su información, también puede hacerlo mucho más simple; test -d /path/to/directory(o [[ -d /path/to/directory ]]en bash) le dirá si un objetivo dado es un directorio o no, y lo hará en silencio.
Patrick
@Patrick, eso solo prueba si es un directorio, no si puedes cdingresar en él.
Stéphane Chazelas
@StephaneChazelas sí. El nombre de la función es directoryExists.
Patrick
Vea una respuesta detallada aquí: Eleve el error en un script Bash .
codeforester

Respuestas:

8

Su script cambia los directorios a medida que se ejecuta, lo que significa que no funcionará con una serie de nombres de ruta relativos. Luego comentaste más tarde que solo querías verificar la existencia del directorio, no la capacidad de uso cd, por lo que las respuestas no necesitan usarse cden absoluto. Revisado. Uso tput y colores de man terminfo:

#!/bin/bash -u
# OUTPUT-COLORING
red=$( tput setaf 1 )
green=$( tput setaf 2 )
NC=$( tput setaf 0 )      # or perhaps: tput sgr0

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    # was: do the cd in a sub-shell so it doesn't change our own PWD
    # was: if errmsg=$( cd -- "$1" 2>&1 ) ; then
    if [ -d "$1" ] ; then
        # was: echo "${green}$1${NC}"
        printf "%s\n" "${green}$1${NC}"
    else
        # was: echo "${red}$1${NC}"
        printf "%s\n" "${red}$1${NC}"
        # was: optional: printf "%s\n" "${red}$1 -- $errmsg${NC}"
    fi
}

(Editado para usar lo más invulnerable en printflugar de la problemática echoque podría actuar sobre las secuencias de escape en el texto).

Ian D. Allen
fuente
Eso también soluciona (a menos que xpg_echo esté activado) los problemas cuando los nombres de archivo contienen caracteres de barra invertida.
Stéphane Chazelas
12

Se usa set -epara establecer el modo de salida en caso de error: si un comando simple devuelve un estado distinto de cero (lo que indica una falla), el shell se cierra.

Tenga en cuenta que set -eno siempre se activa. Los comandos en las posiciones de prueba pueden fallar (por ejemplo if failing_command, failing_command || fallback). Los comandos en el subshell solo conducen a salir del subshell, no el padre: set -e; (false); echo foomuestra foo.

Alternativamente, o además, en bash (y ksh y zsh, pero no sh simple), puede especificar un comando que se ejecuta en caso de que un comando devuelva un estado distinto de cero, con la ERRtrampa, por ejemplo trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Tenga en cuenta que en casos como (false); …, la trampa ERR se ejecuta en la subshell, por lo que no puede hacer que el padre salga.

Gilles 'SO- deja de ser malvado'
fuente
Recientemente experimenté un poco y descubrí una forma conveniente de corregir el ||comportamiento, que permite realizar fácilmente el manejo adecuado de errores sin usar trampas. Mira mi respuesta . ¿Qué opinas sobre ese método?
skozin
@ sam.kozin No tengo tiempo para revisar su respuesta en detalle, se ve bien por principio. Además de la portabilidad, ¿cuáles son los beneficios sobre la trampa ERR de ksh / bash / zsh?
Gilles 'SO- deja de ser malvado'
Probablemente, el único beneficio es la componibilidad, ya que no corre el riesgo de sobrescribir otra trampa que se configuró antes de ejecutar las funciones. Lo cual es una característica útil cuando está escribiendo alguna función común que luego obtendrá y utilizará desde otros scripts. Otro beneficio podría ser la compatibilidad total con POSIX, aunque no es tan importante ya que la ERRseudoseñal es compatible con todos los shells principales. Gracias por el comentario! =)
skozin
@ sam.kozin Olvidé escribir en mi comentario anterior: puede publicar esto en Code Review y publicar un enlace en la sala de chat .
Gilles 'SO- deja de ser malvado'
Gracias por la sugerencia, intentaré seguirla. No sabía sobre la revisión de código.
skozin
6

Para ampliar la respuesta de @Gilles :

De hecho, set -eno funciona dentro de los comandos si usa el ||operador después de ellos, incluso si los ejecuta en una subshell; por ejemplo, esto no funcionaría:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Pero el ||operador es necesario para evitar el regreso de la función externa antes de la limpieza.

Hay un pequeño truco que se puede utilizar para solucionar esto: ejecute el comando interno en segundo plano y luego espere inmediatamente. El waitbuiltin devolverá el código de salida del comando interno, y ahora está usando ||después wait, no la función interna, por lo que set -efunciona correctamente dentro de este último:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Aquí está la función genérica que se basa en esta idea. Debería funcionar en todos los shells compatibles con POSIX si elimina las localpalabras clave, es decir, reemplace todo local x=ycon solo x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Ejemplo de uso:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Ejecutando el ejemplo:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

Lo único que debe tener en cuenta al usar este método es que todas las modificaciones de las variables de Shell realizadas desde el comando al que pasa runno se propagarán a la función de llamada, porque el comando se ejecuta en una subshell.

skozin
fuente
2

No dice exactamente qué quiere decir con catch--- informe y continúe; abortar procesamiento adicional?

Como cddevuelve un estado distinto de cero en caso de error, puede hacer lo siguiente:

cd -- "$1" && echo OK || echo NOT_OK

Simplemente puede salir en caso de falla:

cd -- "$1" || exit 1

O haga eco de su propio mensaje y salga:

cd -- "$1" || { echo NOT_OK; exit 1; }

Y / o suprimir el error proporcionado por cden caso de falla:

cd -- "$1" 2>/dev/null || exit 1

Según los estándares, los comandos deben colocar mensajes de error en STDERR (descriptor de archivo 2). Por lo tanto, 2>/dev/nulldice redirigir STDERR al "bit-bucket" conocido por /dev/null.

(no olvide citar sus variables y marcar el final de las opciones para cd).

JRFerguson
fuente
@Stephane Chazelas punto de cita y señalización de fin de opciones bien tomado. Gracias por editar
JRFerguson
1

En realidad para su caso, diría que la lógica se puede mejorar.

En lugar de cd y luego verifique si existe, verifique si existe y luego vaya al directorio.

if [ -d "$1" ]
then
     printf "${green}${NC}\\n" "$1"
     cd -- "$1"
else 
     printf "${red}${NC}\\n" "$1"
fi  

Pero si su propósito es silenciar los posibles errores cd -- "$1" 2>/dev/null, entonces , esto hará que depurar en el futuro sea más difícil. Puede verificar los indicadores if testing en: Bash if documentación :

BitsOfNix
fuente
Esta respuesta no cita la $1variable y fallará si esa variable contiene espacios en blanco u otros metacaracteres de shell. Tampoco verifica si el usuario tiene permiso para cdingresar.
Ian D. Allen
En realidad estaba tratando de verificar si existía un determinado directorio, no necesariamente un CD. Pero debido a que no lo sabía mejor, pensé que tratar de hacerlo podría causar un error si no existía, ¿por qué no detectarlo? No sabía acerca de si [-d $ 1] eso es exactamente lo que necesitaba. ¡Muchas gracias! (Estoy acostumbrado a programar Java, y buscar un directorio en una declaración if no es exactamente común en Java)
Thomas De Wilde