Wie fange ich einen Fehler in einem Linux-Bash-Skript ab?

13

Ich habe folgendes Skript erstellt:

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

Das Skript funktioniert, aber neben meinen Echos gibt es auch die Ausgabe wann

cd $1

schlägt bei der Ausführung fehl.

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

Ist es möglich, das zu fangen?

Thomas De Wilde
quelle
Nur zu Ihrer Information, Sie können dies auch viel einfacher tun; test -d /path/to/directory(oder [[ -d /path/to/directory ]]in der Bash) wird Ihnen sagen, ob ein bestimmtes Ziel ein Verzeichnis ist oder nicht, und es wird es leise tun.
Patrick
@Patrick, das testet nur, ob es sich um ein Verzeichnis handelt, nicht, wenn Sie hinein können cd.
Stéphane Chazelas
@StephaneChazelas ja. Der Funktionsname ist directoryExists.
Patrick
Eine ausführliche Antwort finden Sie hier: Fehler in einem Bash-Skript auslösen .
Codeforester

Antworten:

8

Ihr Skript ändert Verzeichnisse während der Ausführung, was bedeutet, dass es nicht mit einer Reihe relativer Pfadnamen funktioniert. Sie haben dann später kommentiert, dass Sie nur auf das Vorhandensein eines Verzeichnisses prüfen möchten, nicht auf die Verwendbarkeit cd, sodass Antworten überhaupt nicht verwendet werden müssen cd. Überarbeitet. Verwendung tput und Farben von 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
}

(Bearbeitet, um das Unverwundbarere printfanstelle des Problems zu verwenden echo, das bei Escape-Sequenzen im Text auftreten kann.)

Ian D. Allen
quelle
Dies behebt auch die Probleme, wenn Dateinamen Backslash-Zeichen enthalten (es sei denn, xpg_echo ist aktiviert).
Stéphane Chazelas
12

Dient set -ezum Festlegen des Exit-On-Error-Modus: Wenn ein einfacher Befehl einen Status ungleich Null zurückgibt (was auf einen Fehler hinweist), wird die Shell beendet.

Beachten Sie, dass set -enicht immer in nicht kicken. Befehle in Testpositionen dürfen nicht (zB if failing_command, failing_command || fallback). Befehle in der Subshell führen nur zum Verlassen der Subshell, nicht der übergeordneten: set -e; (false); echo fooDisplays foo.

Alternativ oder zusätzlich können Sie in bash (und ksh und zsh, aber nicht plain sh) einen Befehl angeben, der ausgeführt wird, falls ein Befehl einen Nicht-Null-Status zurückgibt, mit dem ERRTrap, z trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Beachten Sie, dass in solchen Fällen (false); …die ERR-Trap in der Subshell ausgeführt wird, sodass das übergeordnete Element nicht beendet werden kann.

Gilles 'SO - hör auf böse zu sein'
quelle
Kürzlich habe ich ein wenig experimentiert und eine bequeme Methode ||zum Beheben von Fehlern gefunden, mit der sich Fehler problemlos und ohne Verwendung von Traps beheben lassen . Siehe meine Antwort . Was denkst du über diese Methode?
Skozin
@ sam.kozin Ich habe keine Zeit, deine Antwort im Detail zu überprüfen, sie sieht grundsätzlich gut aus. Was sind neben der Portabilität die Vorteile gegenüber der ERR-Falle von ksh / bash / zsh?
Gilles 'SO- hör auf böse zu sein'
Wahrscheinlich ist der einzige Vorteil die Kompositionsfähigkeit, da Sie nicht riskieren, eine andere Falle zu überschreiben, die vor dem Ausführen der Funktion festgelegt wurde. Dies ist eine nützliche Funktion, wenn Sie eine häufig verwendete Funktion schreiben, die Sie später aus anderen Skripten beziehen und verwenden werden. Ein weiterer Vorteil könnte die vollständige POSIX-Kompatibilität sein, obwohl dies nicht so wichtig ist, da ERRPseudosignale in allen wichtigen Shells unterstützt werden. Danke für die Bewertung! =)
skozin
@ sam.kozin Ich habe vergessen, in meinem vorherigen Kommentar zu schreiben: Vielleicht möchten Sie dies auf Code Review veröffentlichen und einen Link im Chatroom veröffentlichen .
Gilles 'SO- hör auf böse zu sein'
Danke für den Vorschlag, ich werde versuchen, ihm zu folgen. Wusste nichts über Code Review.
Skozin
5

