เอาท์พุทการทดแทนกระบวนการออกคำสั่ง


16

echo one; echo two > >(cat); echo three; 

คำสั่งให้ผลลัพธ์ที่ไม่คาดคิด

ฉันอ่านสิ่งนี้: วิธีการใช้กระบวนการทดแทนในทุบตีอย่างไร และบทความอื่น ๆ อีกมากมายเกี่ยวกับการทดแทนกระบวนการบนอินเทอร์เน็ต แต่ไม่เข้าใจว่าทำไมจึงมีพฤติกรรมเช่นนี้

ผลลัพธ์ที่คาดหวัง:

one
two
three

ผลผลิตจริง:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

ทั้งสองคำสั่งนี้ควรจะเทียบเท่ากับมุมมองของฉัน แต่พวกเขาไม่ได้:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

ทำไมฉันคิดว่าพวกเขาควรจะเหมือนกัน? เพราะทั้งสองเชื่อมต่อseqผลลัพธ์กับcatอินพุตผ่านไพพ์นิรนาม - Wikipedia, การแทนที่โปรเซส

คำถาม:ทำไมมันมีพฤติกรรมเช่นนี้? ข้อผิดพลาดของฉันอยู่ที่ไหน ต้องการคำตอบที่ครอบคลุม (พร้อมคำอธิบายว่าเป็นอย่างไรbashภายใต้ประทุน)


2
แม้ว่ามันจะไม่ชัดเจนตั้งแต่แรกเห็น แต่จริงๆแล้วมันเป็นของbash ที่รอกระบวนการในการทดแทนกระบวนการแม้ว่าคำสั่งจะไม่ถูกต้อง
Stéphane Chazelas

2
จริงๆแล้วมันจะดีกว่าถ้าคำถามอื่นถูกทำเครื่องหมายว่าซ้ำกับคำถามนี้เพราะคำถามนี้มีมากขึ้น นี่คือเหตุผลที่ฉันคัดลอกคำตอบของฉันที่นั่น
Stéphane Chazelas

คำตอบ:


21

ใช่bashเช่นเดียวกับในksh(ซึ่งเป็นคุณลักษณะที่มาจาก) กระบวนการภายในการทดแทนกระบวนการจะไม่รอ (ก่อนเรียกใช้คำสั่งถัดไปในสคริปต์)

สำหรับ<(...)อันที่ปกติแล้วก็ดีเหมือนใน:

cmd1 <(cmd2)

เชลล์จะรอcmd1และcmd1โดยทั่วไปจะรอcmd2โดยอาศัยการอ่านจนกว่าจะสิ้นสุดของไฟล์บนไพพ์ที่ถูกทดแทนและโดยทั่วไปจุดสิ้นสุดของไฟล์จะเกิดขึ้นเมื่อcmd2ตาย นั่นเป็นเหตุผลเดียวกันกับเปลือกหอยหลายคน (ไม่bash) ไม่รำคาญรอให้ในcmd2cmd2 | cmd1

สำหรับcmd1 >(cmd2)อย่างไรก็ตามนั่นไม่ใช่กรณีทั่วไปเนื่องจากเป็นเรื่องcmd2ปกติที่รออยู่ที่cmd1นั่นดังนั้นโดยทั่วไปจะออกหลังจาก

ที่ได้รับการแก้ไขในzshที่รออยู่ที่cmd2นั่น (แต่ไม่ใช่ถ้าคุณเขียนเป็นcmd1 > >(cmd2)และcmd1ไม่ได้สร้างขึ้นให้ใช้{cmd1} > >(cmd2)แทนเอกสาร )

kshไม่ต้องรอเป็นค่าเริ่มต้น แต่ให้คุณรอด้วยwaitbuiltin (มันทำให้ pid ใช้งานได้$!แม้ว่ามันจะไม่ช่วยถ้าคุณทำcmd1 >(cmd2) >(cmd3))

