ข้อผิดพลาดในการดักจับในการทดแทนคำสั่งโดยใช้“ -o errtrace” (เช่น set -E)


14

ตามคู่มืออ้างอิงนี้:

-E (เช่น -o errtrace)

หากตั้งค่ากับดักใด ๆ บน ERR จะได้รับมรดกโดยฟังก์ชั่นของเชลล์การแทนที่คำสั่งและคำสั่งที่ดำเนินการในสภาพแวดล้อม subshell กับดัก ERR โดยปกติจะไม่สืบทอดในกรณีเช่นนี้

อย่างไรก็ตามฉันต้องตีความอย่างผิด ๆ เพราะสิ่งต่อไปนี้ใช้ไม่ได้:

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

ฉันรู้แล้วว่าการมอบหมายอย่างง่ายmy_var=$(made_up_name)จะออกจากสคริปต์ด้วยset -e(เช่น errexit)

คือ -E/-o errtraceควรจะทำงานเช่นโค้ดข้างต้นหรือไม่ หรือเป็นไปได้มากว่าฉันอ่านผิด?


2
นี่เป็นคำถามที่ดี การแทนที่echo $( made up name )ด้วยการ$( made up name )สร้างพฤติกรรมที่ต้องการ ฉันไม่มีคำอธิบายว่า
iruvar

ฉันไม่รู้เกี่ยวกับ bash's -E แต่ฉันรู้ว่า -e มีผลกับ shell exit เท่านั้นหากข้อผิดพลาดเป็นผลมาจากคำสั่งสุดท้ายในไพพ์ไลน์ ดังนั้นvar=$( pipe )และ$( pipe )ตัวอย่างของคุณทั้งสองจะเป็นตัวแทนจุดปลายท่อในขณะที่pipe > echoจะไม่ หน้าคนของฉันบอกว่า: "1. ความล้มเหลวของคำสั่งแต่ละคำสั่งในไปป์ไลน์แบบหลายคำสั่งจะไม่ทำให้เชลล์ออกจากการพิจารณาเฉพาะความล้มเหลวของ
ไพพ์

คุณสามารถทำให้มันล้มเหลวแม้ว่า: echo $ ($ {madeupname?}) แต่นั่นคือชุด -e อีกครั้ง -E อยู่นอกประสบการณ์ของฉันเอง
mikeserv

@mikeserv @ 1_CR คู่มือทุบตี @ echoบ่งชี้ว่าechoจะส่งคืน 0 เสมอนี้จะต้องเป็นปัจจัยในการวิเคราะห์ ...

คำตอบ:


5

หมายเหตุ: zshจะบ่นเกี่ยวกับ "รูปแบบที่ไม่ดี" หากคุณไม่ได้กำหนดค่าให้ยอมรับ "ความคิดเห็นแบบอินไลน์" สำหรับตัวอย่างส่วนใหญ่ที่นี่และไม่เรียกใช้ผ่านพร็อกซีเชลล์ที่ฉันได้ทำsh <<-\CMDไป

ตกลงดังนั้นตามที่ฉันระบุไว้ในความคิดเห็นด้านบนฉันไม่รู้เกี่ยวกับทุบตีset -Eโดยเฉพาะแต่ฉันรู้ว่าเปลือกที่เข้ากันได้ POSIX ให้วิธีง่ายๆในการทดสอบค่าถ้าคุณต้องการ:

    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

ด้านบนคุณจะเห็นว่าแม้ว่าผมจะนำมาใช้parameter expansionในการทดสอบ${empty?} _test()ยังคง returnsผ่าน - ตามที่แจ้งในช่วงechoนี้เกิดขึ้นเนื่องจากค่าล้มเหลวฆ่า$( command substitution )subshell ที่มี แต่เปลือกแม่ - _testในขณะนี้ - ช่วยในการขนส่ง และechoไม่ได้ดูแล - มันมีความสุขมากมายที่จะทำหน้าที่เพียง\newline; echoเป็นไม่ได้ทดสอบ

แต่ให้พิจารณาสิ่งนี้:

    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

