Menjebak kesalahan dalam substitusi perintah menggunakan "-o errtrace" (yaitu set -E)

14

Menurut panduan ref ini :

-E (juga -o errtrace)

Jika disetel, setiap jebakan pada ERR diwarisi oleh fungsi shell, pergantian perintah, dan perintah yang dijalankan di lingkungan subkulit. Perangkap ERR biasanya tidak diwariskan dalam kasus-kasus seperti itu.

Namun, saya harus menafsirkannya dengan salah, karena yang berikut ini tidak berfungsi:

#!/usr/bin/env bash
# -*- bash -*-

set -e -o pipefail -o errtrace -o functrace

function boom {
  echo "err status: $?"
  exit $?
}
trap boom ERR


echo $( made up name )
echo "  ! should not be reached ! "

Saya sudah tahu tugas sederhana my_var=$(made_up_name),, akan keluar dari skrip dengan set -e(yaitu errexit).

Apakah -E/-o errtraceseharusnya berfungsi seperti kode di atas? Atau, kemungkinan besar, saya salah baca?

dgo.a
sumber
2
Ini pertanyaan yang bagus. Mengganti echo $( made up name )dengan $( made up name )menghasilkan perilaku yang diinginkan. Saya tidak punya penjelasan.
iruvar
Saya tidak tahu tentang bash -E tetapi saya tahu bahwa -e hanya memengaruhi keluar shell jika kesalahan dihasilkan dari perintah terakhir dalam sebuah pipeline. Jadi, Anda var=$( pipe )dan $( pipe )contoh keduanya akan mewakili titik akhir pipa sedangkan pipe > echotidak. Halaman manual saya mengatakan: "1. Kegagalan setiap perintah individu dalam pipa multi-perintah tidak akan menyebabkan shell keluar. Hanya kegagalan pipa itu sendiri yang akan dipertimbangkan."
mikeserv
Anda dapat membuatnya gagal: echo $ ($ {madeupname?}). Tapi itu set -e. Sekali lagi, -E berada di luar pengalaman saya sendiri.
mikeserv
@mikeserv @ 1_CR Manual bash @ echo menunjukkan bahwa echoselalu kembali 0. Ini harus diperhitungkan dalam analisis ...

Jawaban:

5

Catatan: zshakan mengeluh tentang "pola buruk" jika Anda tidak mengonfigurasinya untuk menerima "komentar sebaris" untuk sebagian besar contoh di sini dan tidak menjalankannya melalui shell proxy seperti yang telah saya lakukan sh <<-\CMD.

Ok, jadi, seperti yang saya nyatakan dalam komentar di atas, saya tidak tahu secara spesifik tentang bashset -E , tetapi saya tahu bahwa shell yang kompatibel dengan POSIX menyediakan cara sederhana untuk menguji suatu nilai jika Anda menginginkannya:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
        echo "echo still works" 
    }
    _test && echo "_test doesnt fail"
    # END
    CMD
sh: line 1: empty: error string
+ echo

+ echo 'echo still works'
echo still works
+ echo '_test doesnt fail'
_test doesnt fail

Di atas Anda akan melihat bahwa meskipun saya dulu masih parameter expansionmenguji${empty?} _test()return s pass - seperti yang tampak dalam yang terakhir echoini terjadi karena nilai gagal membunuh $( command substitution )subkulit yang berisi itu, tapi shell induknya - _testsaat ini - terus truk. Dan echotidak peduli - itu banyak senang untuk melayani hanya \newline; echoadalah tidak ujian.

Tapi pertimbangkan ini:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    _test ||\
            echo "this doesnt even print"
    # END
    CMD
_test+ sh: line 1: empty: function doesnt run

Karena saya _test()'smemasukkan input dengan parameter yang sudah dievaluasi pada fungsi INIT here-documentsekarang _test()bahkan tidak mencoba untuk berjalan sama sekali. Terlebih lagi shcangkang itu tampaknya sepenuhnya menghilangkan hantu dan echo "this doesnt even print" bahkan tidak mencetak.