rc(ที่มีcmd1 >{cmd2}ไวยากรณ์) เช่นเดียวกับการkshยกเว้นคุณจะได้รับ PIDs $apidsของทุกกระบวนการพื้นหลังด้วย

es(พร้อมกับcmd1 >{cmd2}) รอcmd2เหมือนในzshและรอcmd2ใน<{cmd2}การเปลี่ยนเส้นทางกระบวนการ

bashจะทำให้ pid ของcmd2(หรือมากกว่าของ subshell ตามที่มันทำงานcmd2ในกระบวนการลูกของ subshell นั้นแม้ว่ามันจะเป็นคำสั่งสุดท้ายที่นั่น) $!แต่ก็ไม่ยอมให้คุณรอ

หากคุณจำเป็นต้องใช้bashคุณสามารถแก้ไขปัญหาได้โดยใช้คำสั่งที่จะรอคำสั่งทั้งสองด้วย:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

นั่นทำให้ทั้งคู่cmd1และให้cmd2fd 3 ของพวกเขาเปิดสู่ท่อ catจะรอให้ไฟล์สิ้นสุดที่ปลายอีกด้านหนึ่งดังนั้นโดยทั่วไปจะออกเฉพาะเมื่อทั้งคู่cmd1และcmd2ตาย และเชลล์จะรอcatคำสั่งนั้น คุณสามารถเห็นได้ว่าในฐานะที่เป็นเครือข่ายในการยุติกระบวนการพื้นหลังทั้งหมด (คุณสามารถใช้มันเพื่อสิ่งอื่น ๆ ที่เริ่มต้นในพื้นหลังเช่นเดียวกับ&coprocs หรือแม้แต่คำสั่งที่พื้นหลังของตัวเองหากพวกเขาไม่ปิดไฟล์ )

โปรดทราบว่าด้วยกระบวนการ subshell ที่สูญเปล่าดังกล่าวข้างต้นมันทำงานได้แม้ว่าcmd2จะปิด fd 3 แล้ว (โดยปกติคำสั่งจะไม่ทำเช่นนั้น แต่มีบางอย่างที่ชอบsudoหรือsshทำ) ในที่สุดเวอร์ชันในอนาคตbashอาจทำการปรับให้เหมาะสมเช่นเดียวกับในเชลล์อื่น ๆ จากนั้นคุณต้องมีสิ่งต่อไปนี้:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

เพื่อให้แน่ใจว่ายังคงมีกระบวนการเชลล์พิเศษที่เปิด fd 3 นั้นรอsudoคำสั่งนั้น

โปรดทราบว่าcatจะไม่อ่านอะไรเลย (เนื่องจากกระบวนการไม่ได้เขียนลงใน fd 3) มันมีไว้สำหรับการซิงโครไนซ์เท่านั้น มันจะทำการread()เรียกระบบเพียงครั้งเดียวที่จะกลับมาโดยไม่มีอะไรสิ้นสุด

คุณสามารถหลีกเลี่ยงการทำงานcatโดยใช้การทดแทนคำสั่งเพื่อทำการซิงก์ไปป์:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

เวลานี้มันเป็นเปลือกแทนการcatที่อ่านจากท่อที่มีส่วนอื่น ๆ เปิดให้บริการใน FD 3 และcmd1 cmd2เรากำลังใช้การกำหนดตัวแปรเพื่อออกจากสถานะของการอยู่ในที่มีอยู่cmd1$?

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

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

แม้ว่าบันทึกย่อตามที่ระบุไว้ก่อนหน้านี้ว่าshการใช้งานบางอย่างจะไม่รอcmd1หลังจากcmd2เสร็จสิ้น (แม้ว่าจะดีกว่าวิธีอื่น ๆ ) เวลานั้น$?มีสถานะออกจากcmd2; แม้ว่าbashและzshทำให้cmd1สถานะการออกของพร้อมใช้งานใน${PIPESTATUS[0]}และ$pipestatus[1]ตามลำดับ (ดูpipefailตัวเลือกในเปลือกบางอย่างเพื่อให้$?สามารถรายงานความล้มเหลวของคอมโพเนนต์ท่ออื่นที่ไม่ใช่ครั้งสุดท้าย)

