Bagaimana cara menangkap kesalahan dalam skrip bash linux?

13

Saya membuat skrip berikut:

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

Scriptnya bekerja, tetapi di samping gema saya, ada juga output kapan

cd $1

gagal dieksekusi.

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

Apakah mungkin untuk menangkap ini?

Thomas De Wilde
sumber
Hanya FYI, Anda juga bisa melakukan ini lebih sederhana; test -d /path/to/directory(atau [[ -d /path/to/directory ]]dalam bash) akan memberi tahu Anda apakah target yang diberikan adalah direktori atau tidak, dan itu akan melakukannya dengan tenang.
Patrick
@ Patrick, itu hanya menguji apakah itu direktori, bukan jika Anda bisa cdke dalamnya.
Stéphane Chazelas
@StephaneChazelas ya. Nama fungsinya adalah directoryExists.
Patrick
Lihat jawaban terperinci di sini: Angkat kesalahan dalam skrip Bash .
codeforester

Jawaban:

8

Script Anda mengubah direktori saat dijalankan, yang berarti itu tidak akan berfungsi dengan serangkaian nama path relatif. Anda kemudian berkomentar kemudian bahwa Anda hanya ingin memeriksa keberadaan direktori, bukan kemampuan untuk menggunakan cd, jadi jawaban tidak perlu digunakan cdsama sekali. Diperbaiki. Menggunakan tput dan warna dari 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
}

(Diedit untuk menggunakan yang lebih kebal printfdaripada masalah echoyang mungkin bertindak pada urutan melarikan diri dalam teks.)

Ian D. Allen
sumber
Itu juga memperbaiki (kecuali xpg_echo aktif) masalah ketika nama file mengandung karakter backslash.
Stéphane Chazelas
12

Gunakan set -euntuk mengatur mode exit-on-error: jika perintah sederhana mengembalikan status bukan nol (menunjukkan kegagalan), shell keluar.

Waspadalah yang set -etidak selalu berhasil. Perintah di posisi tes diizinkan untuk gagal (mis. if failing_command, failing_command || fallback). Perintah dalam subkulit hanya menyebabkan keluarnya subkulit, bukan induknya: set -e; (false); echo footampilan foo.

Atau, atau sebagai tambahan, dalam bash (dan ksh dan zsh, tetapi bukan sh polos), Anda dapat menentukan perintah yang dijalankan jika perintah mengembalikan status bukan nol, dengan ERRjebakan, mis trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Perhatikan bahwa dalam kasus seperti (false); …, perangkap ERR dieksekusi di subkulit, sehingga tidak dapat menyebabkan induk untuk keluar.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Baru-baru ini saya bereksperimen sedikit dan menemukan cara yang nyaman untuk memperbaiki ||perilaku, yang memungkinkan untuk dengan mudah melakukan penanganan kesalahan yang tepat tanpa menggunakan perangkap. Lihat jawaban saya . Apa pendapat Anda tentang metode itu?
skozin
@ sam.kozin Saya tidak punya waktu untuk meninjau jawaban Anda secara detail, itu terlihat bagus secara prinsip. Terlepas dari portabilitas, apa manfaat dari jebakan ERR ksh / bash / zsh?
Gilles 'SANGAT berhenti menjadi jahat'
Mungkin satu-satunya manfaat adalah kompabilitas, karena Anda tidak mengambil risiko untuk menimpa jebakan lain yang ditetapkan sebelum Anda berfungsi. Yang merupakan fitur yang berguna ketika Anda menulis beberapa fungsi umum yang nantinya akan Anda sumber dan gunakan dari skrip lain. Manfaat lain mungkin kompatibilitas POSIX penuh, meskipun tidak begitu penting karena ERRsinyal semu didukung di semua shell utama. Terima kasih atas ulasannya! =)
skozin
@ sam.kozin Saya lupa menulis di komentar saya sebelumnya: Anda mungkin ingin memposting ini di Code Review dan memposting tautan di chatroom .
Gilles 'SANGAT berhenti menjadi jahat'
Terima kasih atas sarannya, saya akan mencoba mengikutinya. Tidak tahu tentang Tinjauan Kode.
skozin
5

Untuk memperluas jawaban @Gilles ' :

Memang, 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.

Ada sedikit trik yang dapat digunakan untuk memperbaikinya: jalankan 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.

skozin
sumber
2

Anda tidak mengatakan apa yang sebenarnya Anda maksud dengan catch--- laporkan dan lanjutkan; batalkan proses lebih lanjut?

Karena cdmengembalikan status tidak nol pada kegagalan, Anda dapat melakukan:

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

Anda bisa keluar dari kegagalan:

cd -- "$1" || exit 1

Atau, gema pesan Anda sendiri dan keluar:

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

Dan / atau menekan kesalahan yang diberikan oleh cdpada kegagalan:

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

Secara standar, perintah harus meletakkan pesan kesalahan pada STDERR (file descriptor 2). Demikian 2>/dev/nullkata redirect STDERR ke "bit-bucket" yang dikenal oleh /dev/null.

(jangan lupa mengutip variabel Anda dan tandai akhir opsi untuk cd).

JRFerguson
sumber
@Stephane Chazelas mengutip dan memberi tanda akhir dari opsi yang diambil dengan baik. Terima kasih sudah mengedit.
JRFerguson
1

Sebenarnya untuk kasus Anda, saya akan mengatakan bahwa logika dapat ditingkatkan.

Alih-alih cd dan kemudian periksa apakah itu ada, periksa apakah ada kemudian masuk ke direktori.

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

Tetapi jika tujuan Anda adalah untuk membungkam kesalahan yang mungkin terjadi cd -- "$1" 2>/dev/null, tetapi ini akan membuat Anda men-debug di masa depan lebih sulit. Anda dapat memeriksa bendera pengujian jika di: Bash jika dokumentasi :

BitsOfNix
sumber
Jawaban ini gagal mengutip $1variabel dan akan gagal jika variabel itu berisi karakter meta kosong atau karakter shell lainnya. Ini juga gagal memeriksa apakah pengguna memiliki izin untuk cdmasuk ke dalamnya.
Ian D. Allen
Saya sebenarnya mencoba untuk memeriksa apakah ada direktori tertentu, belum tentu cd ke sana. Tetapi karena saya tidak tahu yang lebih baik, saya pikir mencoba untuk melakukannya akan menyebabkan kesalahan jika tidak ada jadi mengapa tidak menangkapnya? Saya tidak tahu tentang jika [-d ​​$ 1] itulah yang saya butuhkan. Jadi terima kasih banyak! (Saya sudah terbiasa dengan proram Java, dan memeriksa direktori dalam pernyataan if jika tidak terlalu umum di Jawa)
Thomas De Wilde