Mungkin itu bukan yang Anda inginkan.

Ini terjadi karena ${var?}perluasan parameter gaya dirancang untuk keluarshell jika ada parameter yang hilang, itu berfungsi seperti ini :

${parameter:?[word]}

Tunjukkan Kesalahan jika Nullatau Unset.Jika parameter tidak disetel atau nol, expansion of word(atau pesan yang menunjukkannya tidak disetel jika kata dihilangkan) akan menjadiwritten to standard error dan shell exits with a non-zero exit status. Jika tidak, nilai parameter shall be substituted. Shell interaktif tidak perlu keluar.

Saya tidak akan menyalin / menempelkan seluruh dokumen, tetapi jika Anda menginginkan kegagalan untuk set but nullnilai, Anda menggunakan formulir:

${var :? error message }

Dengan :colonseperti di atas. Jika Anda ingin nullnilai berhasil, cukup abaikan tanda titik dua. Anda juga dapat meniadakannya dan gagal hanya untuk menetapkan nilai, seperti yang akan saya tunjukkan sebentar lagi.

Berlari lagi _test():

    sh <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    echo "this runs" |\
        ( _test ; echo "this doesnt" ) ||\
            echo "now it prints"
    # END
    CMD
this runs
sh: line 1: empty: function doesnt run
now it prints

Ini berfungsi dengan semua jenis tes cepat, tetapi di atas Anda akan melihat bahwa _test(), jalankan dari tengah yang pipelinegagal, dan sebenarnya command listsubshell yang mengandungnya gagal seluruhnya, karena tidak ada perintah dalam fungsi yang dijalankan maupun yang berikut ini.echo berjalan sama sekali, meskipun itu juga menunjukkan bahwa itu dapat dengan mudah diuji karena echo "now it prints" sekarang dicetak.

Iblis ada dalam perinciannya, saya kira. Dalam kasus di atas, shell yang keluar bukan milik skrip_main | logic | pipeline tetapi yang ( subshell in which we ${test?} ) ||disebut sandboxing kecil.

Dan itu mungkin tidak jelas, tetapi jika Anda hanya ingin lulus untuk kasus sebaliknya, atau hanya set= nilai-nilai, itu cukup sederhana juga:

    sh <<-\CMD
    N= #N is NULL
    _test=$N #_test is also NULL and
    v="something you would rather do without"    
    ( #this subshell dies
        echo "v is ${v+set}: and its value is ${v:+not NULL}"
        echo "So this ${_test:-"\$_test:="} will equal ${_test:="$v"}"
        ${_test:+${N:?so you test for it with a little nesting}}
        echo "sure wish we could do some other things"
    )
    ( #this subshell does some other things 
        unset v #to ensure it is definitely unset
        echo "But here v is ${v-unset}: ${v:+you certainly wont see this}"
        echo "So this ${_test:-"\$_test:="} will equal NULL ${_test:="$v"}"
        ${_test:+${N:?is never substituted}}
        echo "so now we can do some other things" 
    )
    #and even though we set _test and unset v in the subshell
    echo "_test is still ${_test:-"NULL"} and ${v:+"v is still $v"}"
    # END
    CMD
v is set: and its value is not NULL
So this $_test:= will equal something you would rather do without
sh: line 7: N: so you test for it with a little nesting
But here v is unset:
So this $_test:= will equal NULL
so now we can do some other things
_test is still NULL and v is still something you would rather do without

Contoh di atas mengambil keuntungan dari semua 4 bentuk substitusi parameter POSIX dan berbagai :colon nullatau not nulltes mereka. Ada informasi lebih lanjut di tautan di atas, dan ini dia lagi .

