web-dev-qa-db-fra.com

Comment intercepter une erreur dans un script bash linux?

J'ai fait le script suivant:

# !/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"

Le script fonctionne, mais à côté de mes échos, il y a aussi la sortie lorsque

cd $1

échoue à l'exécution.

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

Est-il possible d'attraper cela?

13
Thomas De Wilde

Votre script modifie les répertoires lors de son exécution, ce qui signifie qu'il ne fonctionnera pas avec une série de chemins d'accès relatifs. Vous avez ensuite indiqué plus tard que vous vouliez uniquement vérifier l'existence du répertoire, pas la possibilité d'utiliser cd, donc les réponses n'ont pas du tout besoin d'utiliser cd. Modifié. Utilisation de tput et des couleurs 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
}

(Modifié pour utiliser le printf plus invulnérable au lieu du echo problématique qui pourrait agir sur les séquences d'échappement dans le texte.)

8
Ian D. Allen

Utilisez set -e Pour définir le mode exit-on-error: si une simple commande renvoie un état différent de zéro (indiquant un échec), le shell se ferme.

Attention, set -e Ne se déclenche pas toujours. Les commandes dans les positions de test peuvent échouer (par exemple if failing_command, failing_command || fallback). Les commandes dans le sous-shell conduisent uniquement à quitter le sous-shell, pas le parent: set -e; (false); echo foo affiche foo.

Alternativement, ou en plus, dans bash (et ksh et zsh, mais pas plain sh), vous pouvez spécifier une commande qui est exécutée au cas où une commande retourne un état différent de zéro, avec le piège ERR, par exemple trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Notez que dans des cas comme (false); …, Le trap ERR est exécuté dans le sous-shell, il ne peut donc pas provoquer la sortie du parent.

Pour développer la @ réponse de Gilles :

En effet, set -e ne fonctionne pas dans les commandes si vous utilisez || opérateur après eux, même si vous les exécutez en sous-shell; par exemple, cela ne fonctionnerait pas:

#!/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

Mais || L'opérateur est nécessaire pour empêcher le retour de la fonction externe avant le nettoyage.

Il existe une petite astuce qui peut être utilisée pour résoudre ce problème: exécutez la commande interne en arrière-plan, puis attendez-la immédiatement. La fonction intégrée wait renverra le code de sortie de la commande interne, et maintenant vous utilisez || après wait, pas la fonction interne, donc set -e fonctionne correctement à l'intérieur de ce dernier:

#!/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

Voici la fonction générique qui s'appuie sur cette idée. Il devrait fonctionner dans tous les shells compatibles POSIX si vous supprimez les mots clés local, c'est-à-dire remplacer tous les local x=y avec juste 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
}

Exemple d'utilisation:

#!/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 "$@"

Exécuter l'exemple:

$ ./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

La seule chose dont vous devez être conscient lorsque vous utilisez cette méthode est que toutes les modifications des variables Shell effectuées à partir de la commande que vous passez à run ne se propageront pas à la fonction appelante, car la commande s'exécute dans un sous-shell.

6
skozin

Vous ne dites pas exactement ce que vous entendez par catch --- rapportez et continuez; abandonner le traitement ultérieur?

Puisque cd renvoie un état différent de zéro en cas d'échec, vous pouvez faire:

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

Vous pouvez simplement quitter en cas d'échec:

cd -- "$1" || exit 1

Ou, faites écho à votre propre message et quittez:

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

Et/ou supprimez l'erreur fournie par cd en cas d'échec:

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

Selon les normes, les commandes devraient placer des messages d'erreur sur STDERR (descripteur de fichier 2). Donc 2>/dev/null dit rediriger STDERR vers le "bit-bucket" connu par /dev/null.

(n'oubliez pas de citer vos variables et de marquer la fin des options pour cd).

2
JRFerguson

En fait, pour votre cas, je dirais que la logique peut être améliorée.

Au lieu de cd puis vérifiez s'il existe, vérifiez s'il existe puis allez dans le répertoire.

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

Mais si votre but est de faire taire les erreurs possibles, alors cd -- "$1" 2>/dev/null, mais cela vous rendra plus difficile le débogage à l'avenir. Vous pouvez vérifier les drapeaux if testing à: Bash if documentation :

1
BitsOfNix