เนื่องจากฉันป้อน_test()'sอินพุตด้วยพารามิเตอร์ที่ประเมินล่วงหน้าในINIT here-documentตอนนี้_test()ฟังก์ชั่นจึงไม่แม้แต่จะพยายามเลยแม้แต่น้อย shเห็นได้ชัดว่ามีเปลือกมากกว่าให้ผีทั้งหมดและecho "this doesnt even print" ไม่ได้พิมพ์

อาจไม่ใช่สิ่งที่คุณต้องการ

สิ่งนี้เกิดขึ้นเนื่องจากการ${var?}ขยายพารามิเตอร์สไตล์ถูกออกแบบมาเพื่อออกจากshellในกรณีที่พารามิเตอร์หายไปมันทำงานเช่นนี้ :

${parameter:?[word]}

บ่งชี้ถึงข้อผิดพลาดถ้าNullหรือUnset.ถ้าพารามิเตอร์ถูก unset หรือ null ที่expansion of word(หรือข้อความที่บ่งชี้ว่าไม่มีการตั้งค่าถ้าคำที่ถูกละไว้) ต้องและwritten to standard error มิฉะนั้นค่าของ เชลล์แบบโต้ตอบไม่จำเป็นต้องออกshell exits with a non-zero exit statusparameter shall be substituted

ฉันจะไม่คัดลอก / วางเอกสารทั้งหมด แต่ถ้าคุณต้องการความล้มเหลวสำหรับset but nullค่าที่คุณใช้แบบฟอร์ม:

${var :? error message }

ด้วย:colonดังกล่าวข้างต้น หากคุณต้องการnullค่าที่จะประสบความสำเร็จเพียงละเว้นลำไส้ใหญ่ คุณสามารถลบล้างมันและล้มเหลวเฉพาะกับค่าที่ตั้งไว้เพราะฉันจะแสดงในช่วงเวลาหนึ่ง

วิ่งอีกของ _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

งานนี้กับทุกชนิดของการทดสอบอย่างรวดเร็ว แต่ข้างต้นคุณจะเห็นว่า_test()การเรียกใช้จากตรงกลางของที่pipelineล้มเหลวและในความเป็นจริงมีของcommand listsubshell ล้มเหลวอย่างสิ้นเชิงที่ไม่มีคำสั่งภายในระยะฟังก์ชันมิได้ดังต่อไปนี้echoการทำงานที่ทุกคน แม้ว่ามันจะแสดงให้เห็นว่ามันสามารถทดสอบได้ง่ายเพราะecho "now it prints" ตอนนี้พิมพ์

ฉันคิดว่าปีศาจอยู่ในรายละเอียด ในกรณีข้างต้นเชลล์ที่ออกไม่ได้เป็นสคริปต์_main | logic | pipelineแต่( subshell in which we ${test?} ) ||มีการเรียกใช้แซนด์บ็อกซ์เพียงเล็กน้อย

และอาจไม่ชัดเจน แต่ถ้าคุณต้องการส่งเฉพาะกรณีตรงข้ามหรือset=ค่าเท่านั้นมันค่อนข้างง่ายเช่นกัน:

    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

ตัวอย่างข้างต้นใช้ประโยชน์จากการแทนที่พารามิเตอร์ POSIX ทั้ง 4 รูปแบบและการทดสอบ:colon nullหรือnot nullการทดสอบที่หลากหลาย มีข้อมูลเพิ่มเติมในการเชื่อมโยงดังกล่าวข้างต้นและที่นี่ก็เป็นอีกครั้ง

และฉันคิดว่าเราควรแสดง_testฟังก์ชั่นการทำงานของเราด้วยใช่ไหม เราเพิ่งประกาศempty=somethingว่าเป็นพารามิเตอร์สำหรับฟังก์ชั่นของเรา (หรือก่อนเวลาใด ๆ ):

    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!

ควรสังเกตว่าการประเมินนี้อยู่คนเดียว - ไม่จำเป็นต้องทดสอบเพิ่มเติมเพื่อล้มเหลว อีกสองสามตัวอย่าง:

    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?

