Perilaku yang benar dari perangkap EXIT dan ERR saat menggunakan `set -eu`

27

Saya mengamati beberapa perilaku aneh saat menggunakan set -e( errexit), set -u( nounset) bersama dengan perangkap ERR dan EXIT. Mereka tampaknya terkait, sehingga menempatkan mereka dalam satu pertanyaan tampaknya masuk akal.

1) set -utidak memicu jebakan ERR

  • Kode:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
  • Diharapkan: ERR trap dipanggil, RC! = 0
  • Aktual: ERR trap tidak dipanggil, RC == 1
  • Catatan: set -etidak mengubah hasilnya

2) Menggunakan set -eukode keluar dalam perangkap EXIT adalah 0 bukan 1

  • Kode:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
  • Diharapkan: EXIT trap dipanggil, RC == 1
  • Aktual: EXIT trap disebut, RC == 0
  • Catatan: Saat menggunakan set +e, RC == 1. Perangkap EXIT mengembalikan RC yang tepat saat ada perintah lain yang melempar kesalahan.
  • Sunting: Ada posting SO tentang topik ini dengan komentar menarik yang menunjukkan bahwa ini mungkin terkait dengan versi Bash yang digunakan. Menguji cuplikan ini dengan Bash 4.3.11 menghasilkan RC = 1, jadi itu lebih baik. Sayangnya meningkatkan Bash (dari 3.2.51) pada semua host tidak mungkin saat ini, jadi kami harus datang dengan beberapa solusi lain.

Adakah yang bisa menjelaskan perilaku ini?

Pencarian topik-topik ini tidak terlalu berhasil, yang agak mengejutkan mengingat jumlah posting pada pengaturan dan jebakan Bash. Ada satu utas forum , tetapi kesimpulannya agak tidak memuaskan.

dvdgsng
sumber
3
Dari 4 pada saya pikir bashputus dengan standar dan mulai menempatkan jebakan dalam subkulit. Perangkap seharusnya dieksekusi di lingkungan yang sama dari mana datang kembalinya, tetapi bashbelum melakukan itu untuk sementara waktu.
mikeserv
1
Tunggu sebentar - apakah Anda ingin solusi atau penjelasan? Dan jika Anda menginginkan solusi, lalu solusi untuk apa sebenarnya? Apa yang Anda inginkan terjadi? set -edan set -ukeduanya dirancang khusus untuk membunuh shell scripted. Menggunakan mereka dalam kondisi yang dapat memicu aplikasi mereka akan mematikan shell scripted. Tidak ada jalan keluarnya, kecuali untuk tidak menggunakannya, dan sebagai gantinya untuk menguji kondisi-kondisi tersebut ketika mereka berlaku dalam urutan kode. Jadi, pada dasarnya, Anda dapat menulis kode-shell yang baik, atau dapat Anda gunakan set -eu.
mikeserv
2
Sebenarnya, saya sedang mencari keduanya, karena saya tidak dapat menemukan informasi yang cukup tentang mengapa -utidak akan memicu perangkap ERR (ini adalah kesalahan, jadi seharusnya tidak memicu perangkap) atau kode kesalahan adalah 0 bukannya 1. yang terakhir sepertinya adalah bug yang sudah diperbaiki di versi yang lebih baru, jadi begitu. Tetapi bagian pertama cukup sulit untuk dipahami jika Anda belum menyadari bahwa kesalahan dalam evaluasi shell (ekspansi parameter) dan kesalahan aktual dalam perintah tampaknya merupakan dua hal yang berbeda. Untuk solusinya, seperti yang Anda sarankan, saya sekarang mencoba untuk menghindari -eudan memeriksa secara manual ketika diperlukan
dvdgsng
1
@ dvdsng - Bagus. Itulah cara untuk pergi - Anda harus memposting skrip Anda ketika Anda melakukan sebagai jawaban dan memberi hadiah kepada diri Anda sendiri. Saya sangat tidak suka opsi-opsi itu - mereka tidak mengizinkan penanganan pengecualian dengan cara apa pun yang aman.
mikeserv
1
@dvdsng - di mana salah satu dari opsi-opsi itu berguna, dalam konteks subshelled. Jadi bisa dibayangkan bahwa apa pun yang Anda gunakan sebelumnya dapat dilokalisasi ke dalam konteks subkulit seperti: (set -u; : $UNSET_VAR)dan serupa. Hal-hal semacam ini bisa bagus juga - Anda dapat menjatuhkan banyak sekali-sekali &&:, (set -e; mkdir dir; cd dir; touch dirfile)jika Anda mengerti maksud saya. Hanya saja itu adalah konteks yang terkontrol - ketika Anda menetapkannya sebagai opsi global, Anda kehilangan kendali dan menjadi terkontrol. Namun, biasanya ada solusi yang lebih efisien.
mikeserv

Jawaban:

15

Dari man bash:

  • set -u
    • Perlakukan variabel dan parameter yang tidak disetel selain dari parameter khusus "@"dan "*"sebagai kesalahan saat melakukan ekspansi parameter. Jika ekspansi dilakukan pada variabel atau parameter yang tidak disetel, shell mencetak pesan kesalahan, dan, jika tidak -interaktif, keluar dengan status bukan nol.

