รับสถานะทางออกของกระบวนการที่ถูกส่งไปยังกระบวนการอื่น


287

ฉันมีสองกระบวนการfooและbarเชื่อมต่อกับท่อ:

$ foo | bar

barออกจาก 0 เสมอ fooฉันสนใจในรหัสทางออกของ มีวิธีใดบ้างที่จะไปถึงที่นั่น?


คำตอบ:


256

หากคุณกำลังใช้bashคุณสามารถใช้PIPESTATUSตัวแปรอาร์เรย์เพื่อรับสถานะการออกของแต่ละองค์ประกอบของไปป์ไลน์

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

หากคุณกำลังใช้zshงานอาร์เรย์เหล่านั้นจะถูกเรียกpipestatus(case case!) และดัชนี array เริ่มต้นที่หนึ่ง:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

หากต้องการรวมเข้าด้วยกันในฟังก์ชั่นในลักษณะที่ไม่สูญเสียค่า:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

เรียกใช้ข้างต้นในbashหรือzshและคุณจะได้ผลลัพธ์เดียวกัน มีเพียงหนึ่งรายการเท่านั้นretval_bashและretval_zshจะถูกตั้งค่า อีกอันจะว่างเปล่า สิ่งนี้จะทำให้ฟังก์ชั่นลงท้ายด้วยreturn $retval_bash $retval_zsh(หมายเหตุการขาดเครื่องหมายคำพูด!)


9
และpipestatusใน zsh น่าเสียดายที่กระสุนอื่นไม่มีคุณสมบัตินี้
Gilles

8
หมายเหตุ: อาร์เรย์ใน zsh เริ่มต้นโดยไม่echo "$pipestatus[1]" "$pipestatus[2]"ตั้งใจที่ดัชนี 1 ดังนั้นจึงเป็น
Christoph Wurm

5
คุณสามารถตรวจสอบขั้นตอนทั้งหมดเช่นนี้ได้:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0

7
@JanHudec: บางทีคุณควรอ่านคำตอบห้าคำแรกของฉัน นอกจากนี้กรุณาชี้ให้เห็นว่าคำถามที่ขอคำตอบเพียง POSIX
camh

12
@JanHudec: ไม่ได้ติดแท็ก POSIX ทำไมคุณคิดว่าคำตอบต้องเป็น POSIX ไม่ได้ระบุดังนั้นฉันจึงให้คำตอบที่ผ่านการรับรอง ไม่มีอะไรไม่ถูกต้องเกี่ยวกับคำตอบของฉันรวมทั้งมีหลายคำตอบในการแก้ไขกรณีอื่น
camh

237

การทำเช่นนี้มี 3 วิธี:

Pipefail

วิธีแรกคือการตั้งค่าpipefailตัวเลือก ( ksh, zshหรือbash) นี่คือวิธีที่ง่ายที่สุดและสิ่งที่มันทำคือการตั้งค่าสถานะ$?การออกเป็นรหัสทางออกของโปรแกรมสุดท้ายเพื่อออกไม่ใช่ศูนย์ (หรือศูนย์ถ้าออกทั้งหมดประสบความสำเร็จ)

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash ยังมีตัวแปรอาเรย์ที่เรียกว่า$PIPESTATUS( $pipestatusในzsh) ซึ่งมีสถานะทางออกของโปรแกรมทั้งหมดในขั้นตอนสุดท้าย

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

คุณสามารถใช้ตัวอย่างคำสั่งที่ 3 เพื่อรับค่าเฉพาะในไปป์ไลน์ที่คุณต้องการ

แยกการประหารชีวิต

นี่เป็นวิธีแก้ปัญหาที่ไม่สะดวกที่สุด เรียกใช้แต่ละคำสั่งแยกจากกันและจับภาพสถานะ

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

2
ยี้! ฉันเพิ่งจะโพสต์เกี่ยวกับ PIPESTATUS
slm

1
สำหรับการอ้างอิงมีเทคนิคอื่น ๆ ที่กล่าวถึงในคำถาม SO นี้: stackoverflow.com/questions/1221833/…
slm

1
@Patrick the pipestatus solution ทำงานบน bash, quastion เพิ่มเติมในกรณีที่ฉันใช้ ksh script คุณคิดว่าเราสามารถหาบางอย่างที่คล้ายกับ pipestatus ได้หรือไม่? , (ในขณะเดียวกันฉันเห็น pipestatus ที่ไม่รองรับโดย ksh)
yael

2
@ Yael ฉันไม่ได้ใช้kshแต่จากคร่าว ๆ มัน manpage มันไม่สนับสนุน$PIPESTATUSหรืออะไรที่คล้ายกัน มันสนับสนุนpipefailตัวเลือกว่า
Patrick

1
ฉันตัดสินใจไปกับ pipefail เพราะมันทำให้ฉันได้รับสถานะคำสั่งที่ล้มเหลวที่นี่:LOG=$(failed_command | successful_command)
vmrob

51

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

