ฉันจะได้รับทุบตีเพื่อออกจากความล้มเหลว backtick ในลักษณะที่คล้ายกับ pipefail ได้อย่างไร


55

ดังนั้นฉันชอบที่จะทำให้สคริปต์ทุบตีของฉันแข็งขึ้นทุกที่ที่ฉันสามารถทำได้ (และเมื่อไม่สามารถมอบหมายให้เป็นภาษาเช่น Python / Ruby) เพื่อให้แน่ใจว่าข้อผิดพลาดจะไม่หายไป

ในหลอดเลือดดำนั้นฉันมีเคร่งครัด. sh ซึ่งมีสิ่งต่าง ๆ เช่น:

set -e
set -u
set -o pipefail

และแหล่งที่มาในสคริปต์อื่น ๆ อย่างไรก็ตามในขณะที่ pipefail จะรับ:

false | echo it kept going | true

มันจะไม่รับ:

echo The output is '`false; echo something else`' 

ผลลัพธ์จะเป็น

ผลลัพธ์คือ ''

False ส่งคืนสถานะที่ไม่เป็นศูนย์และไม่มีการ stdout ในไปป์มันจะล้มเหลว แต่ที่นี่ไม่มีข้อผิดพลาดเกิดขึ้น เมื่อนี่เป็นการคำนวณที่เก็บไว้ในตัวแปรจริงในภายหลังและตั้งค่าเป็นค่าว่างนี่อาจทำให้เกิดปัญหาในภายหลัง

ดังนั้น - มีวิธีรับทุบตีในการรักษาผลตอบแทนที่ไม่เป็นศูนย์ภายใน backtick เป็นเหตุผลเพียงพอที่จะออก?

คำตอบ:


51

ภาษาที่แน่นอนที่ใช้ในสเปคUNIX เดียวเพื่ออธิบายความหมายของset -eคือ:

เมื่อเปิดใช้ตัวเลือกนี้หากคำสั่งอย่างง่ายล้มเหลวด้วยเหตุผลใดก็ตามที่ระบุไว้ในผลที่ตามมาของข้อผิดพลาดของเชลล์หรือส่งกลับค่าสถานะการออก> 0 และไม่ใช่ [คำสั่งแบบมีเงื่อนไขหรือไม่มีเงื่อนไข] เชลล์จะออกทันที

มีความคลุมเครือเป็นสิ่งที่เกิดขึ้นคือเมื่อคำสั่งดังกล่าวเกิดขึ้นในsubshell จากมุมมองที่ใช้งานได้จริงเชลล์ย่อยทั้งหมดสามารถทำได้คือออกและส่งคืนสถานะที่ไม่ใช่ศูนย์ไปยังเชลล์พาเรนต์ ไม่ว่าพาเรนต์เชลล์จะเปิดออกจะขึ้นอยู่กับว่าสถานะที่ไม่ใช่ศูนย์นี้แปลเป็นคำสั่งง่าย ๆ ที่ล้มเหลวในพาเรนต์เชลล์หรือไม่

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

โปรดทราบว่าสิ่งนี้จะดำเนินการตามที่ตั้งใจไว้เฉพาะในกรณีที่มีการทดแทนคำสั่งเดียวเนื่องจากสถานะการทดแทนครั้งล่าสุดเท่านั้นที่จะถูกนำมาพิจารณา ตัวอย่างเช่นคำสั่งต่อไปนี้สำเร็จ (ทั้งตามมาตรฐานและในการนำไปใช้ทุกครั้งที่ฉันเห็น):

a=$(false)$(echo foo)

กรณีที่จะดูก็คือsubshells อย่างชัดเจน(somecommand) : ตามการตีความด้านบนเชลล์ย่อยอาจส่งคืนสถานะที่ไม่ใช่ศูนย์ แต่เนื่องจากนี่ไม่ใช่คำสั่งง่ายๆในเชลล์พาเรนต์เชลล์พาเรนต์ควรดำเนินการต่อ ในความเป็นจริงกระสุนทั้งหมดที่ฉันรู้จักทำให้ผู้ปกครองกลับมาที่จุดนี้ แม้ว่าสิ่งนี้จะมีประโยชน์ในหลาย ๆ กรณีเช่นในกรณี(cd /some/dir && somecommand)ที่มีการใช้วงเล็บเพื่อให้การดำเนินการเช่นไดเรกทอรีปัจจุบันเปลี่ยนไปภายในเครื่อง แต่จะเป็นการละเมิดข้อกำหนดหากset -eถูกปิดใน subshell หรือถ้า subshell ส่งคืนสถานะที่ไม่ใช่ศูนย์ในลักษณะที่ จะไม่ยุติมันเช่นการใช้!คำสั่งจริง ตัวอย่างเช่นทั้งหมดของ ash, bash, pdksh, ksh93 และ zsh exit โดยไม่แสดงfooในตัวอย่างต่อไปนี้:

set -e; (set +e; false); echo "This should be displayed"
set -e; (! true); echo "This should be displayed"

ยังไม่มีคำสั่งง่ายๆที่ล้มเหลวในขณะที่set -eมีผลบังคับใช้!

กรณีที่มีปัญหาประการที่สามคือองค์ประกอบในไปป์ไลน์ที่ไม่น่าสนใจ ในทางปฏิบัติเชลล์ทั้งหมดไม่สนใจความล้มเหลวขององค์ประกอบของไปป์ไลน์นอกเหนือจากที่ผ่านมาและแสดงพฤติกรรมหนึ่งในสองอย่างที่เกี่ยวข้องกับองค์ประกอบไปป์ไลน์สุดท้าย:

  • ATT ksh และ zsh ซึ่งเรียกใช้งานองค์ประกอบสุดท้ายของไพพ์ไลน์ในพาเรนต์เชลล์ทำธุรกิจตามปกติ: หากคำสั่งง่าย ๆ ล้มเหลวในอิลิเมนต์สุดท้ายของไพพ์ไลน์เชลล์ดำเนินการคำสั่งนั้นซึ่งเกิดขึ้นเป็นเปลือกหลัก ทางออก
  • กระสุนอื่น ๆ ประมาณพฤติกรรมโดยออกหากองค์ประกอบสุดท้ายของไปป์ไลน์คืนสถานะที่ไม่ใช่ศูนย์

เหมือนก่อนหน้าการปิดset -eหรือใช้การปฏิเสธในองค์ประกอบสุดท้ายของไปป์ไลน์ทำให้เกิดการส่งคืนสถานะที่ไม่ใช่ศูนย์ในวิธีที่ไม่ควรยกเลิกเชลล์ เชลล์อื่นที่ไม่ใช่ ATT ksh และ zsh จะออกจาก

pipefailตัวเลือกของ Bash ทำให้ไปป์ไลน์เพื่อออกจากทันทีภายใต้set -eองค์ประกอบใด ๆ ที่ส่งคืนสถานะที่ไม่ใช่ศูนย์

โปรดทราบว่าเป็นภาวะแทรกซ้อนเพิ่มเติมทุบตีปิดset -eใน subshells เว้นแต่ว่าจะอยู่ในโหมด POSIX ( set -o posixหรือมีPOSIXLY_CORRECTในสภาพแวดล้อมเมื่อเริ่มทุบตี)

ทั้งหมดนี้แสดงให้เห็นว่าข้อมูลจำเพาะ POSIX โชคไม่ดีที่งานระบุ-eตัวเลือก โชคดีที่กระสุนที่มีอยู่ส่วนใหญ่มีความสอดคล้องในพฤติกรรมของพวกเขา


ขอบคุณสำหรับสิ่งนี้. ประสบการณ์ที่ฉันมีก็คือข้อผิดพลาดบางอย่างที่ติดอยู่ใน bash รุ่นใหม่กว่านั้นถูกละเว้นในเวอร์ชันก่อนหน้าด้วย set -e ความตั้งใจของฉันที่นี่คือการทำให้สคริปต์แข็งขึ้นจนเกินขอบเขตที่เงื่อนไขการส่งคืน / ล้มเหลวข้อผิดพลาดใด ๆ ที่ไม่ได้รับการจัดการจะทำให้สคริปต์ออก นี่เป็นระบบแบบดั้งเดิมที่รู้จักกันในการผลิตไฟล์เอาต์พุตขยะและรหัสทางออกแฮปปี้ "0" หลังจากครึ่งชั่วโมงแห่งความโกลาหลที่มี env ผิด - เว้นแต่คุณจะดูผลลัพธ์เหมือนเหยี่ยว (และไม่ใช่ข้อผิดพลาดทั้งหมด) อยู่ใน stderr บางส่วนอยู่ใน stdout และบางส่วนเป็น / dev / null'd) คุณก็ไม่รู้
Danny Staple

"ตัวอย่างเช่นทางออกของเถ้า bash, pdksh, ksh93 และ zsh ทั้งหมดโดยไม่แสดง foo บนตัวอย่างต่อไปนี้": BusyBox ash ไม่ทำงานวิธีนั้นจะแสดงผลลัพธ์ตามข้อมูลจำเพาะ (ทดสอบกับ BusyBox 1.19.4.) set -e; (cd /nonexisting)หรือไม่ก็ออกจากที่มี
dubiousjim