Dan saya kira kita harus menunjukkan milik kita _test fungsi bekerja juga, kan? Kami hanya mendeklarasikan empty=somethingsebagai parameter untuk fungsi kami (atau kapan saja sebelumnya):

    sh <<-\CMD
    _test() { echo $( echo ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?tested as a pass before function runs}
    INIT
    echo "this runs" >&2 |\
        ( empty=not_empty _test ; echo "yay! I print now!" ) ||\
            echo "suspiciously quiet"
    # END
    CMD
this runs
not_empty
echo still works
yay! I print now!

Perlu dicatat bahwa evaluasi ini berdiri sendiri - tidak memerlukan tes tambahan untuk gagal. Beberapa contoh lagi:

    sh <<-\CMD
    empty= 
    ${empty?null, no colon, no failure}
    unset empty
    echo "${empty?this is stderr} this is not"
    # END
    CMD
sh: line 3: empty: this is stderr

    sh <<-\CMD
    _input_fn() { set -- "$@" #redundant
            echo ${*?WHERES MY DATA?}
            #echo is not necessary though
            shift #sure hope we have more than $1 parameter
            : ${*?WHERES MY DATA?} #: do nothing, gracefully
    }
    _input_fn heres some stuff
    _input_fn one #here
    # shell dies - third try doesnt run
    _input_fn you there?
    # END
    CMD
heres some stuff
one
sh: line :5 *: WHERES MY DATA?

Dan akhirnya kami kembali ke pertanyaan awal: bagaimana menangani kesalahan dalam$(command substitution) subkulit? Yang benar adalah - ada dua cara, tetapi tidak ada yang langsung. Inti dari masalah adalah proses evaluasi shell - ekspansi shell (termasuk$(command substitution)) terjadi lebih awal dalam proses evaluasi shell daripada eksekusi perintah shell saat ini - yaitu ketika kesalahan Anda dapat ditangkap dan dijebak.

Masalah yang dialami op adalah pada saat shell saat ini mengevaluasi kesalahan, the $(command substitution) subshell sudah diganti - tidak ada kesalahan yang tersisa.

Jadi apa dua cara itu? Anda melakukannya secara eksplisit di dalam$(command substitution) subkulit dengan tes seperti yang Anda lakukan tanpanya, atau Anda menyerap hasilnya ke dalam variabel shell saat ini dan menguji nilainya.

Metode 1:

    echo "$(madeup && echo \: || echo '${fail:?die}')" |\
          . /dev/stdin

sh: command not found: madeup
/dev/stdin:1: fail: die

    echo $?

126

Metode 2:

    var="$(madeup)" ; echo "${var:?die} still not stderr"

sh: command not found: madeup
sh: var: die

    echo $?

1

Ini akan gagal terlepas dari jumlah variabel yang dinyatakan per baris:

   v1="$(madeup)" v2="$(ls)" ; echo "${v1:?}" "${v2:?}"

sh: command not found: madeup
sh: v1: parameter not set

Dan nilai pengembalian kami tetap konstan:

    echo $?
1

SEKARANG JEJAK:

    trap 'printf %s\\n trap resurrects shell!' ERR
    v1="$(madeup)" v2="$(printf %s\\n shown after trap)"
    echo "${v1:?#1 - still stderr}" "${v2:?invisible}"

sh: command not found: madeup
sh: v1: #1 - still stderr
trap
resurrects
shell!
shown
after
trap

    echo $?
0
mikeserv
sumber
Misalnya, praktis spesifikasinya: echo $ {v = $ (madeupname)}; gema "$ {v:? trap1st}! tidak tercapai!"
mikeserv
1
Untuk meringkas: Ekspansi parameter ini adalah cara yang bagus untuk mengontrol jika variabel diatur dll. Sehubungan dengan status keluar dari gema, saya perhatikan bahwa echo "abc" "${v1:?}"tampaknya tidak mengeksekusi (abc tidak pernah dicetak). Dan shell mengembalikan 1. Ini benar dengan atau tanpa perintah genap ( "${v1:?}"langsung pada cli). Tetapi untuk skrip OP, semua yang diperlukan untuk memicu jebakan adalah menempatkan tugas variabelnya yang berisi substitusi untuk perintah yang tidak ada sendirian pada satu baris. Kalau tidak, perilaku gema adalah untuk mengembalikan 0 selalu, kecuali terganggu seperti dengan tes yang Anda jelaskan.
Adil v=$( madeup ). Saya tidak melihat apa yang tidak aman dengan itu. Itu hanya tugas, orang itu salah mengeja perintah misalnya v="$(lss)". Itu kesalahan. Ya Anda dapat memverifikasi dengan status kesalahan dari perintah terakhir $? - karena itu adalah perintah di telepon (penugasan tanpa nama perintah) dan tidak ada yang lain - bukan argumen untuk menggemakan. Plus, di sini ia terjebak oleh fungsi as! = 0, jadi Anda mendapat umpan balik dua kali. Kalau tidak pasti seperti yang Anda jelaskan ada cara yang lebih baik untuk melakukan ini secara tertib dalam suatu kerangka kerja tetapi OP memiliki 1 baris: gema plus substitusi yang gagal. Dia bertanya-tanya tentang gema.
Ya, penugasan sederhana disebutkan dalam pertanyaan dan diberikan begitu saja, dan sementara saya tidak bisa menawarkan saran khusus tentang bagaimana memanfaatkan -E, saya memang mencoba untuk menunjukkan hasil yang serupa dengan masalah yang diperagakan lebih dari mungkin tanpa menggunakan bashism. Dalam kasus apa pun, khususnya masalah yang Anda sebutkan - seperti penugasan tunggal per baris - yang membuat solusi seperti itu sulit untuk ditangani dalam pipa, yang saya juga menunjukkan cara menangani. Meskipun benar apa yang Anda katakan - tidak ada yang tidak aman hanya dengan menugaskannya.
mikeserv
1
Poin bagus - mungkin lebih banyak upaya diperlukan. Aku akan memikirkannya.
mikeserv
1

Jika disetel, setiap jebakan pada ERR diwarisi oleh fungsi shell, pergantian perintah, dan perintah yang dijalankan di lingkungan subkulit

Dalam skrip Anda, ini adalah eksekusi perintah ( echo $( made up name )). Dalam perintah bash dibatasi dengan baik ; atau dengan baris baru . Di perintah

echo $( made up name )

$( made up name )dianggap sebagai bagian dari perintah. Bahkan jika bagian ini gagal dan kembali dengan kesalahan, seluruh perintah dijalankan dengan sukses karena echotidak tahu tentang hal itu. Ketika perintah kembali dengan 0 no trap dipicu.

Anda harus memasukkannya ke dalam dua perintah, penugasan, dan gema

var=$(made_up_name)
echo $var
totti
sumber
echo $vartidak gagal - $vardiperluas menjadi kosong sebelum echobenar-benar melihatnya.
mikeserv
0

Ini karena bug di bash. Selama Pergantian Komando langkah dalam

echo $( made up name )

mademenjalankan (atau gagal ditemukan) dalam sebuah subkulit, tetapi subkulit tersebut "dioptimalkan" sedemikian rupa sehingga tidak menggunakan beberapa perangkap dari shell induk. Ini diperbaiki dalam versi 4.4.5 :

Dalam keadaan tertentu, perintah sederhana dioptimalkan untuk menghilangkan garpu, sehingga perangkap EXIT tidak dieksekusi.

Dengan bash 4.4.5 atau lebih tinggi, Anda akan melihat output berikut:

error.sh: line 13: made: command not found
err status: 127
  ! should not be reached !

Penangan perangkap telah dipanggil seperti yang diharapkan, kemudian subkulit keluar. (set -e hanya menyebabkan subkulit keluar, bukan induknya, sehingga pesan "tidak boleh dihubungi" seharusnya, pada kenyataannya, tercapai.)

Solusi untuk versi yang lebih lama adalah untuk memaksa pembuatan subkulit penuh yang tidak dioptimalkan:

echo $( ( made up name ) )

Ruang tambahan diperlukan untuk membedakan dari Ekspansi Aritmatika.

JigglyNaga
sumber