ในที่สุดเราก็กลับมาที่คำถามเดิม: จะจัดการข้อผิดพลาดใน$(command substitution)subshell ได้อย่างไร? ความจริงก็คือ - มีสองวิธี แต่ไม่มีทางตรง หลักของปัญหาคือกระบวนการประเมินของเชลล์ - การขยายเชลล์ (รวมถึง$(command substitution)) เกิดขึ้นก่อนหน้านี้ในกระบวนการประเมินของเชลล์มากกว่าการดำเนินการคำสั่งเชลล์ปัจจุบัน - ซึ่งเมื่อข้อผิดพลาดของคุณอาจถูกดักจับและติดอยู่

ปัญหาที่พบจาก op คือเมื่อถึงเวลาที่เชลล์ปัจจุบันประเมินหาข้อผิดพลาด$(command substitution)subshell นั้นถูกแทนที่แล้ว - ไม่มีข้อผิดพลาดเหลืออยู่

ดังนั้นสองวิธีคืออะไร ไม่ว่าคุณจะทำอย่างชัดเจนภายใน$(command substitution)subshell ด้วยการทดสอบตามที่คุณต้องการโดยไม่มีหรือคุณดูดซับผลลัพธ์ลงในตัวแปร shell ปัจจุบันและทดสอบค่าของมัน

วิธีที่ 1:

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

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

    echo $?

126

วิธีที่ 2:

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

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

    echo $?

1

สิ่งนี้จะล้มเหลวโดยไม่คำนึงถึงจำนวนตัวแปรที่ประกาศต่อบรรทัด:

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

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

และค่าตอบแทนของเรายังคงที่:

    echo $?
1

ตอนนี้การดักฟัง:

    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

ตัวอย่างเช่นสเปคที่แน่นอนของเขาจริง: echo $ {v = $ (madeupname)}; echo "$ {v:? trap1st}! ไม่ถึง!"
mikeserv

1
ในการสรุป: การขยายพารามิเตอร์เหล่านี้เป็นวิธีที่ดีในการควบคุมว่ามีการตั้งค่าตัวแปร ฯลฯ เกี่ยวกับสถานะทางออกของเสียงสะท้อนฉันสังเกตเห็นว่าecho "abc" "${v1:?}"ดูเหมือนจะไม่ทำงาน (abc ไม่เคยพิมพ์) และเชลล์ส่งคืน 1 นี่เป็นจริงโดยมีหรือไม่มีคำสั่งคู่ ( "${v1:?}"บน cli โดยตรง) แต่สำหรับสคริปต์ของ OP สิ่งที่ต้องใช้เพื่อทริกเกอร์กับดักคือการวางตัวแปรที่มีการทดแทนสำหรับคำสั่งที่ไม่มีอยู่คนเดียวบนบรรทัดเดียว มิฉะนั้นพฤติกรรมของเสียงก้องคือการกลับ 0 เสมอเว้นแต่ขัดจังหวะเช่นเดียวกับการทดสอบที่คุณอธิบาย

v=$( madeup )เพียงแค่ ฉันไม่เห็นสิ่งที่ไม่ปลอดภัยกับสิ่งนั้น มันเป็นเพียงแค่การมอบหมายบุคคลนั้นสะกดคำสั่งเช่นv="$(lss)"นั้น มันผิดพลาด ใช่คุณสามารถตรวจสอบกับสถานะข้อผิดพลาดของคำสั่งสุดท้าย $? - เพราะมันเป็นคำสั่งในบรรทัด (การกำหนดโดยไม่มีชื่อคำสั่ง) และไม่มีอะไรอื่น - ไม่ใช่อาร์กิวเมนต์ที่จะสะท้อน นอกจากนี้ที่นี่มันติดกับฟังก์ชั่นเป็น! = 0 ดังนั้นคุณจะได้รับข้อเสนอแนะสองครั้ง มิฉะนั้นแล้วอย่างที่คุณอธิบายมีวิธีที่ดีกว่าที่จะทำสิ่งนี้อย่างเป็นระเบียบภายในกรอบ แต่ OP มี 1 บรรทัดเดียว: เสียงสะท้อนบวกกับการทดแทนที่ล้มเหลวของเขา เขาสงสัยเกี่ยวกับเสียงสะท้อน