27

(ตอบคำถามของฉันเองเพราะฉันพบวิธีแก้ปัญหา) วิธีแก้ปัญหาหนึ่งคือการกำหนดสิ่งนี้ให้กับตัวแปรกลางเสมอ วิธี$?ตั้งค่ารหัสส่งคืน ( ) นี้

ดังนั้น

ABC=`exit 1`
echo $?

จะส่งออก1(หรือแทนที่จะออกหากset -eมี) อย่างไรก็ตาม:

echo `exit 1`
echo $?

จะแสดงผล0หลังจากบรรทัดว่าง โค้ดส่งคืนของ echo (หรือคำสั่งอื่นที่รันด้วยเอาต์พุต backtick) จะแทนที่รหัสส่งคืน 0

ฉันยังคงเปิดรับโซลูชั่นที่ไม่ต้องการตัวแปรระดับกลาง แต่สิ่งนี้ทำให้ฉันได้รับวิธีการบางอย่าง


23

เมื่อ OP ชี้ให้เห็นในคำตอบของเขาเองการกำหนดเอาต์พุตของคำสั่งย่อยให้กับตัวแปรจะช่วยแก้ปัญหาได้ $?ที่เหลือได้รับบาดเจ็บ

อย่างไรก็ตามกรณีที่ขอบหนึ่งยังสามารถไขปริศนาคุณด้วยค่าลบที่ผิดพลาด (เช่นคำสั่งล้มเหลว แต่ข้อผิดพลาดไม่ได้ทำให้เกิดฟอง) localการประกาศตัวแปร:

local myvar=$(subcommand)จะกลับมาเสมอ0 !

bash(1) ชี้ให้เห็นสิ่งนี้:

   local [option] [name[=value] ...]
          ... The return status is 0 unless local is used outside a function,
          an invalid name is supplied, or name is a readonly variable.

นี่คือกรณีทดสอบง่ายๆ:

#!/bin/bash

function test1() {
  data1=$(false) # undeclared variable
  echo 'data1=$(false):' "$?"
  local data2=$(false) # declaring and assigning in one go
  echo 'local data2=$(false):' "$?"
  local data3
  data3=$(false) # assigning a declared variable
  echo 'local data3; data3=$(false):' "$?"
}

test1

ผลลัพธ์:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1

ขอบคุณความไม่ลงรอยกันระหว่างVAR=...และlocal VAR=...ทำให้ฉันผิดหวังจริงๆ!
จอนนี่

13

ดังที่คนอื่น ๆ บอกว่าlocalจะคืนค่า 0 เสมอทางออกคือการประกาศตัวแปรก่อน:

function testcase()
{
    local MYRESULT

    MYRESULT=$(false)
    if (( $? != 0 )); then
        echo "False returned false!"
        return 1
    fi

    return 0
}

เอาท์พุท:

$ testcase
False returned false!
$ 

4

หากต้องการออกจากความล้มเหลวในการทดแทนคำสั่งคุณอาจตั้งค่า-eในเชลล์ย่อยอย่างชัดเจนดังนี้:

set -e
x=$(set -e; false; true)
echo "this will never be shown"

1

จุดที่น่าสนใจ!

ฉันไม่เคยพบเจอสิ่งนั้นเลยเพราะฉันไม่ใช่เพื่อนของset -e(แทนที่จะชอบtrap ... ERR) แต่ได้ทำการทดสอบแล้วว่า: trap ... ERRอย่าจับข้อผิดพลาดภายใน$(...)(หรือ backticks แบบเก่า ๆ )

ฉันคิดว่าปัญหาคือ (บ่อยมาก) ที่นี่ subshell ถูกเรียกและ-eชัดเจนหมายถึงเปลือกปัจจุบัน

มีเพียงโซลูชันอื่น ๆ ที่นึกถึงในขณะนี้เท่านั้นที่จะใช้อ่าน:

 ls -l ghost_under_bed | read name

การขว้างERRและ-eการใช้กระสุนจะจบลง ปัญหาเท่านั้น: ใช้งานได้เฉพาะกับคำสั่งที่มีหนึ่งบรรทัดเอาต์พุต


1
ไม่แน่ใจเกี่ยวกับเคล็ดลับการอ่านโดยพยายามนำไปสู่ตัวแปรที่ไม่ถูกผูกไว้ ฉันคิดว่าอาจเป็นเพราะส่วนอื่น ๆ ของไปป์นั้นเป็น subshell อย่างมีประสิทธิภาพชื่อจะไม่สามารถใช้ได้
Danny Staple
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.