สถานการณ์:

someprog | filter

คุณต้องการสถานะออกจากและเอาท์พุทจากsomeprogfilter

นี่คือทางออกของฉัน:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

ผลลัพธ์ของการสร้างนี้คือ stdout จากfilterเป็น stdout ของการสร้างและสถานะการออกจากsomeprogเป็นสถานะการออกของการก่อสร้าง


โครงสร้างนี้ยังทำงานร่วมกับคำสั่งง่ายๆการจัดกลุ่ม{...}แทน (...)subshells subshells มีความหมายบางอย่างในหมู่คนอื่น ๆ ค่าใช้จ่ายในการปฏิบัติงานซึ่งเราไม่ต้องการที่นี่ อ่านคู่มือทุบตีละเอียดสำหรับรายละเอียดเพิ่มเติม: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

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

สำหรับส่วนที่เหลือของข้อความนี้ฉันจะใช้ตัวแปรย่อย


ตัวอย่างsomeprogและfilter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

ตัวอย่างผลลัพธ์:

filtered line1
filtered line2
filtered line3
42

หมายเหตุ: กระบวนการย่อยสืบทอดตัวอธิบายไฟล์ที่เปิดอยู่จากพาเรนต์ นั่นหมายความว่าsomeprogจะสืบทอด open file descriptor 3 และ 4 ถ้าsomeprogเขียนไปยัง file descriptor 3 ดังนั้นนั่นจะกลายเป็นสถานะการออก สถานะการออกจริงจะถูกละเว้นเพราะreadจะอ่านเพียงครั้งเดียว

หากคุณกังวลว่าคุณsomeprogอาจจะเขียนไปยังแฟ้มบ่ง 3 หรือ 4 someprogแล้วมันเป็นที่ดีที่สุดที่จะปิดอธิบายไฟล์ก่อนที่จะเรียก

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

exec 3>&- 4>&-ก่อนที่จะsomeprogปิดอธิบายไฟล์ก่อนที่จะดำเนินsomeprogดังนั้นสำหรับsomeprogผู้ที่อธิบายไฟล์ก็ไม่อยู่

มันสามารถเขียนได้เช่นนี้ someprog 3>&- 4>&-


คำอธิบายทีละขั้นตอนของการสร้าง:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