โปรดทราบว่าyashมีปัญหาคล้ายกันกับคุณสมบัติการเปลี่ยนเส้นทางของกระบวนการ cmd1 >(cmd2)จะเขียนที่cmd1 /dev/fd/3 3>(cmd2)นั่น แต่cmd2ไม่ได้รอและคุณไม่สามารถใช้waitเพื่อรอได้และ pid ของมันไม่ได้ทำให้พร้อมใช้งานใน$!ตัวแปรเช่นกัน คุณต้องการใช้ arounds bashการทำงานเช่นเดียวกับ


ประการแรกฉันลองecho one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;แล้วทำให้มันง่ายขึ้นecho one; echo two > >(cat) | cat; echo three;และมันส่งออกค่าในลำดับที่ถูกต้องเช่นกัน การปรับเปลี่ยนคำอธิบายทั้งหมดนี้3>&1 >&4 4>&-จำเป็นหรือไม่ นอกจากนี้ผมไม่ได้รับนี้>&4 4>&- เราเปลี่ยนเส้นทางstdoutไปยัง FD ที่สี่แล้วปิด FD สี่อีกครั้งแล้วใช้4>&1มัน ทำไมมันจึงจำเป็นและมันทำงานอย่างไร? อาจจะเป็นฉันควรสร้างคำถามใหม่ในหัวข้อนี้?
MiniMax

1
@MiniMax แต่ที่นั่นคุณส่งผลกระทบต่อ stdout cmd1และcmd2จุดที่มีการเต้นรำเล็ก ๆ น้อย ๆ กับ file descriptor คือการคืนค่าดั้งเดิมและใช้เฉพาะไพพ์พิเศษสำหรับการรอแทนการแชนเนลเอาท์พุทของคำสั่ง
Stéphane Chazelas

@MiniMax มันใช้เวลาพอสมควรที่จะเข้าใจว่าฉันไม่ได้ไปป์ที่ระดับต่ำเช่นนี้มาก่อน ด้านขวาสุด4>&1สร้างไฟล์ descriptor (fd) 4 สำหรับรายการคำสั่ง braces ด้านนอกและทำให้เท่ากับ stdout ของ braces ภายนอก เครื่องมือจัดฟันด้านในมี stdin / stdout / stderr ตั้งค่าโดยอัตโนมัติเพื่อเชื่อมต่อกับเครื่องมือจัดฟันด้านนอก อย่างไรก็ตาม3>&1ทำให้ fd 3 เชื่อมต่อกับ stdin ของเครื่องมือจัดฟันด้านนอก >&4ทำให้ stdout ของเครื่องมือจัดฟันด้านในเชื่อมต่อกับเครื่องมือจัดฟันด้านนอก fd 4 (เครื่องมือที่เราสร้างไว้ก่อนหน้านี้) 4>&-ปิด fd 4 จากวงเล็บปีกกาด้านใน (เนื่องจาก stdout วงเล็บปีกกาภายในเชื่อมต่อกับวงเล็บปีกกา fd 4) แล้ว
Nicholas Pipitone

@MiniMax ส่วนที่สับสนคือส่วนจากขวาไปซ้าย4>&1ถูกเรียกใช้ก่อนที่จะมีการเปลี่ยนเส้นทางอื่นดังนั้นคุณจะไม่ "ใช้อีกครั้ง4>&1" โดยรวมแล้วเครื่องมือจัดฟันด้านในกำลังส่งข้อมูลไปยัง stdout ซึ่งถูกเขียนทับด้วย fd 4 อะไรก็ตามที่ได้รับ fd 4 ที่ได้รับการจัดฟันด้านในคือการจัดฟันด้านนอก 'fd 4 ซึ่งเท่ากับ stdout ดั้งเดิมของการจัดฟันด้านนอก'
Nicholas Pipitone