ใช่มีการกล่าวถึงการมอบหมายอย่างง่ายในคำถามรวมถึงการอนุญาตและในขณะที่ฉันไม่สามารถให้คำแนะนำเฉพาะเกี่ยวกับวิธีการใช้ประโยชน์จาก -E ได้ฉันพยายามแสดงผลลัพธ์ที่คล้ายกันกับปัญหาที่แสดงให้เห็นมากกว่าที่เป็นไปได้ bashisms ไม่ว่าในกรณีใดมันเป็นปัญหาเฉพาะที่คุณพูดถึง - เช่นการมอบหมายครั้งเดียวต่อบรรทัด - ซึ่งทำให้การแก้ปัญหาดังกล่าวยากที่จะจัดการในท่อส่งซึ่งฉันยังแสดงให้เห็นถึงวิธีการจัดการ แม้ว่ามันจะเป็นจริงในสิ่งที่คุณพูด - ไม่มีอะไรที่ไม่ปลอดภัยเพียงแค่มอบหมายมัน
mikeserv

1
จุดดี - อาจต้องใช้ความพยายามมากกว่านี้ คิดดูไม่ดี
mikeserv

1

หากตั้งค่ากับดักใด ๆ บน ERR จะได้รับมรดกโดยฟังก์ชั่นของเชลล์การแทนที่คำสั่งและคำสั่งที่ดำเนินการในสภาพแวดล้อมย่อย

ในสคริปต์ของคุณมันเป็นการดำเนินการคำสั่ง ( echo $( made up name )) ในคำสั่ง bash ถูกคั่นด้วย; หรือมีการขึ้นบรรทัดใหม่ ในการออกคำสั่ง

echo $( made up name )

$( made up name )ถือเป็นส่วนหนึ่งของคำสั่ง แม้ว่าส่วนนี้จะล้มเหลวและกลับมาพร้อมกับข้อผิดพลาดคำสั่งทั้งหมดดำเนินการได้สำเร็จโดยที่echoไม่ทราบ เมื่อคำสั่งส่งคืนโดยไม่มี 0 กับดักจะถูกทริกเกอร์

คุณต้องใส่มันในสองคำสั่งการกำหนดและ echo

var=$(made_up_name)
echo $var

echo $varไม่ล้มเหลว - $varถูกขยายให้ว่างเปล่าก่อนที่echoจะได้เห็นจริง ๆ
mikeserv

0

นี่เป็นเพราะข้อผิดพลาดในการทุบตี ระหว่างขั้นตอนการทดแทนคำสั่ง

echo $( made up name )

maderun (หรือไม่สามารถพบได้) ใน subshell แต่ subshell นั้น "ดีที่สุด" ในลักษณะที่มันไม่ได้ใช้กับดักบางอย่างจาก parent parent สิ่งนี้ได้รับการแก้ไขในเวอร์ชัน 4.4.5 :

ภายใต้สถานการณ์บางอย่างคำสั่งง่าย ๆ ถูกปรับให้เหมาะสมเพื่อกำจัดทางแยกทำให้เกิดกับดัก EXIT ที่ไม่ถูกเรียกใช้งาน

ด้วย bash 4.4.5 หรือสูงกว่าคุณควรเห็นผลลัพธ์ต่อไปนี้:

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

ตัวจัดการแทร็บได้รับการเรียกตามที่คาดไว้จากนั้นจึงออกจากเชลล์ย่อย ( set -eทำให้เกิด subshell เพื่อออกเท่านั้นไม่ใช่พาเรนต์เพื่อให้ได้ข้อความ "ไม่ควรไปถึง" ในความเป็นจริงควรเข้าถึงได้)

วิธีหลีกเลี่ยงปัญหาสำหรับรุ่นเก่าคือการบังคับให้สร้าง subshell แบบเต็มและไม่เหมาะ:

echo $( ( made up name ) )

จำเป็นต้องมีช่องว่างเพิ่มเติมเพื่อแยกความแตกต่างจากการขยายตัวทางคณิตศาสตร์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.