จากล่างขึ้นบน:

  1. เชลล์ย่อยถูกสร้างขึ้นด้วย file descriptor 4 ถูกเปลี่ยนเส้นทางไปยัง stdout ซึ่งหมายความว่าสิ่งที่พิมพ์ไปยัง file descriptor 4 ใน subshell จะจบลงด้วย stdout ของโครงสร้างทั้งหมด
  2. มีการสร้างไปป์และคำสั่งทางด้านซ้าย ( #part3) และขวา ( #part2) จะถูกดำเนินการ exit $xsเป็นคำสั่งสุดท้ายของไปป์และนั่นหมายความว่าสตริงจาก stdin จะเป็นสถานะทางออกของการสร้างทั้งหมด
  3. เชลล์ย่อยถูกสร้างขึ้นด้วย file descriptor 3 ถูกเปลี่ยนเส้นทางไปยัง stdout ซึ่งหมายความว่าอะไรก็ตามที่ถูกพิมพ์ไปยัง file descriptor 3 ใน subshell นี้จะจบลง#part2และจะเป็นสถานะทางออกของการสร้างทั้งหมด
  4. มีการสร้างไปป์และคำสั่งทางด้านซ้าย ( #part5และ#part6) และขวา ( filter >&4) จะถูกดำเนินการ เอาต์พุตของfilterถูกเปลี่ยนทิศทางไปยัง file descriptor 4 ใน#part1ไฟล์ descriptor 4 ถูกเปลี่ยนทิศทางไปที่ stdout ซึ่งหมายความว่าผลลัพธ์ของfilterคือ stdout ของการสร้างทั้งหมด
  5. สถานะออกจาก#part6จะมีการพิมพ์ที่จะยื่นบ่ง 3. ใน#part3ไฟล์อธิบาย 3 #part2ถูกเปลี่ยนเส้นทางไปยัง ซึ่งหมายความว่าสถานะการออกจาก#part6จะเป็นสถานะการออกสุดท้ายสำหรับการก่อสร้างทั้งหมด
  6. someprogถูกประหารชีวิต #part5ออกจากสถานะที่มีการดำเนินการใน stdout จะนำโดยท่อในและส่งต่อไปยัง#part4 filterเอาต์พุตจากfilterจะถึง stdout ตามที่อธิบายไว้ใน#part4

1
ดี สำหรับฟังก์ชั่นที่ฉันทำได้(read; exit $REPLY)
jthill

5
(exec 3>&- 4>&-; someprog)someprog 3>&- 4>&-ช่วยลดความยุ่งยากในการ
Roger Pate

วิธีนี้ใช้ได้โดยไม่มี subshells:{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
josch

36

แม้ว่าจะไม่ใช่สิ่งที่คุณถามคุณสามารถใช้

#!/bin/bash -o pipefail

เพื่อให้ไพพ์ของคุณส่งกลับค่าไม่ใช่ศูนย์สุดท้าย

อาจเป็นรหัสน้อย

แก้ไข: ตัวอย่าง

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1

9
set -o pipefailbash foo.shภายในสคริปต์ที่ควรจะมีประสิทธิภาพมากขึ้นเช่นในกรณีที่มีคนรันสคริปต์ผ่าน
maxschlepzig

มันทำงานอย่างไร คุณมีตัวอย่างหรือไม่
Johan

2
โปรดทราบว่าไม่ได้-o pipefailอยู่ใน POSIX
scan

2
สิ่งนี้ใช้ไม่ได้ใน BASH 3.2.25 (1) - ปล่อย ที่ด้านบนของ / tmp / ff #!/bin/bash -o pipefailฉันมี ข้อผิดพลาดคือ:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Felipe Alvarez

2
@FelipeAlvarez: สภาพแวดล้อมบางอย่าง (รวมถึง linux) ไม่แยกวิเคราะห์ช่องว่างบน#!บรรทัดที่เกินจากบรรทัดแรกและดังนั้นสิ่งนี้จะกลายเป็น/bin/bash -o pipefail /tmp/ffแทนที่จะเป็นสิ่งที่จำเป็น/bin/bash -o pipefail /tmp/ff- getopt(หรือคล้ายกัน) การแยกวิเคราะห์โดยใช้รายการoptargซึ่งเป็นรายการถัดไปARGVเป็นอาร์กิวเมนต์ ถึง-oดังนั้นจึงล้มเหลว หากคุณต้องการทำเสื้อคลุม (พูดbash-pfเพียงแค่ทำexec /bin/bash -o pipefail "$@"และวางไว้ใน#!บรรทัดที่จะทำงานดูเพิ่มเติมที่: en.wikipedia.org/wiki/Shebang_%28Unix%29
lindes

22

สิ่งที่ฉันทำเมื่อเป็นไปได้คือการให้อาหารรหัสทางออกจากเข้าfoo barตัวอย่างเช่นหากฉันรู้ว่าfooไม่เคยสร้างบรรทัดที่มีเพียงตัวเลขฉันก็สามารถวางรหัสทางออกได้

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

หรือถ้าฉันรู้ว่าผลลัพธ์จากfooไม่เคยมีบรรทัดที่มีเพียงแค่.:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

สิ่งนี้สามารถทำได้หากมีวิธีการbarทำงานทั้งหมดยกเว้นบรรทัดสุดท้ายและส่งผ่านบรรทัดสุดท้ายเป็นรหัสออก

หากbarเป็นไพพ์ไลน์ที่ซับซ้อนที่คุณไม่ต้องการเอาต์พุตคุณสามารถข้ามส่วนของมันได้โดยพิมพ์รหัสออกบนตัวอธิบายไฟล์อื่น

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

หลังจากนี้$exit_codesมักเป็นfoo:X bar:Yไปได้ แต่อาจเป็นไปได้bar:Y foo:Xว่าbarก่อนที่จะอ่านอินพุตทั้งหมดหรือหากคุณโชคไม่ดี ฉันคิดว่าการเขียนไปยังไพพ์ไลน์สูงสุด 512 ไบต์เป็นอะตอมมิกในทุกยูนิเซฟดังนั้นfoo:$?และbar:$?ส่วนต่างๆจะไม่ถูกรวมกันตราบใดที่สตริงแท็กมีขนาดไม่เกิน 507 ไบต์

หากคุณต้องการจับเอาท์พุทจากbarมันจะยาก คุณสามารถรวมเทคนิคต่าง ๆ ข้างต้นได้โดยจัดเรียงเอาต์พุตbarไม่ให้มีบรรทัดที่ดูเหมือนตัวบ่งชี้รหัสการออก แต่มันจะดูยุ่งเหยิง

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

และแน่นอนว่ามีตัวเลือกง่าย ๆ ในการใช้ไฟล์ชั่วคราวเพื่อเก็บสถานะ เรียบง่าย แต่ไม่ว่าง่ายในการผลิต:

  • หากมีหลายสคริปต์ทำงานพร้อมกันหรือหากสคริปต์เดียวกันใช้วิธีนี้ในหลาย ๆ ที่คุณต้องตรวจสอบให้แน่ใจว่าพวกเขาใช้ชื่อไฟล์ชั่วคราวที่แตกต่างกัน
  • การสร้างไฟล์ชั่วคราวอย่างปลอดภัยในไดเรกทอรีที่แชร์นั้นยาก บ่อยครั้งที่/tmpเป็นที่เดียวที่สคริปต์จะสามารถเขียนไฟล์ได้ ใช้mktempงานซึ่งไม่ใช่ POSIX แต่มีให้บริการใน unices ร้ายแรงทั้งหมดในปัจจุบัน
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

1
เมื่อใช้วิธีการไฟล์ชั่วคราวฉันต้องการเพิ่มกับดักสำหรับ EXIT ที่ลบไฟล์ชั่วคราวทั้งหมดเพื่อไม่ให้มีขยะในขณะที่สคริปต์ตาย
miracle173

17

เริ่มต้นจากท่อ:

foo | bar | baz

นี่คือโซลูชันทั่วไปที่ใช้ POSIX เชลล์เท่านั้นและไม่มีไฟล์ชั่วคราว:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses มีรหัสสถานะของกระบวนการที่ล้มเหลวใด ๆ ตามลำดับแบบสุ่มพร้อมดัชนีเพื่อบอกคำสั่งที่ปล่อยแต่ละสถานะ

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

สังเกตคำพูด$error_statusesในการทดสอบของฉัน; หากไม่มีพวกเขาgrepจะไม่สามารถแยกความแตกต่างได้เนื่องจากบรรทัดใหม่ถูกบังคับให้มีช่องว่าง


12

ดังนั้นฉันจึงอยากให้คำตอบเหมือน Lesmana แต่ฉันคิดว่าของฉันอาจจะง่ายกว่าและบริสุทธิ์กว่าเล็กน้อย - Bourne-shell solution:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

ฉันคิดว่านี่เป็นคำอธิบายที่ดีที่สุดจากภายในออก - command1 จะดำเนินการและพิมพ์เอาต์พุตปกติบน stdout (file descriptor 1) จากนั้นเมื่อเสร็จแล้ว printf จะดำเนินการและพิมพ์รหัสทางออกของ command1 บน stdout แต่ stdout นั้นถูกเปลี่ยนเส้นทางไป ตัวอธิบายไฟล์ 3.

ในขณะที่ command1 กำลังทำงาน stdout ของมันจะถูกไพพ์ไปที่ command2 (เอาต์พุตของ printf ไม่เคยทำให้มันเป็น command2 เพราะเราส่งมันไปที่ file descriptor 3 แทนที่จะเป็น 1 ซึ่งเป็นสิ่งที่ pipe อ่าน จากนั้นเราเปลี่ยนเส้นทางเอาต์พุตของ command2 ไปยัง file descriptor 4 เพื่อให้มันยังคงอยู่จาก file descriptor 1 - เพราะเราต้องการ file descriptor 1 ฟรีอีกเล็กน้อยในภายหลังเพราะเราจะนำเอา printf บน file descriptor 3 1 - เพราะนั่นคือสิ่งที่การทดแทนคำสั่ง (backticks) จะจับภาพและนั่นคือสิ่งที่จะถูกวางลงในตัวแปร

บิตสุดท้ายของเวทมนตร์คือสิ่งแรกที่exec 4>&1เราทำในฐานะคำสั่งแยกต่างหาก - มันเปิดไฟล์ descriptor 4 เป็นสำเนาของ stdout ของเปลือกนอก การทดแทนคำสั่งจะจับสิ่งที่เขียนบนมาตรฐานออกมาจากมุมมองของคำสั่งภายใน - แต่เนื่องจากเอาต์พุตของ command2 จะไปที่ไฟล์ descriptor 4 เท่าที่เกี่ยวข้องกับการทดแทนคำสั่งการแทนที่คำสั่งจะไม่จับ - อย่างไรก็ตาม เมื่อได้รับ "ออก" ของการทดแทนคำสั่งมันยังคงเป็นไปอย่างมีประสิทธิภาพเพื่ออธิบายไฟล์โดยรวมของสคริปต์ 1

( exec 4>&1จะต้องมีคำสั่งแยกต่างหากเพราะเชลล์ทั่วไปจำนวนมากไม่ชอบเมื่อคุณพยายามเขียนไปยังไฟล์ descriptor ภายในการทดแทนคำสั่งที่เปิดในคำสั่ง "ภายนอก" ที่ใช้การทดแทนดังนั้นนี่คือ วิธีพกพาที่ง่ายที่สุดที่จะทำ)

คุณสามารถดูได้ในเชิงเทคนิคน้อยและขี้เล่นมากขึ้นราวกับว่าเอาต์พุตของคำสั่งกระโดดข้ามกัน: command1 ไพพ์ไปยัง command2 ดังนั้นเอาต์พุตของ printf ข้ามคำสั่ง 2 ดังนั้น command2 จึงไม่จับแล้ว เอาต์พุตของคำสั่ง 2 กระโดดข้ามไปและออกจากการทดแทนคำสั่งเช่นเดียวกับ printf ลงทันเวลาที่จะถูกดักจับโดยการทดแทนเพื่อให้มันจบลงในตัวแปรและเอาต์พุตของ command2 ดำเนินไปในลักษณะที่ถูกเขียนไปยังเอาต์พุตมาตรฐาน ในท่อปกติ

นอกจากนี้ตามที่ฉันเข้าใจ$?จะยังคงมีโค้ดส่งคืนของคำสั่งที่สองในไพพ์เนื่องจากการกำหนดตัวแปรการแทนที่คำสั่งและคำสั่งผสมนั้นโปร่งใสทั้งหมดในโค้ดส่งคืนของคำสั่งภายในดังนั้นสถานะการส่งคืนของ command2 ควรแพร่กระจายออกไป - สิ่งนี้และไม่ต้องกำหนดฟังก์ชั่นเพิ่มเติมคือเหตุผลที่ฉันคิดว่านี่อาจเป็นทางออกที่ดีกว่าที่ lesmana เสนอ

ตามที่ระบุใน lesmana lesmana อาจเป็นไปได้ว่าคำสั่ง 1 จะจบลงด้วยการใช้ file descriptors 3 หรือ 4 เพื่อให้มีประสิทธิภาพมากขึ้นคุณจะต้อง:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

โปรดทราบว่าฉันใช้คำสั่งผสมในตัวอย่างของฉัน แต่ใช้ subshells (การใช้( )แทน{ }จะใช้ได้แม้ว่าอาจจะมีประสิทธิภาพน้อยลง)

คำสั่งสืบทอดไฟล์ descriptor จากกระบวนการที่เรียกใช้ดังนั้นบรรทัดที่สองทั้งหมดจะสืบทอดไฟล์ descriptor สี่และคำสั่งผสมตามด้วย3>&1จะสืบทอดสืบทอด descriptor ไฟล์สาม ดังนั้นการ4>&-ทำให้แน่ใจว่าคำสั่งผสมภายในจะไม่สืบทอดตัวอธิบายไฟล์สี่และ3>&-จะไม่สืบทอดตัวอธิบายไฟล์สามดังนั้น command1 จะได้รับ 'สภาพแวดล้อมที่สะอาดกว่า' และเป็นมาตรฐานมากขึ้น คุณสามารถขยับด้านใน4>&-ข้างๆ3>&-แต่ฉันคิดว่าทำไมไม่ จำกัด ขอบเขตเท่าที่จะทำได้

ฉันไม่แน่ใจว่าบ่อยครั้งที่สิ่งต่าง ๆ ใช้ file descriptor สามหรือสี่โดยตรง - ฉันคิดว่าโปรแกรมส่วนใหญ่ใช้ syscalls ที่ส่งคืนไฟล์ descriptor ที่ไม่ได้ใช้ที่ไม่ได้ใช้แล้ว แต่บางครั้งโค้ดเขียนไปยัง file descriptor 3 โดยตรงฉัน เดา (ฉันนึกภาพออกว่าโปรแกรมกำลังตรวจสอบ file descriptor เพื่อดูว่ามันเปิดอยู่หรือไม่และใช้มันถ้ามันเป็นหรือทำงานต่างไปตามนั้นถ้าไม่ใช่) ดังนั้นสิ่งหลังน่าจะดีที่สุดที่คุณควรคำนึงถึงและใช้สำหรับกรณีทั่วไป


ดูน่าสนใจ แต่ฉันไม่สามารถเข้าใจได้อย่างที่คุณคาดหวังว่าคำสั่งนี้จะทำและคอมพิวเตอร์ของฉันก็ไม่สามารถทำได้เช่นกัน -bash: 3: Bad file descriptorฉันได้รับ
G-Man

@ G-Man ใช่แล้วฉันลืมไปว่าทุบตีไม่รู้ว่ามันกำลังทำอะไรเมื่อพูดถึงตัวอธิบายไฟล์ซึ่งแตกต่างจากกระสุนที่ฉันใช้โดยทั่วไป (ขี้เถ้าที่มาพร้อมกับช่องว่าง) ฉันจะแจ้งให้คุณทราบเมื่อฉันคิดถึงการแก้ปัญหาที่ทำให้ทุบตีมีความสุข ในระหว่างนี้หากคุณมีกล่องเดเบียนสะดวกคุณสามารถลองใช้งานได้ในทันทีหรือหากคุณมีช่องว่างว่างคุณสามารถลองใช้กับช่องยุ่ง ๆ
mtraceur

@ G-Man สำหรับสิ่งที่ฉันคาดว่าคำสั่งจะทำและสิ่งที่มันทำในเปลือกอื่น ๆ คือการเปลี่ยนเส้นทาง stdout จาก command1 ดังนั้นจึงไม่ได้รับการติดตั้งโดยการทดแทนคำสั่ง แต่เมื่อนอกการทดแทนคำสั่งมันจะลดลง fd3 กลับไปที่ stdout ดังนั้นมันจึงถูกไพพ์ตามที่คาดไว้ใน command2 เมื่อ command1 ออกจากการทำงาน printf จะเริ่มต้นและพิมพ์สถานะการออกซึ่งถูกดักจับไว้ในตัวแปรโดยการทดแทนคำสั่ง รายละเอียดแยกย่อยมากที่นี่: stackoverflow.com/questions/985876/tee-and-exit-status/ ......นอกจากนี้ความคิดเห็นของคุณที่อ่านว่าราวกับว่ามันจะเป็นการดูถูกเหยียดหยาม?
mtraceur

ฉันจะเริ่มจากตรงไหนดี (1) ฉันขอโทษถ้าคุณรู้สึกดูถูก “ ดูน่าสนใจ” มีความหมายอย่างจริงจัง มันจะดีมากถ้ามีอะไรที่กะทัดรัดพอ ๆ กับที่ใช้งานได้ดีอย่างที่คุณคาดไว้ นอกเหนือจากนั้นฉันกำลังพูดเพียงว่าฉันไม่เข้าใจสิ่งที่ทางออกของคุณควรจะทำ ฉันทำงาน / เล่นกับ Unix มาเป็นเวลานาน (ตั้งแต่ก่อนที่ Linux มีอยู่) และถ้าฉันไม่เข้าใจบางสิ่งนั่นเป็นธงสีแดงที่บางทีคนอื่นอาจไม่เข้าใจและ ต้องการคำอธิบายเพิ่มเติม (IMNSHO) … (ต่อ)
G-Man

(ต่อ) เนื่องจากคุณ “ …ชอบคิด…ว่า [คุณ] เข้าใจทุกอย่างมากกว่าคนทั่วไป” บางทีคุณควรจำไว้ว่าวัตถุประสงค์ของการแลกเปลี่ยนแบบสแต็กนั้นไม่ใช่บริการเขียนคำสั่ง หาวิธีแก้ปัญหาแบบครั้งเดียวหลายพันคำถามที่แตกต่างเล็กน้อย แต่เป็นการสอนให้คนรู้จักวิธีแก้ปัญหาของตนเอง และท้ายที่สุดคุณอาจจำเป็นต้องอธิบายสิ่งต่าง ๆ ให้ดีพอที่“ คนทั่วไป” สามารถเข้าใจได้ ดูคำตอบที่เลสมานาเป็นตัวอย่างของคำอธิบายที่ดีเยี่ยม … (ต่อ)
G-Man


7

วิธีการแก้ปัญหาของ lesmana ด้านบนสามารถทำได้โดยไม่ต้องมีค่าใช้จ่ายในการเริ่มต้นกระบวนการย่อยแบบซ้อนโดยใช้{ .. }แทน (โปรดจำไว้ว่าคำสั่งที่จัดกลุ่มรูปแบบนี้จะต้องจบด้วยเครื่องหมายอัฒภาคเสมอ) บางสิ่งเช่นนี้

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

ฉันได้ตรวจสอบโครงสร้างนี้ด้วยขีดกลางเวอร์ชัน 0.5.5 และ bash เวอร์ชัน 3.2.25 และ 4.2.42 ดังนั้นแม้ว่าบางเชลล์ไม่รองรับการ{ .. }จัดกลุ่ม แต่ก็ยังเป็นไปตาม POSIX


ใช้งานได้ดีกับหอยส่วนใหญ่ที่ฉันได้ลองใช้รวมถึง NetBSD sh, pdksh, mksh, dash, bash อย่างไรก็ตามฉันไม่สามารถใช้งานกับ AT&T Ksh (93s +, 93u +) หรือ zsh (4.3.9, 5.2) ได้แม้จะเป็นset -o pipefailksh หรือwaitคำสั่งโรยจำนวนใด ๆ ก็ตาม ฉันคิดว่ามันอาจเป็นปัญหาในการแยกวิเคราะห์สำหรับ ksh อย่างน้อยที่สุดถ้าฉันใช้ subshells แล้วก็ใช้งานได้ดี แต่ถึงแม้จะมีการifเลือกตัวแปร subshell สำหรับ ksh แต่ปล่อยให้คำสั่งผสมสำหรับผู้อื่นมันล้มเหลว .
เกร็กเอ. วูดส์

4

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

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

แก้ไข: นี่เป็นรุ่นที่ดีกว่าตามความคิดเห็นของ Gilles:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: และนี่คือตัวแปรที่เบากว่าเล็กน้อยตามความคิดเห็น dubiousjim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))

3
สิ่งนี้ไม่ทำงานด้วยเหตุผลหลายประการ 1. ไฟล์ชั่วคราวอาจถูกอ่านก่อนที่จะเขียน 2. การสร้างไฟล์ชั่วคราวในไดเรกทอรีที่ใช้งานร่วมกันซึ่งมีชื่อที่คาดเดาได้นั้นไม่ปลอดภัย (DoS เล็กน้อย, symlink race) 3. หากสคริปต์เดียวกันใช้เคล็ดลับนี้หลายครั้งสคริปต์จะใช้ชื่อไฟล์เดิมเสมอ ในการแก้ปัญหา 1 ให้อ่านไฟล์หลังจากไปป์ไลน์เสร็จสมบูรณ์ เพื่อแก้ปัญหา 2 และ 3 ให้ใช้ไฟล์ชั่วคราวที่มีชื่อที่สร้างแบบสุ่มหรือในไดเรกทอรีส่วนตัว
Gilles

+1 ทีนี้ $ {PIPESTATUS [0]} นั้นง่ายกว่า แต่แนวคิดพื้นฐานที่นี่ใช้ได้ถ้าใครรู้เกี่ยวกับปัญหาที่ Gilles กล่าวถึง
Johan

คุณสามารถบันทึกไม่กี่ subshells (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))นี้: @Johan: ฉันเห็นด้วยกับ Bash ง่ายกว่า แต่ในบางบริบทการรู้วิธีหลีกเลี่ยง Bash นั้นคุ้มค่า
dubiousjim

4

การติดตามต่อไปนี้มีความหมายว่าเป็นส่วนเพิ่มเติมสำหรับคำตอบของ @Patrik ในกรณีที่คุณไม่สามารถใช้หนึ่งในวิธีแก้ปัญหาทั่วไป

คำตอบนี้ถือว่าต่อไปนี้:

  • คุณมีเปลือกหอยที่ไม่ทราบ$PIPESTATUSหรือset -o pipefail
  • คุณต้องการใช้ไพพ์สำหรับการประมวลผลแบบขนานดังนั้นจึงไม่มีไฟล์ชั่วคราว
  • คุณไม่ต้องการมีความยุ่งเหยิงเพิ่มเติมหากคุณขัดจังหวะสคริปต์อาจเกิดจากไฟฟ้าดับกะทันหัน
  • วิธีแก้ปัญหานี้ควรจะง่ายต่อการติดตามและทำความสะอาดให้อ่าน
  • คุณไม่ต้องการที่จะแนะนำ subshells เพิ่มเติม
  • คุณไม่สามารถเล่นซอกับไฟล์ descriptors ที่มีอยู่ได้ดังนั้นต้องไม่สัมผัส stdin / out / err (แต่คุณสามารถแนะนำใหม่ได้ชั่วคราว)

สมมติฐานเพิ่มเติม คุณสามารถกำจัดได้ทั้งหมด แต่สิ่งนี้ปิดบังสูตรมากเกินไปดังนั้นจึงไม่ครอบคลุมที่นี่:

  • สิ่งที่คุณต้องการรู้คือคำสั่งทั้งหมดใน PIPE มีรหัสทางออก 0
  • คุณไม่ต้องการข้อมูลแถบด้านข้างเพิ่มเติม
  • เชลล์ของคุณรอให้คำสั่งไพพ์คืนกลับมา

ก่อนหน้า: foo | bar | bazอย่างไรก็ตามสิ่งนี้จะส่งคืนรหัสออกของคำสั่งสุดท้ายเท่านั้น ( baz)

ต้องการ: $?ต้องไม่เป็น0(จริง) หากคำสั่งใด ๆ ในไพพ์ล้มเหลว

หลังจาก:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

อธิบาย:

  • tempfile mktempถูกสร้างขึ้นด้วย โดยปกติจะสร้างไฟล์ในทันที/tmp
  • tempfile นี้จะถูกเปลี่ยนเส้นทางไปยัง FD 9 สำหรับการเขียนและ FD 8 สำหรับการอ่าน
  • จากนั้น tempfile จะถูกลบทันที มันยังคงเปิดอยู่จนกระทั่งจนกระทั่ง FDs ทั้งสองหมดไป
  • ตอนนี้เริ่มต้นท่อแล้ว แต่ละขั้นตอนจะเพิ่มใน FD 9 เท่านั้นหากมีข้อผิดพลาด
  • waitเป็นสิ่งจำเป็นสำหรับkshเพราะkshอื่นไม่รอให้ท่อทุกคำสั่งที่จะเสร็จสิ้น อย่างไรก็ตามโปรดทราบว่ามีผลข้างเคียงที่ไม่พึงประสงค์หากมีงานเบื้องหลังบางอย่างดังนั้นฉันจึงแสดงความคิดเห็นโดยค่าเริ่มต้น หากการรอไม่เจ็บคุณสามารถแสดงความคิดเห็นได้
  • หลังจากนั้นเนื้อหาของไฟล์จะถูกอ่าน ถ้ามันว่างเปล่า (เพราะผลงานทั้งหมด) readกลับfalseมาtrueแสดงว่ามีข้อผิดพลาด

สิ่งนี้สามารถใช้เป็นการแทนที่ปลั๊กอินสำหรับคำสั่งเดียวและต้องการเพียงต่อไปนี้:

  • FDs ที่ไม่ได้ใช้ 9 และ 8
  • ตัวแปรสภาพแวดล้อมเดียวเพื่อเก็บชื่อของ tempfile
  • และสูตรนี้สามารถปรับให้เข้ากับเปลือกใด ๆ ที่เป็นธรรมซึ่งอนุญาตการเปลี่ยนเส้นทาง IO
  • นอกจากนี้ยังเป็นแพลตฟอร์มที่ไม่เชื่อเรื่องพระเจ้าและไม่ต้องการสิ่งที่ต้องการ /proc/fd/N

ข้อบกพร่อง:

สคริปต์นี้มีข้อบกพร่องในกรณีที่/tmpพื้นที่ไม่เพียงพอ หากคุณต้องการป้องกันกรณีเทียมนี้ด้วยคุณสามารถทำได้ดังนี้อย่างไรก็ตามนี่มีข้อเสียที่จำนวน0ในนั้น000ขึ้นอยู่กับจำนวนคำสั่งในไพพ์ดังนั้นมันจึงซับซ้อนกว่าเล็กน้อย:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

หมายเหตุพกพา:

  • kshและเชลล์ที่คล้ายกันซึ่งรอคำสั่งไปป์สุดท้ายเท่านั้นที่ต้องการการไม่แสดงwaitข้อคิดเห็น

  • ตัวอย่างสุดท้ายใช้printf "%1s" "$?"แทนecho -n "$?"เพราะเป็นอุปกรณ์พกพามากกว่า ไม่ใช่ทุกแพลตฟอร์มตีความ-nอย่างถูกต้อง

  • printf "$?"จะทำเช่นกัน แต่printf "%1s"จับกรณีมุมบางส่วนในกรณีที่คุณเรียกใช้สคริปต์บนแพลตฟอร์มที่แตกหักบางอย่าง (อ่าน: ถ้าคุณเกิดขึ้นกับโปรแกรมparanoia_mode=extremeมา)

  • FD 8 และ FD 9 สามารถสูงขึ้นได้บนแพลตฟอร์มที่รองรับหลายหลัก AFAIR เชลล์ที่สอดคล้องกับ POSIX จะต้องรองรับตัวเลขหลักเดียวเท่านั้น

  • ได้รับการทดสอบกับ Debian 8.2 sh, bash, ksh, ash, sashและแม้กระทั่งcsh


3

ด้วยความระมัดระวังเล็กน้อยสิ่งนี้ควรใช้งานได้:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

วิธีการล้างเช่น jlliagre คุณไม่ทิ้งไฟล์ไว้เบื้องหลังที่เรียกว่าสถานะ foo?
Johan

@Johan: หากคุณต้องการคำแนะนำของฉันอย่าลังเลที่จะโหวตมัน); นอกจากจะไม่ปล่อยไฟล์ไว้มันมีข้อดีของการอนุญาตให้กระบวนการหลาย ๆ กระบวนการเรียกใช้พร้อมกันและไดเรกทอรีปัจจุบันไม่จำเป็นต้องเขียนทับได้
jlliagre