Bash ทำให้รู้สึกเหมือน4>5หมายถึง "4 ไปถึง 5" แต่จริงๆแล้ว "fd 4 ถูกเขียนทับด้วย fd 5" และก่อนการประมวลผล fd 0/1/2 จะเชื่อมต่ออัตโนมัติ (พร้อมกับ fd ของเปลือกนอก) และคุณสามารถเขียนทับมันได้ตามที่คุณต้องการ อย่างน้อยฉันก็ตีความเอกสารทุบตี หากคุณเข้าใจเรื่องอื่นอย่างนี้ lmk
Nicholas Pipitone

4

คุณสามารถไพพ์คำสั่งที่สองเป็นอีกคำสั่งหนึ่งได้ catซึ่งจะรอจนกระทั่งอินพุตไพพ์ปิดลง Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

สั้นและง่าย

==========

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

เมื่อคุณมีecho two > >(cat); echo three, คดเคี้ยวออกจากเปลือกโต้ตอบและทำงานเป็นอิสระจาก>(cat) echo twoดังนั้นecho twoเสร็จสิ้นแล้วecho threeได้รับการดำเนินการ แต่ก่อนที่จะ>(cat)เสร็จสิ้น เมื่อbashได้รับข้อมูลจาก>(cat)เมื่อไม่ได้คาดหวังไว้ (สองสามมิลลิวินาทีในภายหลัง) จะช่วยให้คุณได้รับสถานการณ์เหมือนพรอมต์ที่คุณต้องกดขึ้นบรรทัดใหม่เพื่อกลับไปที่เทอร์มินัล (เหมือนกับผู้ใช้รายอื่นmesgคุณ)

อย่างไรก็ตามให้echo two > >(cat) | cat; echo threeสอง subshells จะเกิด (ตามเอกสารของ|สัญลักษณ์)

หนึ่ง subshell ชื่อเป็นสำหรับecho two > >(cat)และหนึ่ง subshell ชื่อ B catหมายถึง A เชื่อมต่อกับ B โดยอัตโนมัติ (stdout ของ A คือ stdin ของ B) จากนั้นecho twoและ>(cat)เริ่มดำเนินการ >(cat)stdout ของถูกตั้งค่าเป็น stdout ของ A ซึ่งเท่ากับ stdin ของ B หลังจากecho twoเสร็จสิ้น A จะออกแล้วปิด stdout อย่างไรก็ตาม>(cat)ยังคงมีการอ้างอิงถึง stdin ของ B ที่สองcatstdin 's ถือ stdin B และที่catจะไม่ออกจนกว่าจะเห็น EOF EOF จะได้รับก็ต่อเมื่อไม่มีใครมีเปิดไฟล์ในโหมดเขียนอีกต่อไปดังนั้น>(cat)'s stdout catคือการปิดกั้นที่สอง B ยังคงรออยู่ในวินาทีcatนั้น เนื่องจากecho twoออกมา>(cat)ในที่สุดก็ได้รับ EOF ดังนั้น>(cat)ล้างบัฟเฟอร์และออก ไม่มีใครถือcatstdin ของ B / วินาทีอีกต่อไปดังนั้นวินาทีที่ออกและ B ได้รับการรอcatอ่าน EOF (B ไม่ได้อ่าน stdin เลยไม่สนใจ) EOF นี้เป็นสาเหตุที่สองcatเพื่อล้างบัฟเฟอร์ปิด stdout และออกจากนั้น B ออกเพราะcatcat

ข้อแม้ของเรื่องนี้ก็คือทุบตีที่วางไข่ subshell สำหรับ>(cat)! ด้วยเหตุนี้คุณจะเห็นว่า

echo two > >(sleep 5) | cat; echo three

