Comment intercepter une erreur dans un script bash linux?

13

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 quand

cd $1

échoue à l'exécution.

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

Est-il possible d'attraper cela?

Thomas De Wilde
la source
Juste un FYI, vous pouvez également le faire beaucoup plus simple; test -d /path/to/directory(ou [[ -d /path/to/directory ]]en bash) vous dira si une cible donnée est un répertoire ou non, et il le fera tranquillement.
Patrick
@Patrick, qui teste simplement si c'est un répertoire, pas si vous le pouvez cd.
Stéphane Chazelas
@StephaneChazelas oui. Le nom de la fonction est directoryExists.
Patrick
Voir une réponse détaillée ici: Générer une erreur dans un script Bash .
codeforester

Réponses:

8

Votre script change de répertoire au fur et à mesure 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 commenté plus tard que vous vouliez seulement vérifier l'existence du répertoire, pas la capacité à utiliser cd, donc les réponses n'ont pas du tout besoin d'être utilisées cd. Modifié. Utilisation tput et 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 plus invulnérable printfau lieu de la problématique echoqui pourrait agir sur les séquences d'échappement dans le texte.)

Ian D. Allen
la source
Cela corrige également (sauf si xpg_echo est activé) les problèmes lorsque les noms de fichiers contiennent des barres obliques inverses.
Stéphane Chazelas
12

Utilisez set -epour 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, ce set -en'est pas toujours le cas. 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 foos'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 ERRpiège, 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.

Gilles 'SO- arrête d'être méchant'
la source
Récemment, j'ai expérimenté un peu et découvert un moyen pratique de corriger le ||comportement, ce qui permet de gérer facilement les erreurs sans utiliser de pièges. Voir ma réponse . Que pensez-vous de cette méthode?
skozin
@ sam.kozin Je n'ai pas le temps de revoir votre réponse en détail, elle semble bonne par principe. Outre la portabilité, quels sont les avantages par rapport au piège ERR de ksh / bash / zsh?
Gilles 'SO- arrête d'être méchant'
Le seul avantage est probablement la composabilité, car vous ne risquez pas d'écraser un autre piège qui a été défini avant l'exécution de la fonction. Ce qui est une fonctionnalité utile lorsque vous écrivez une fonction courante que vous allez ultérieurement source et utiliser à partir d'autres scripts. Un autre avantage pourrait être la compatibilité POSIX complète, bien qu'il ne soit pas aussi important que le ERRpseudo-signal est pris en charge dans tous les shells principaux. Merci pour la revue! =)
skozin
@ sam.kozin J'ai oublié d'écrire dans mon commentaire précédent: vous voudrez peut-être publier ceci sur Code Review et publier un lien dans le salon de discussion .
Gilles 'SO- arrête d'être méchant'
Merci pour la suggestion, je vais essayer de la suivre. Ne connaissait pas la révision du code.
skozin
6

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

En effet, set -ene fonctionne pas à l'intérieur des commandes si vous utilisez l' ||opérateur après elles, même si vous les exécutez dans un 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 waitintégrée renverra le code de sortie de la commande interne, et maintenant vous utilisez ||après wait, pas la fonction interne, donc set -efonctionne correctement à l'intérieur de cette dernière:

#!/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. Cela devrait fonctionner dans tous les shells compatibles POSIX si vous supprimez des localmots clés, c'est-à-dire remplacez tout local x=ypar 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 à laquelle vous passez runne se propageront pas à la fonction appelante, car la commande s'exécute dans un sous-shell.

skozin
la source
2

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

Depuis cdretourne un état non nul 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 supprimer l'erreur fournie par cden cas d'échec:

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

Selon les normes, les commandes doivent placer des messages d'erreur sur STDERR (descripteur de fichier 2). Ainsi 2>/dev/nulldit 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).

JRFerguson
la source
@Stephane Chazelas point de cotation et de signalisation de fin d'options bien pris. Merci d'avoir édité.
JRFerguson
1

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 cd -- "$1" 2>/dev/null, cela vous rendra plus difficile le débogage à l'avenir. Vous pouvez vérifier les drapeaux if testing sur: Bash if documentation :

BitsOfNix
la source
Cette réponse ne parvient pas à citer la $1variable et échouera si cette variable contient des blancs ou d'autres métacaractères shell. Il ne parvient pas non plus à vérifier si l'utilisateur y est autorisé cd.
Ian D. Allen
J'essayais en fait de vérifier si un certain répertoire existait, pas nécessairement un CD. Mais parce que je ne savais pas mieux, je pensais qu'essayer de créer un CD provoquerait une erreur s'il n'existait pas, alors pourquoi ne pas l'attraper? Je ne savais pas si le [-d $ 1] était exactement ce dont j'avais besoin. Merci beaucoup! (J'ai l'habitude de proram Java, et la recherche d'un répertoire dans une instruction if n'est pas exactement commune en Java)
Thomas De Wilde