POSIX menyatakan bahwa, jika terjadi kesalahan ekspansi , shell non-interaktif akan keluar ketika ekspansi dikaitkan dengan shell khusus builtin (yang merupakan pembedaan yang bashsecara teratur diabaikan, dan karenanya mungkin tidak relevan) atau utilitas lain selain .

  • Konsekuensi Kesalahan Shell :
    • Sebuah kesalahan ekspansi adalah salah satu yang terjadi ketika ekspansi shell didefinisikan dalam Firman Ekspansi dilakukan (misalnya, "${x!y}"karena !tidak operator valid) ; suatu implementasi dapat memperlakukan ini sebagai kesalahan sintaksis jika itu dapat mendeteksi mereka selama tokenization, daripada selama ekspansi.
    • [A] n shell interaktif harus menulis pesan diagnostik ke kesalahan standar tanpa keluar.

Juga dari man bash:

  • trap ... ERR
    • Jika sigspec adalah ERR , perintah arg dieksekusi setiap kali sebuah pipa (yang dapat terdiri dari satu perintah sederhana) , daftar, atau perintah gabungan mengembalikan status keluar yang tidak nol, dengan syarat sebagai berikut:
      • Perangkap ERR tidak dieksekusi jika perintah yang gagal adalah bagian dari daftar perintah segera mengikuti kata kunci whileatau until...
      • ... bagian dari tes dalam sebuah ifpernyataan ...
      • ... bagian dari perintah yang dieksekusi dalam daftar &&atau ||kecuali perintah yang mengikuti akhir &&atau ||...
      • ... perintah apa pun dalam pipa tetapi yang terakhir ...
      • ... atau jika nilai pengembalian perintah sedang dibalik menggunakan !.
    • Ini adalah kondisi yang sama yang dipatuhi oleh opsi errexit -e .

Perhatikan di atas bahwa perangkap ERR adalah semua tentang evaluasi pengembalian perintah lain . Tetapi ketika kesalahan ekspansi terjadi, tidak ada perintah yang dijalankan untuk mengembalikan apa pun. Dalam contoh Anda, echo tidak pernah terjadi - karena sementara shell mengevaluasi dan memperluas argumennya, ia menemukan -uvariabel nset, yang telah ditentukan oleh opsi shell eksplisit untuk menyebabkan keluar langsung dari shell saat ini, scripted.

Dan perangkap EXIT , jika ada, dieksekusi, dan shell keluar dengan pesan diagnostik dan status keluar selain 0 - persis seperti yang seharusnya dilakukan.

Adapun hal rc: 0 , saya berharap itu adalah bug versi tertentu dari beberapa jenis - mungkin berkaitan dengan dua pemicu untuk EXIT yang terjadi pada saat yang sama dan yang satu mendapatkan kode keluar yang lain (yang seharusnya tidak terjadi) . Lagi pula, dengan bashbiner terbaru yang diinstal oleh pacman:

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

Saya menambahkan baris pertama sehingga Anda dapat melihat bahwa kondisi shell adalah kondisi shell yang scripted - tidak interaktif. Outputnya adalah:

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

Berikut adalah beberapa catatan yang relevan dari daftar perubahan terbaru :

  • Memperbaiki bug yang menyebabkan perintah asinkron tidak diatur $?dengan benar.
  • Memperbaiki bug yang menyebabkan pesan kesalahan yang dihasilkan oleh kesalahan ekspansi di forperintah memiliki nomor baris yang salah.
  • Memperbaiki bug yang menyebabkan SIGINT dan SIGQUIT tidak dapat trapdi-pable dalam perintah subshell asynchronous.
  • Memperbaiki masalah dengan penanganan interupsi yang menyebabkan SIGINT kedua dan selanjutnya diabaikan oleh shell interaktif.
  • Shell tidak lagi memblokir penerimaan sinyal saat menjalankan trappenangan untuk sinyal-sinyal itu, dan memungkinkan sebagian besar trap penangan untuk dijalankan secara rekursif (menjalankan trappenangan saat trappenangan menjalankan) .

Saya pikir yang terakhir atau yang paling relevan - atau mungkin kombinasi keduanya. Sebuah traphandler adalah dengan sifatnya asynchronous karena seluruh pekerjaan adalah untuk menunggu dan menangani sinyal asynchronous . Dan Anda memicu dua secara bersamaan dengan -eudan $UNSET_VAR.

Dan mungkin Anda harus memperbarui, tetapi jika Anda menyukai diri Anda sendiri, Anda akan melakukannya dengan shell yang berbeda sama sekali.

mikeserv
sumber
Terima kasih atas penjelasan tentang bagaimana ekspansi parameter ditangani secara berbeda. Itu menjelaskan banyak hal kepada saya.
dvdgsng
Saya memberi Anda karunia karena penjelasan Anda sangat membantu.
dvdgsng
@dvdgsng - Gracias. Karena penasaran, apakah Anda pernah menemukan solusi Anda?
mikeserv
9

(Saya menggunakan bash 4.2.53). Untuk bagian 1, halaman bash man hanya mengatakan "Pesan kesalahan akan ditulis ke kesalahan standar, dan shell non-interaktif akan keluar". Itu tidak mengatakan perangkap ERR akan dipanggil, meskipun saya setuju itu akan berguna jika itu benar.

Untuk menjadi pragmatis, jika apa yang Anda inginkan adalah mengatasi lebih bersih dengan variabel yang tidak terdefinisi, solusi yang mungkin adalah dengan meletakkan sebagian besar kode Anda di dalam suatu fungsi, kemudian jalankan fungsi itu dalam sub-shell dan pulihkan kode kembali dan output stderr. Berikut ini contoh fungsi "cmd ()":

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

Di bash saya, saya mengerti

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1
meuh
sumber
bagus, solusi praktis yang benar-benar menambah nilai!
Florian Heigl