So erweitern Sie die Antwort von @Gilles :

Funktioniert in der Tat set -enicht in Befehlen, wenn Sie ||Operator nach ihnen verwenden, selbst wenn Sie sie in einer Subshell ausführen. zB würde das nicht funktionieren:

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

Der ||Bediener muss jedoch verhindern, dass die äußere Funktion vor der Bereinigung wiederhergestellt wird.

Es gibt einen kleinen Trick, der verwendet werden kann, um dies zu beheben: Führen Sie den inneren Befehl im Hintergrund aus und warten Sie dann sofort darauf. Der waiteingebaute Code gibt den Exit-Code des inneren Befehls zurück. Jetzt verwenden Sie ||after wait, nicht die innere Funktion, und set -efunktioniert in letzterer ordnungsgemäß:

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

Hier ist die generische Funktion, die auf dieser Idee aufbaut. Es sollte in allen POSIX-kompatiblen Shells funktionieren, wenn Sie localSchlüsselwörter entfernen , dh alle local x=ydurch Folgendes ersetzen 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
}

Anwendungsbeispiel:

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

Beispiel ausführen:

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

Das einzige, was Sie beachten müssen, wenn Sie diese Methode verwenden, ist, dass alle Änderungen an Shell-Variablen, die Sie mit dem übergebenen Befehl vornehmen, runnicht an die aufrufende Funktion weitergegeben werden, da der Befehl in einer Subshell ausgeführt wird.

Skozin
quelle
2

Sie sagen nicht genau, was Sie mit catch--- melden und fortfahren meinen ; weitere Bearbeitung abbrechen?

Da bei cdeinem Fehler ein Status ungleich Null zurückgegeben wird, können Sie Folgendes tun:

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

Sie können einfach bei einem Fehler beenden:

cd -- "$1" || exit 1

Oder geben Sie Ihre eigene Nachricht wieder und beenden Sie das Programm:

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

Und / oder unterdrücken Sie den Fehler, der bei einem Fehler cdauftritt:

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

Standardmäßig sollten Befehle Fehlermeldungen in STDERR (Dateideskriptor 2) einfügen. So 2>/dev/nullheißt die Umleitung von STDERR auf den "Bit-Bucket", den man kennt /dev/null.

(Vergessen Sie nicht, Ihre Variablen anzugeben und das Ende der Optionen für anzugeben. cd)

JRFerguson
quelle
@Stephane Chazelas Punkt des Zitierens und Signalisierens von Optionen gut angenommen. Vielen Dank für die Bearbeitung.
JRFerguson
1

Eigentlich würde ich für Ihren Fall sagen, dass die Logik verbessert werden kann.

Anstelle von cd und überprüfe, ob es existiert, überprüfe, ob es existiert, und gehe dann in das Verzeichnis.

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

Wenn Sie jedoch die möglichen Fehler ausschließen möchten cd -- "$1" 2>/dev/null, wird dies das Debuggen in Zukunft erschweren. Sie können überprüfen, ob Flags getestet wurden unter: Bash if documentation :

BitsOfNix
quelle
Diese Antwort zitiert die $1Variable nicht in Anführungszeichen und schlägt fehl, wenn diese Variable Leerzeichen oder andere Shell-Metazeichen enthält. Es wird auch nicht geprüft, ob der Benutzer die Berechtigung dazu cdhat.
Ian D. Allen
Ich habe tatsächlich versucht zu überprüfen, ob ein bestimmtes Verzeichnis existiert, nicht unbedingt eine CD. Aber weil ich es nicht besser wusste, dachte ich, der Versuch, eine CD zu erstellen, würde einen Fehler verursachen, wenn er nicht existiert. Warum sollte ich ihn nicht fangen? Ich wusste nicht, ob [-d $ 1] genau das ist, was ich brauchte. Also vielen Dank! (Ich bin es gewohnt, Java zu programmieren und nach einem Verzeichnis in einer if-Anweisung zu suchen, die in Java nicht unbedingt üblich ist.)
Thomas De Wilde