จะยังคงรอ 5 วินาทีก่อนที่จะดำเนินการecho threeแม้ว่าsleep 5จะไม่ได้ถือ stdin ของ B เพราะนี่คือ subshell C ที่ซ่อนอยู่ที่เกิดขึ้น>(sleep 5)กำลังรออยู่sleepและ C กำลังถือ stdin ของ B อยู่ คุณสามารถดูว่า

echo two > >(exec sleep 5) | cat; echo three

จะไม่รออย่างไรก็ตามเนื่องจากsleepไม่ได้ถือ stdin ของ B และไม่มี Ghost subshell C ที่ถือ stdin ของ B (exec จะบังคับให้ sleep เพื่อแทนที่ C แทนการฟอร์กและทำให้ C รออยู่sleep) โดยไม่คำนึงถึงข้อแม้นี้

echo two > >(exec cat) | cat; echo three

จะยังคงใช้งานฟังก์ชั่นตามลำดับอย่างถูกต้องตามที่อธิบายไว้ก่อนหน้านี้


ดังที่ระบุไว้ในการแปลงด้วย @MiniMax ในความคิดเห็นต่อคำตอบของฉันซึ่งมีข้อเสียของ stdout ของคำสั่งและหมายความว่าเอาต์พุตจะต้องอ่านและเขียนในช่วงต่อเวลาพิเศษ
Stéphane Chazelas

คำอธิบายไม่ถูกต้อง Aไม่รอให้catเกิดมา>(cat)กลับกลายในดังที่ฉันพูดถึงคำตอบของฉันเหตุผลที่echo two > >(sleep 5 &>/dev/null) | cat; echo threeผลลัพธ์threeหลังจาก 5 วินาทีเป็นเพราะรุ่นปัจจุบันของbashขยะเปลือกกระบวนการพิเศษ>(sleep 5)ที่รอsleepและกระบวนการนั้นยังคงมี stdout ไปที่pipeซึ่งป้องกันไม่ให้ที่สองcatจากการยกเลิก หากคุณแทนที่ด้วยecho two > >(exec sleep 5 &>/dev/null) | cat; echo threeเพื่อกำจัดกระบวนการพิเศษนั้นคุณจะพบว่ามันส่งคืนทันที
Stéphane Chazelas

มันทำให้ subshell ซ้อนกันหรือไม่ ฉันพยายามตรวจสอบการนำ bash มาใช้เพื่อทำความเข้าใจฉันมั่นใจว่าecho two > >(sleep 5 &>/dev/null)อย่างน้อยที่สุดจะได้รับ subshell ของตัวเอง เป็นรายละเอียดการใช้งานที่ไม่มีเอกสารซึ่งทำให้เกิดsleep 5subshell ของตัวเองหรือไม่ หากมีการบันทึกไว้แล้วมันจะเป็นวิธีที่ถูกต้องในการทำให้ตัวละครมีน้อยลง (เว้นแต่จะมีการวนรอบที่แน่นฉันไม่คิดว่าจะมีใครสังเกตเห็นปัญหาด้านประสิทธิภาพด้วย subshell หรือ cat) ' หากยังไม่มีการจัดทำเป็นเอกสารให้ทำการแฮ็กข้อมูลที่ดีแฮ็คจะไม่สามารถใช้งานได้ในเวอร์ชันต่อไป
Nicholas Pipitone

$(...), <(...)ทำแน่นอนเกี่ยวข้องกับ subshell แต่ ksh93 หรือ zsh จะเรียกใช้คำสั่งสุดท้ายใน subshell ว่าในกระบวนการเดียวกันไม่ได้bashซึ่งเป็นเหตุผลที่ยังคงมีการดำเนินการอื่นที่ถือท่อเปิดในขณะที่sleepกำลังเรียกใช้ไม่ได้ถือเปิดท่อ เวอร์ชันในอนาคตของbashอาจใช้การเพิ่มประสิทธิภาพที่คล้ายกัน
Stéphane Chazelas

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