2

บล็อก 'if' ต่อไปนี้จะทำงานเฉพาะในกรณีที่คำสั่ง 'สำเร็จ'

if command; then
   # ...
fi

คุณสามารถเรียกใช้สิ่งนี้:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

ซึ่งจะเรียกใช้haconf -makerwและเก็บ stdout และ stderr ไว้ที่ "$ haconf_out" หากค่าที่ส่งคืนจากhaconfนั้นเป็นจริงบล็อก 'if' จะถูกเรียกใช้งานและgrepจะอ่าน "$ haconf_out" พยายามจับคู่กับ "Cluster ที่สามารถเขียนได้"

สังเกตว่าท่อทำความสะอาดตัวเองโดยอัตโนมัติ ด้วยการเปลี่ยนเส้นทางคุณจะต้องระมัดระวังในการลบ "$ haconf_out" เมื่อเสร็จแล้ว

ไม่หรูหราเท่าpipefailแต่เป็นทางเลือกที่ถูกต้องหากฟังก์ชันนี้ไม่สามารถเข้าถึงได้


1
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$

0

(ด้วยการทุบตีอย่างน้อย) รวมกับset -eหนึ่งสามารถใช้ subshell เพื่อเลียนแบบ pipefail และออกจากข้อผิดพลาดท่ออย่างชัดเจน

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

ดังนั้นหากfooล้มเหลวด้วยเหตุผลบางประการ - ส่วนที่เหลือของโปรแกรมจะไม่ถูกเรียกใช้และออกจากสคริปต์พร้อมรหัสข้อผิดพลาดที่เกี่ยวข้อง (สิ่งนี้อนุมานว่าfooพิมพ์ข้อผิดพลาดของตัวเองซึ่งเพียงพอที่จะเข้าใจสาเหตุของความล้มเหลว)


-1

แก้ไข : คำตอบนี้ผิด แต่น่าสนใจดังนั้นฉันจะปล่อยไว้เพื่ออ้างอิงในอนาคต


การเพิ่ม!คำสั่งลงในกลับรหัสย้อนกลับ

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #

1
ฉันคิดว่านี่ไม่เกี่ยวข้อง ในตัวอย่างของคุณฉันต้องการทราบรหัสทางออกของlsไม่ใช่กลับรหัสทางออกของbogus_command
Michael Mrozek

2
ฉันแนะนำให้ดึงคำตอบนั้นกลับมา
maxschlepzig

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