Come rilevare un errore in uno script bash di Linux?


13

Ho realizzato il seguente 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"

La sceneggiatura funziona, ma accanto ai miei echi, c'è anche l'output quando

cd $1

fallisce nell'esecuzione.

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

È possibile catturarlo?


Solo un FYI, puoi anche farlo molto più semplice; test -d /path/to/directory(o [[ -d /path/to/directory ]]in bash) ti dirà se un determinato target è una directory o meno, e lo farà tranquillamente.
Patrick,

@Patrick, che verifica se si tratta di una directory, non se riesci ad accedervi cd.
Stéphane Chazelas,

@StephaneChazelas yes. Il nome della funzione è directoryExists.
Patrick,

Vedi una risposta dettagliata qui: solleva l'errore in uno script Bash .
codeforester,

Risposte:


8

Lo script cambia directory durante l'esecuzione, il che significa che non funzionerà con una serie di nomi di percorso relativi. Successivamente hai commentato che volevi solo verificare l'esistenza della directory, non la possibilità di utilizzare cd, quindi le risposte non devono assolutamente essere utilizzate cd. Revised. Utilizzo tput e colori di 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
}

(Modificato per usare il più invulnerabile printfinvece del problematico echoche potrebbe agire sulle sequenze di escape nel testo.)


Ciò risolve anche (a meno che xpg_echo non sia attivo) i problemi quando i nomi dei file contengono caratteri di barra rovesciata.
Stéphane Chazelas,

12

Utilizzare set -eper impostare la modalità Exit-On-Error: se un semplice comando restituisce uno stato diverso da zero (che indica un errore), la shell viene chiusa.

Attenzione che set -enon sempre calciare in. I comandi in posizioni di prova sono autorizzati a fallire (ad esempio if failing_command, failing_command || fallback). I comandi in subshell portano solo a uscire dalla subshell, non dal genitore: set -e; (false); echo foovisualizza foo.

In alternativa, o in aggiunta, in bash (e ksh e zsh, ma non in sh), è possibile specificare un comando che viene eseguito nel caso in cui un comando restituisca uno stato diverso da zero, con la ERRtrap, ad es trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Si noti che in casi del genere (false); …, la trap ERR viene eseguita nella subshell, quindi non può causare la chiusura del genitore.


Di recente ho sperimentato un po 'e ho scoperto un modo conveniente per correggere il ||comportamento, che consente di eseguire facilmente la corretta gestione degli errori senza usare trap. Vedere la mia risposta . Cosa ne pensi di quel metodo?
skozin,

@ sam.kozin Non ho tempo di rivedere in dettaglio la tua risposta, in linea di principio sembra buona. Oltre alla portabilità, quali sono i vantaggi rispetto alla trap ERR di ksh / bash / zsh?
Gilles 'SO- smetti di essere malvagio' l'

Probabilmente l'unico vantaggio è la componibilità, poiché non rischi di sovrascrivere un'altra trappola impostata prima che la funzione venga eseguita. Che è una funzione utile quando scrivi alcune funzioni comuni che in seguito otterrai e utilizzerai da altri script. Un altro vantaggio potrebbe essere la piena compatibilità POSIX, sebbene non sia così importante in quanto lo ERRpseudo-segnale è supportato in tutte le principali shell. Grazie per la recensione! =)
skozin,

@ sam.kozin Ho dimenticato di scrivere nel mio commento precedente: potresti voler pubblicare questo su Code Review e pubblicare un link nella chatroom .
Gilles 'SO- smetti di essere malvagio' l'

Grazie per il suggerimento, proverò a seguirlo. Non sapevo di Code Review.
skozin,

6

Per espandere la risposta di @Gilles :

In effetti, set -enon funziona all'interno dei comandi se si utilizza l' ||operatore dopo di loro, anche se li si esegue in una subshell; ad esempio, questo non funzionerebbe:

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

Ma l' ||operatore è necessario per impedire il ritorno dalla funzione esterna prima della pulizia.

C'è un piccolo trucco che può essere usato per risolvere questo problema: esegui il comando interno in background, quindi attendi immediatamente. Il waitbuiltin restituirà il codice di uscita del comando interno, e ora stai usando ||dopo wait, non la funzione interna, quindi set -efunziona correttamente all'interno di quest'ultimo:

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

Ecco la funzione generica che si basa su questa idea. Dovrebbe funzionare in tutte le shell compatibili con POSIX se rimuovete le localparole chiave, ovvero sostituite tutte 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
}

Esempio di utilizzo:

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

Eseguendo l'esempio:

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

L'unica cosa di cui è necessario essere consapevoli quando si utilizza questo metodo è che tutte le modifiche delle variabili Shell eseguite dal comando a cui si passa runnon si propagheranno alla funzione chiamante, poiché il comando viene eseguito in una subshell.


2

Non dici cosa intendi esattamente per catch--- segnala e continua; interrompere l'ulteriore elaborazione?

Poiché cdrestituisce uno stato diverso da zero in caso di errore, è possibile eseguire:

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

Potresti semplicemente uscire in caso di fallimento:

cd -- "$1" || exit 1

Oppure, fai eco al tuo messaggio ed esci:

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

E / o sopprimere l'errore fornito da cdin caso di errore:

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

In base agli standard, i comandi dovrebbero inserire messaggi di errore su STDERR (descrittore di file 2). Così 2>/dev/nulldice reindirizzare STDERR al "bit-bucket" conosciuto da /dev/null.

(non dimenticare di citare le variabili e contrassegnare la fine delle opzioni per cd).


@Stephane Chazelas punto di quotazione e segnalazione fine opzioni ben preso. Grazie per il montaggio.
JRFerguson,

1

In realtà per il tuo caso direi che la logica può essere migliorata.

Invece di cd e poi controlla se esiste, controlla se esiste quindi vai nella directory.

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

Ma se il tuo scopo è quello di mettere a tacere i possibili errori cd -- "$1" 2>/dev/null, allora questo ti renderà più difficile il debug in futuro. Puoi verificare i flag if test su: Bash se documentazione :


Questa risposta non riesce a citare la $1variabile e fallirà se quella variabile contiene spazi vuoti o altri metacaratteri della shell. Inoltre, non riesce a verificare se l'utente dispone dell'autorizzazione per cdaccedervi.
Ian D. Allen,

In realtà stavo cercando di verificare se esistesse una determinata directory, non necessariamente cd. Ma poiché non sapevo di meglio, ho pensato che provare a fare cd su di esso avrebbe causato un errore se non esistesse, quindi perché non prenderlo? Non sapevo se [-d $ 1] fosse esattamente quello di cui avevo bisogno. Grazie mille! (Sono abituato a precaricare Java e verificare la presenza di una directory in un'istruzione if non è esattamente comune in Java)
Thomas De Wilde
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.