Kesalahan penanganan di Bash

240

Apa metode favorit Anda untuk menangani kesalahan di Bash? Contoh terbaik penanganan kesalahan yang saya temukan di web ditulis oleh William Shotts, Jr di http://www.linuxcommand.org .

Dia menyarankan menggunakan fungsi berikut untuk penanganan kesalahan di Bash:

#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>

PROGNAME=$(basename $0)

function error_exit
{

#   ----------------------------------------------------------------
#   Function for exit due to fatal program error
#       Accepts 1 argument:
#           string containing descriptive error message
#   ---------------------------------------------------------------- 

    echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

Apakah Anda memiliki rutinitas penanganan kesalahan yang lebih baik yang Anda gunakan dalam skrip Bash?

Noob
sumber
1
Lihat jawaban terperinci ini: Naikkan kesalahan dalam skrip Bash .
codeforester
1
Lihat implementasi logging dan penanganan kesalahan di sini: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Jawaban:

154

Gunakan perangkap!

tempfiles=( )
cleanup() {
  rm -f "${tempfiles[@]}"
}
trap cleanup 0

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

... lalu, setiap kali Anda membuat file sementara:

temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )

dan $temp_fooakan dihapus saat keluar, dan nomor baris saat ini akan dicetak. ( set -ejuga akan memberi Anda perilaku exit-on-error, meskipun disertai dengan peringatan serius dan memperlemah prediktabilitas dan portabilitas kode).

Anda dapat membiarkan trap memanggil errorAnda (dalam hal ini ia menggunakan kode keluar default 1 dan tidak ada pesan) atau menyebutnya sendiri dan memberikan nilai eksplisit; misalnya:

error ${LINENO} "the foobar failed" 2

akan keluar dengan status 2, dan memberikan pesan eksplisit.

Charles Duffy
sumber
4
@ demo, kapitalisasi variabel disengaja. All-caps konvensional hanya untuk variabel bawaan shell dan lingkungan - menggunakan huruf kecil untuk semua hal lain mencegah konflik namespace. Lihat juga stackoverflow.com/questions/673055/…
Charles Duffy
1
sebelum Anda memecahkannya lagi, uji perubahan Anda. Konvensi adalah hal yang baik, tetapi kedua karena kode berfungsi.
Draemon,
3
@Raemon, saya sebenarnya tidak setuju. Jelas kode yang rusak diperhatikan dan diperbaiki. Kode praktik buruk tetapi sebagian besar bekerja berfungsi selamanya (dan diperbanyak).
Charles Duffy
1
tapi kamu tidak menyadarinya. Kode rusak mendapat perhatian karena kode yang berfungsi adalah perhatian utama.
Draemon
5
itu tidak benar-benar serampangan ( stackoverflow.com/a/10927223/26334 ) dan jika kode tersebut sudah tidak kompatibel dengan POSIX menghapus kata kunci fungsi tidak membuatnya lagi bisa berjalan di bawah POSIX sh, tapi poin utama saya adalah bahwa Anda ve (IMO) mendevaluasi jawaban dengan melemahkan rekomendasi untuk menggunakan set -e. Stackoverflow bukan tentang kode "Anda", ini tentang memiliki jawaban terbaik.
Draemon
123

Itu solusi yang bagus. Saya hanya ingin menambahkan

set -e

sebagai mekanisme kesalahan yang belum sempurna. Ini akan segera menghentikan skrip Anda jika perintah sederhana gagal. Saya pikir ini seharusnya menjadi perilaku default: karena kesalahan seperti itu hampir selalu menandakan sesuatu yang tidak terduga, itu tidak benar-benar 'waras' untuk terus menjalankan perintah berikut.

Bruno De Fraine
sumber
29
set -ebukan tanpa Gotcha : Lihat mywiki.wooledge.org/BashFAQ/105 untuk beberapa
Charles Duffy
3
@CharlesDuffy, beberapa gotcha dapat diatasi denganset -o pipefail
hobs
7
@CharlesDuffy Terima kasih telah menunjuk ke gotcha; secara keseluruhan, saya masih berpikir set -ememiliki rasio biaya-manfaat yang tinggi.
Bruno De Fraine
3
@BrunoDeFraine Saya menggunakan set -ediri saya sendiri, tetapi sejumlah pelanggan tetap lainnya di irc.freenode.org # bash menyarankan (dalam istilah yang cukup kuat) menentangnya. Minimal, para gotcha yang dimaksud harus dipahami dengan baik.
Charles Duffy
3
set -e -o pipefail -u # dan tahu apa yang Anda lakukan
Sam Watkins
78

Membaca semua jawaban di halaman ini sangat menginspirasi saya.

Jadi, inilah petunjuk saya:

konten file: lib.trap.sh

lib_name='trap'
lib_version=20121026

stderr_log="/dev/shm/stderr.log"

#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

if test "${g_libs[$lib_name]+_}"; then
    return 0
else
    if test ${#g_libs[@]} == 0; then
        declare -A g_libs
    fi
    g_libs[$lib_name]=$lib_version
fi


#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

exec 2>"$stderr_log"


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function exit_handler ()
{
    local error_code="$?"

    test $error_code == 0 && return;

    #
    # LOCAL VARIABLES:
    # ------------------------------------------------------------------
    #    
    local i=0
    local regex=''
    local mem=''

    local error_file=''
    local error_lineno=''
    local error_message='unknown'

    local lineno=''


    #
    # PRINT THE HEADER:
    # ------------------------------------------------------------------
    #
    # Color the output if it's an interactive terminal
    test -t 1 && tput bold; tput setf 4                                 ## red bold
    echo -e "\n(!) EXIT HANDLER:\n"


    #
    # GETTING LAST ERROR OCCURRED:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    #
    # Read last file from the error log
    # ------------------------------------------------------------------
    #
    if test -f "$stderr_log"
        then
            stderr=$( tail -n 1 "$stderr_log" )
            rm "$stderr_log"
    fi

    #
    # Managing the line to extract information:
    # ------------------------------------------------------------------
    #

    if test -n "$stderr"
        then        
            # Exploding stderr on :
            mem="$IFS"
            local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
            IFS=':'
            local stderr_parts=( $shrunk_stderr )
            IFS="$mem"

            # Storing information on the error
            error_file="${stderr_parts[0]}"
            error_lineno="${stderr_parts[1]}"
            error_message=""

            for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
                do
                    error_message="$error_message "${stderr_parts[$i-1]}": "
            done

            # Removing last ':' (colon character)
            error_message="${error_message%:*}"

            # Trim
            error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
    fi

    #
    # GETTING BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
    _backtrace=$( backtrace 2 )


    #
    # MANAGING THE OUTPUT:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    local lineno=""
    regex='^([a-z]{1,}) ([0-9]{1,})$'

    if [[ $error_lineno =~ $regex ]]

        # The error line was found on the log
        # (e.g. type 'ff' without quotes wherever)
        # --------------------------------------------------------------
        then
            local row="${BASH_REMATCH[1]}"
            lineno="${BASH_REMATCH[2]}"

            echo -e "FILE:\t\t${error_file}"
            echo -e "${row^^}:\t\t${lineno}\n"

            echo -e "ERROR CODE:\t${error_code}"             
            test -t 1 && tput setf 6                                    ## white yellow
            echo -e "ERROR MESSAGE:\n$error_message"


        else
            regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
            if [[ "$_backtrace" =~ $regex ]]

                # The file was found on the log but not the error line
                # (could not reproduce this case so far)
                # ------------------------------------------------------
                then
                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    echo -e "ERROR MESSAGE:\n${stderr}"

                # Neither the error line nor the error file was found on the log
                # (e.g. type 'cp ffd fdf' without quotes wherever)
                # ------------------------------------------------------
                else
                    #
                    # The error file is the first on backtrace list:

                    # Exploding backtrace on newlines
                    mem=$IFS
                    IFS='
                    '
                    #
                    # Substring: I keep only the carriage return
                    # (others needed only for tabbing purpose)
                    IFS=${IFS:0:1}
                    local lines=( $_backtrace )

                    IFS=$mem

                    error_file=""

                    if test -n "${lines[1]}"
                        then
                            array=( ${lines[1]} )

                            for (( i=2; i<${#array[@]}; i++ ))
                                do
                                    error_file="$error_file ${array[$i]}"
                            done

                            # Trim
                            error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
                    fi

                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    if test -n "${stderr}"
                        then
                            echo -e "ERROR MESSAGE:\n${stderr}"
                        else
                            echo -e "ERROR MESSAGE:\n${error_message}"
                    fi
            fi
    fi

    #
    # PRINTING THE BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 7                                            ## white bold
    echo -e "\n$_backtrace\n"

    #
    # EXITING:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 4                                            ## red bold
    echo "Exiting!"

    test -t 1 && tput sgr0 # Reset terminal

    exit "$error_code"
}
trap exit_handler EXIT                                                  # ! ! ! TRAP EXIT ! ! !
trap exit ERR                                                           # ! ! ! TRAP ERR ! ! !


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function backtrace
{
    local _start_from_=0

    local params=( "$@" )
    if (( "${#params[@]}" >= "1" ))
        then
            _start_from_="$1"
    fi

    local i=0
    local first=false
    while caller $i > /dev/null
    do
        if test -n "$_start_from_" && (( "$i" + 1   >= "$_start_from_" ))
            then
                if test "$first" == false
                    then
                        echo "BACKTRACE IS:"
                        first=true
                fi
                caller $i
        fi
        let "i=i+1"
    done
}

return 0



Contoh penggunaan:
konten file: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

echo "doing something wrong now .."
echo "$foo"

exit 0


Berlari:

bash trap-test.sh

Keluaran:

doing something wrong now ..

(!) EXIT HANDLER:

FILE:       trap-test.sh
LINE:       6

ERROR CODE: 1
ERROR MESSAGE:
foo:   unassigned variable

BACKTRACE IS:
1 main trap-test.sh

Exiting!


Seperti yang Anda lihat dari tangkapan layar di bawah ini, outputnya berwarna dan pesan kesalahannya muncul dalam bahasa yang digunakan.

masukkan deskripsi gambar di sini

Luca Borrione
sumber
3
hal ini luar biasa .. Anda harus membuat proyek github untuk itu, sehingga orang dapat dengan mudah melakukan perbaikan dan berkontribusi kembali. Saya menggabungkannya dengan log4bash dan bersama-sama itu menciptakan env yang kuat untuk membuat skrip bash yang baik.
Dominik Dorn
1
FYI - test ${#g_libs[@]} == 0tidak kompatibel dengan POSIX (uji POSIX mendukung =perbandingan string atau -equntuk perbandingan numerik, tetapi tidak ==, belum lagi kurangnya array dalam POSIX), dan jika Anda tidak mencoba untuk menjadi kompatibel dengan POSIX, mengapa di dunia yang Anda gunakan testsama sekali alih-alih konteks matematika? (( ${#g_libs[@]} == 0 ))bagaimanapun, lebih mudah dibaca.
Charles Duffy
2
@Luca - ini benar-benar hebat! Gambar Anda mengilhami saya untuk membuat implementasi sendiri ini, yang membuatnya bahkan beberapa langkah lebih jauh. Saya sudah mempostingnya di jawaban saya di bawah ini .
niieani
3
Bravissimo !! Ini adalah cara terbaik untuk men-debug skrip.Grazie mille Satu-satunya hal yang saya tambahkan adalah cek untuk OS X seperti ini: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy
1
Sedikit self-plug yang tidak tahu malu, tetapi kami telah mengambil cuplikan ini, membersihkannya, menambahkan lebih banyak fitur, meningkatkan pemformatan output, dan membuatnya lebih kompatibel dengan POSIX (berfungsi pada Linux dan OSX). Ini diterbitkan sebagai bagian dari Privex ShellCore di Github: github.com/Privex/shell-core
Someguy123
22

Alternatif yang setara dengan "set -e" adalah

set -o errexit

Itu membuat arti bendera agak lebih jelas daripada hanya "-e".

Tambahan acak: untuk menonaktifkan sementara flag, dan kembali ke default (melanjutkan eksekusi terlepas dari kode keluar), cukup gunakan

set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e

Ini mencegah penanganan kesalahan yang tepat yang disebutkan dalam respons lain, tetapi cepat & efektif (seperti halnya bash).

Ben Scholbrock
sumber
1
menggunakan $(foo)pada garis telanjang daripada hanya foobiasanya Hal yang Salah. Mengapa mempromosikannya dengan memberikannya sebagai contoh?
Charles Duffy
20

Terinspirasi oleh ide-ide yang disajikan di sini, saya telah mengembangkan cara yang mudah dibaca dan nyaman untuk menangani kesalahan dalam skrip bash dalam proyek bash boilerplate saya .

Dengan hanya sumber pustaka, Anda mendapatkan berikut ini dari kotak (yaitu akan menghentikan eksekusi pada setiap kesalahan, seolah-olah menggunakan set -eberkattrap on ERRdan beberapa bash-fu ):

penanganan kesalahan bash-oo-framework

Ada beberapa fitur tambahan yang membantu menangani kesalahan, seperti coba dan tangkap , atau lemparan kata kunci , yang memungkinkan Anda untuk memutuskan eksekusi pada suatu titik untuk melihat jejak balik. Plus, jika terminal mendukungnya, ia mengeluarkan emoji powerline, mewarnai bagian-bagian output agar mudah dibaca, dan menggarisbawahi metode yang menyebabkan pengecualian dalam konteks garis kode.

Kelemahannya adalah - ini tidak portabel - kode bekerja di bash, mungkin> = 4 saja (tapi saya bayangkan itu bisa diangkut dengan upaya untuk bash 3).

Kode dipisahkan menjadi beberapa file untuk penanganan yang lebih baik, tetapi saya terinspirasi oleh ide backtrace dari jawaban di atas oleh Luca Borrione .

Untuk membaca lebih lanjut atau melihat sumbernya, lihat GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw

niieani
sumber
Ini ada di dalam proyek Kerangka Berorientasi Objek Bash . ... Untungnya hanya memiliki 7.4k LOC (menurut GLOC ). OOP - Nyeri berorientasi objek?
ingyhere
@ di mana itu sangat modular (dan ramah-hapus), jadi Anda hanya dapat menggunakan bagian pengecualian jika itu yang Anda
pilih
11

Saya lebih suka sesuatu yang sangat mudah dipanggil. Jadi saya menggunakan sesuatu yang terlihat sedikit rumit, tetapi mudah digunakan. Saya biasanya hanya menyalin dan menempelkan kode di bawah ini ke skrip saya. Penjelasan mengikuti kode.

#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
    echo
    echo "$@"
    exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM

#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'

Saya biasanya melakukan panggilan ke fungsi pembersihan di samping fungsi error_exit, tetapi ini bervariasi dari satu skrip ke skrip yang lain sehingga saya mengabaikannya. Perangkap menangkap sinyal terminasi umum dan memastikan semuanya dibersihkan. Alias ​​adalah apa yang melakukan sihir yang sebenarnya. Saya suka memeriksa semuanya untuk kegagalan. Jadi secara umum saya memanggil program dalam "jika!" ketikkan pernyataan. Dengan mengurangi 1 dari nomor baris alias akan memberi tahu saya di mana kegagalan terjadi. Itu juga sangat sederhana untuk dipanggil, dan cukup banyak bukti bodoh. Di bawah ini adalah contoh (cukup ganti / bin / false dengan apa pun yang akan Anda panggil).

#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
    die "Who knew false is false."
fi
Michael Nooner
sumber
2
Bisakah Anda memperluas pernyataan "Kami harus secara eksplisit mengizinkan alias" ? Saya khawatir bahwa beberapa perilaku yang tidak terduga akan terjadi. Adakah cara untuk mencapai hal yang sama dengan dampak yang lebih kecil?
blong
Saya tidak perlu $LINENO - 1. Tunjukkan dengan benar tanpa itu.
kyb
Contoh penggunaan lebih pendek dalam bash dan zshfalse || die "hello death"
kyb
6

Pertimbangan lain adalah kode keluar untuk kembali. Hanya " 1" cukup standar, meskipun ada beberapa kode keluar khusus yang digunakan bash sendiri , dan halaman yang sama berpendapat bahwa kode yang ditentukan pengguna harus dalam kisaran 64-113 untuk memenuhi standar C / C ++.

Anda mungkin juga mempertimbangkan pendekatan vektor bit yang mountdigunakan untuk kode keluarnya:

 0  success
 1  incorrect invocation or permissions
 2  system error (out of memory, cannot fork, no more loop devices)
 4  internal mount bug or missing nfs support in mount
 8  user interrupt
16  problems writing or locking /etc/mtab
32  mount failure
64  some mount succeeded

OR-menyatukan kode memungkinkan skrip Anda memberi sinyal beberapa kesalahan simultan.

Yukondude
sumber
4

Saya menggunakan kode jebakan berikut, ini juga memungkinkan kesalahan untuk dilacak melalui pipa dan perintah 'waktu'

#!/bin/bash
set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
function error() {
    JOB="$0"              # job name
    LASTLINE="$1"         # line of error occurrence
    LASTERR="$2"          # error code
    echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
    exit 1
}
trap 'error ${LINENO} ${?}' ERR
Olivier Delrieu
sumber
5
Kata functionkunci ini tidak kompatibel dengan POSIX. Pertimbangkan untuk membuat pernyataan Anda dengan adil error() {, tanpa functionsebelumnya.
Charles Duffy
5
${$?}seharusnya $?, atau ${?}jika Anda bersikeras menggunakan kawat gigi yang tidak perlu; batin $salah.
Charles Duffy
3
@CharlesDuffy saat ini, POSIX adalah GNU / Linux tidak kompatibel (masih, saya ambil poin Anda)
Croad Langshan
3

Saya sudah menggunakan

die() {
        echo $1
        kill $$
}

sebelum; Saya pikir karena 'exit' gagal untuk saya karena suatu alasan. Default di atas sepertinya ide yang bagus.

pjz
sumber
Lebih baik mengirim pesan kesalahan ke STDERR, bukan?
ankostis
3

Ini telah membantu saya dengan baik untuk sementara waktu sekarang. Ini mencetak pesan kesalahan atau peringatan dengan warna merah, satu baris per parameter, dan memungkinkan kode keluar opsional.

# Custom errors
EX_UNKNOWN=1

warning()
{
    # Output warning messages
    # Color the output red if it's an interactive terminal
    # @param $1...: Messages

    test -t 1 && tput setf 4

    printf '%s\n' "$@" >&2

    test -t 1 && tput sgr0 # Reset terminal
    true
}

error()
{
    # Output error messages with optional exit code
    # @param $1...: Messages
    # @param $N: Exit code (optional)

    messages=( "$@" )

    # If the last parameter is a number, it's not part of the messages
    last_parameter="${messages[@]: -1}"
    if [[ "$last_parameter" =~ ^[0-9]*$ ]]
    then
        exit_code=$last_parameter
        unset messages[$((${#messages[@]} - 1))]
    fi

    warning "${messages[@]}"

    exit ${exit_code:-$EX_UNKNOWN}
}
l0b0
sumber
3

Tidak yakin apakah ini akan membantu Anda, tetapi saya memodifikasi beberapa fungsi yang disarankan di sini untuk memasukkan pemeriksaan kesalahan (kode keluar dari perintah sebelumnya) di dalamnya. Pada setiap "periksa", saya juga memberikan "pesan" sebagai parameter tentang kesalahan tersebut untuk tujuan pencatatan.

#!/bin/bash

error_exit()
{
    if [ "$?" != "0" ]; then
        log.sh "$1"
        exit 1
    fi
}

Sekarang untuk memanggilnya dalam skrip yang sama (atau yang lain jika saya gunakan export -f error_exit) Saya cukup menulis nama fungsi dan meneruskan pesan sebagai parameter, seperti ini:

#!/bin/bash

cd /home/myuser/afolder
error_exit "Unable to switch to folder"

rm *
error_exit "Unable to delete all files"

Dengan menggunakan ini saya dapat membuat file bash yang sangat kuat untuk beberapa proses otomatis dan itu akan berhenti jika terjadi kesalahan dan memberi tahu saya ( log.shakan melakukannya)

Nelson Rodriguez
sumber
2
Pertimbangkan untuk menggunakan sintaks POSIX untuk mendefinisikan fungsi - tanpa functionkata kunci, adil error_exit() {.
Charles Duffy
2
adakah alasan mengapa kamu tidak melakukannya cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares
@ Pierre-OlivierVares Tidak ada alasan khusus untuk tidak menggunakan ||. Ini hanya kutipan dari kode yang ada dan saya baru saja menambahkan baris "penanganan kesalahan" setelah setiap baris yang menyangkut. Beberapa sangat panjang dan itu lebih bersih untuk memilikinya pada jalur (langsung) yang terpisah
Nelson Rodriguez
Sepertinya solusi bersih, namun, pemeriksaan shell mengeluh: github.com/koalaman/shellcheck/wiki/SC2181
mhulse
1

Trik ini berguna untuk perintah atau fungsi yang hilang. Nama fungsi yang hilang (atau dapat dieksekusi) akan diberikan dalam $ _

function handle_error {
    status=$?
    last_call=$1

    # 127 is 'command not found'
    (( status != 127 )) && return

    echo "you tried to call $last_call"
    return
}

# Trap errors.
trap 'handle_error "$_"' ERR
Orwellophile
sumber
Bukankah $_fungsi yang tersedia sama $?? Saya tidak yakin ada alasan untuk menggunakan salah satu fungsi tetapi tidak yang lain.
ingyhere
1

Fungsi ini telah melayani saya dengan cukup baik baru-baru ini:

action () {
    # Test if the first parameter is non-zero
    # and return straight away if so
    if test $1 -ne 0
    then
        return $1
    fi

    # Discard the control parameter
    # and execute the rest
    shift 1
    "$@"
    local status=$?

    # Test the exit status of the command run
    # and display an error message on failure
    if test ${status} -ne 0
    then
        echo Command \""$@"\" failed >&2
    fi

    return ${status}
}

Anda menyebutnya dengan menambahkan 0 atau nilai pengembalian terakhir ke nama perintah yang akan dijalankan, sehingga Anda dapat mengaitkan perintah tanpa harus memeriksa nilai kesalahan. Dengan ini, pernyataan ini memblokir:

command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...

Menjadi ini:

action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...

<<<Error-handling code here>>>

Jika salah satu perintah gagal, kode kesalahan hanya diteruskan ke ujung blok. Saya merasa berguna ketika Anda tidak ingin perintah berikutnya dieksekusi jika yang sebelumnya gagal, tetapi Anda juga tidak ingin skrip keluar langsung (misalnya, di dalam loop).

xarxziux
sumber
0

Menggunakan perangkap tidak selalu merupakan opsi. Misalnya, jika Anda sedang menulis beberapa jenis fungsi yang dapat digunakan kembali yang memerlukan penanganan kesalahan dan yang dapat dipanggil dari skrip apa pun (setelah sumber file dengan fungsi pembantu), fungsi itu tidak dapat mengasumsikan apa pun tentang waktu keluar skrip luar, yang membuat menggunakan perangkap sangat sulit. Kerugian lain dari menggunakan jebakan adalah kompabilitas yang buruk, karena Anda berisiko menimpa jebakan sebelumnya yang mungkin ditetapkan sebelumnya dalam rantai penelepon.

Ada sedikit trik yang dapat digunakan untuk melakukan penanganan kesalahan yang tepat tanpa jebakan. Seperti yang mungkin sudah Anda ketahui dari jawaban lain, set -etidak berfungsi di dalam perintah jika Anda menggunakan ||operator setelahnya, bahkan jika Anda menjalankannya dalam subkulit; mis. ini tidak akan berfungsi:

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

Tetapi ||operator diperlukan untuk mencegah kembali dari fungsi luar sebelum pembersihan. Caranya adalah dengan menjalankan perintah dalam di latar belakang, dan kemudian segera tunggu. The waitbuiltin akan mengembalikan kode keluar dari perintah batin, dan sekarang Anda menggunakan ||setelah wait, bukan fungsi batin, jadi set -ebekerja dengan baik dalam kedua:

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

Inilah fungsi generik yang dibangun di atas gagasan ini. Ini harus bekerja di semua shell yang kompatibel dengan POSIX jika Anda menghapus localkata kunci, yaitu ganti semua local x=yhanya dengan 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
}

Contoh penggunaan:

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

Menjalankan contoh:

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

Satu-satunya hal yang perlu Anda perhatikan ketika menggunakan metode ini adalah bahwa semua modifikasi variabel Shell yang dilakukan dari perintah yang Anda lewati runtidak akan merambat ke fungsi panggilan, karena perintah berjalan dalam subkulit.

sam.kozin
sumber