จะทำไพพ์สองทิศทางระหว่างสองโปรแกรมได้อย่างไร


63

ทุกคนรู้วิธีที่จะทำให้ท่อทิศทางเดียวระหว่างสองโปรแกรม (ผูกstdoutหนึ่งครั้งแรกและครั้งที่สองอย่างใดอย่างหนึ่ง):stdinfirst | second

แต่จะทำไพพ์สองทิศทางคือ cross-bind stdinและstdoutสองโปรแกรมได้อย่างไร มีวิธีง่าย ๆ ในการทำเชลล์หรือไม่?

shell  pipe 

คำตอบ:


30

หากไพพ์ในระบบของคุณเป็นแบบสองทิศทาง (เช่นที่อยู่บน Solaris 11 และ BSD บางตัวเป็นอย่างน้อย แต่ไม่ใช่ Linux):

cmd1 <&1 | cmd2 >&0

ระวังการหยุดชะงักแม้ว่า

นอกจากนี้ทราบว่าบางรุ่น ksh93 ในบางระบบใช้ท่อ ( |) โดยใช้คู่ซ็อกเก็ต ซ็อกเก็ตคู่เป็นแบบสองทิศทาง แต่ ksh93 ปิดทิศทางกลับอย่างชัดเจนดังนั้นคำสั่งข้างต้นจะไม่ทำงานกับ ksh93s เหล่านั้นแม้ในระบบที่ไพพ์ (ตามที่สร้างโดยการpipe(2)เรียกของระบบ) เป็นแบบสองทิศทาง


1
ไม่มีใครรู้ว่านี้ทำงานบน linux ด้วยหรือไม่ (ใช้ archlinux ที่นี่)
heinrich5991


41

ดีมันค่อนข้าง "ง่าย" ด้วยชื่อ pipes ( mkfifo) ฉันใส่คำพูดง่าย ๆ เพราะถ้าโปรแกรมไม่ได้รับการออกแบบสำหรับสิ่งนี้

mkfifo fifo0 fifo1
( prog1 > fifo0 < fifo1 ) &
( prog2 > fifo1 < fifo0 ) &
( exec 30<fifo0 31<fifo1 )      # write can't open until there is a reader
                                # and vice versa if we did it the other way

ตอนนี้มีบัฟเฟอร์ปกติที่เกี่ยวข้องในการเขียน stdout ตัวอย่างเช่นหากทั้งสองโปรแกรมเป็น:

#!/usr/bin/perl
use 5.010;
say 1;
print while (<>);

คุณคาดหวังวงวนไม่สิ้นสุด แต่แทนที่จะทั้งสองจะหยุดชะงัก; คุณจะต้องเพิ่ม$| = 1(หรือเทียบเท่า) เพื่อปิดบัฟเฟอร์การส่งออก การหยุดชะงักเกิดขึ้นเนื่องจากทั้งสองโปรแกรมกำลังรออะไรบางอย่างใน stdin แต่พวกเขาไม่เห็นมันเพราะมันนั่งอยู่ใน stdout buffer ของโปรแกรมอื่นและยังไม่ได้เขียนลงในไพพ์

อัพเดท : การรวมคำแนะนำจากStéphane Charzelas และ Joost:

mkfifo fifo0 fifo1
prog1 > fifo0 < fifo1 &
prog2 < fifo0 > fifo1

ทำเช่นเดียวกันสั้นกว่าและพกพาได้มากกว่า


23
หนึ่งชื่อไปป์นั้นเพียงพอแล้ว: prog1 < fifo | prog2 > fifo.
Andrey Vihrov

2
@AndreyVihrov เป็นเรื่องจริงคุณสามารถแทนที่ไปป์ที่ไม่ระบุชื่อสำหรับหนึ่งในรายการที่มีชื่อ แต่ฉันชอบความสมมาตร :-P
derobert

3
@ user14284: บน Linux prog1 < fifo | tee /dev/stderr | prog2 | tee /dev/stderr > fifoคุณอาจจะสามารถทำมันได้กับสิ่งที่ต้องการ
Andrey Vihrov

3
หากคุณทำมันprog2 < fifo0 > fifo1คุณสามารถหลีกเลี่ยงการเต้นรำเล็ก ๆ น้อย ๆ ของคุณด้วยexec 30< ...(ซึ่งโดยวิธีการทำงานร่วมกับbashหรือyashสำหรับ fds มากกว่า 10 เช่นนั้น)
Stéphane Chazelas

1
@Joost Hmmm ปรากฏว่าคุณถูกต้องว่าไม่จำเป็นต้องมีอย่างน้อยในการทุบตี ฉันอาจกังวลว่าเนื่องจากเชลล์ทำการเปลี่ยนเส้นทาง (รวมถึงการเปิดท่อ) มันอาจบล็อก - แต่อย่างน้อยทุบตีส้อมก่อนที่จะเปิด Fifos dashดูเหมือนว่าตกลงเกินไป ( แต่มีพฤติกรรมแตกต่างกันเล็กน้อย)
derobert

13

ฉันไม่แน่ใจว่านี่คือสิ่งที่คุณพยายามทำ:

nc -l -p 8096 -c second &
nc -c first 127.0.0.1 8096 &

สิ่งนี้เริ่มต้นด้วยการเปิดซ็อกเก็ตการฟังบนพอร์ต 8096 และเมื่อมีการสร้างการเชื่อมต่อแล้วโปรแกรมจะวางไข่secondโดยstdinใช้เอาต์พุตสตรีมและstdoutเป็นสตรีมอินพุต

จากนั้นวินาทีncจะถูกเรียกใช้ซึ่งเชื่อมต่อกับพอร์ตฟังและวางไข่โปรแกรมfirstด้วยstdoutอินพุตสตรีมและstdinเอาต์พุตสตรีม

สิ่งนี้ไม่ได้ใช้งานไปป์อย่างแน่นอน แต่ดูเหมือนจะทำสิ่งที่คุณต้องการ

เนื่องจากเป็นการใช้เครือข่ายจึงสามารถทำได้บนคอมพิวเตอร์ระยะไกล 2 เครื่อง นี่เกือบจะเป็นวิธีที่เว็บเซิร์ฟเวอร์ ( second) และเว็บเบราว์เซอร์ ( first) ทำงาน


1
และnc -Uสำหรับซ็อกเก็ตโดเมน UNIX ที่ใช้พื้นที่ที่อยู่ของระบบไฟล์เท่านั้น
Ciro Santilli 新疆改造中心法轮功六四事件

-c ตัวเลือกไม่พร้อมใช้งานใน Linux ความสุขของฉันคืออายุสั้นหนึ่ง :-(
pablochacin


6

bashเวอร์ชัน 4 มีcoprocคำสั่งที่อนุญาตให้ทำสิ่งนี้bashโดยบริสุทธิ์โดยไม่มีไพพ์

coproc cmd1
eval "exec cmd2 <&${COPROC[0]} >&${COPROC[1]}"

กระสุนอื่น ๆ ก็สามารถทำได้coprocเช่นกัน

ด้านล่างนี้เป็นคำตอบที่ละเอียดยิ่งขึ้น แต่เชื่อมโยงสามคำสั่งแทนที่จะเป็นสองคำสั่งซึ่งทำให้น่าสนใจยิ่งขึ้นเพียงเล็กน้อยเท่านั้น

หากคุณมีความสุขที่จะใช้catและstdbufจากนั้นสร้างสามารถทำให้เข้าใจง่ายขึ้น

เวอร์ชันที่ใช้bashกับcatและstdbufเข้าใจง่าย:

# start pipeline
coproc {
    cmd1 | cmd2 | cmd3
}
# create command to reconnect STDOUT `cmd3` to STDIN of `cmd1`
endcmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"
# eval the command.
eval "${endcmd}"

หมายเหตุต้องใช้ eval เนื่องจากการขยายตัวแปรใน <& $ var นั้นผิดกฎหมายในเวอร์ชัน bash 4.2.25 ของฉัน

เวอร์ชันที่ใช้ pure bash: แบ่งออกเป็นสองส่วนเปิดตัวไปป์ไลน์แรกภายใต้ coproc จากนั้นรับประทานอาหารกลางวันส่วนที่สอง (ทั้งคำสั่งเดียวหรือไปป์ไลน์) เชื่อมต่อไปยังส่วนแรก:

coproc {
    cmd 1 | cmd2
}
endcmd="exec cmd3 <&${COPROC[0]} >&${COPROC[1]}"
eval "${endcmd}"

พิสูจน์แนวคิด:

ไฟล์./progเพียงหุ่นจำลองที่จะใช้ติดแท็กและพิมพ์ซ้ำบรรทัด การใช้ subshells เพื่อหลีกเลี่ยงปัญหาการบัฟเฟอร์อาจทำให้เกิด overkill ไม่ใช่จุดที่นี่

#!/bin/bash
let c=0
sleep 2

[ "$1" == "1" ] && ( echo start )

while : ; do
  line=$( head -1 )
  echo "$1:${c} ${line}" 1>&2
  sleep 2
  ( echo "$1:${c} ${line}" )
  let c++
  [ $c -eq 3 ] && exit
done

ไฟล์./start_cat นี้เป็นรุ่นที่ใช้bash, catและstdbuf

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2 \
    | stdbuf -i0 -o0 ./prog 3
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 /bin/cat <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

./start_partหรือไฟล์ นี่เป็นรุ่นที่ใช้บริสุทธิ์bashเท่านั้น สำหรับวัตถุประสงค์ในการสาธิตฉันยังใช้งานอยู่stdbufเนื่องจาก prog ที่แท้จริงของคุณจะต้องจัดการกับการบัฟเฟอร์ภายในเพื่อหลีกเลี่ยงการบล็อกเนื่องจากการบัฟเฟอร์

#!/bin/bash

echo starting first cmd>&2

coproc {
  stdbuf -i0 -o0 ./prog 1 \
    | stdbuf -i0 -o0 ./prog 2
}

echo "Delaying remainer" 1>&2
sleep 5
cmd="exec stdbuf -i0 -o0 ./prog 3 <&${COPROC[0]} >&${COPROC[1]}"

echo "Running: ${cmd}" >&2
eval "${cmd}"

เอาท์พุท:

> ~/iolooptest$ ./start_part
starting first cmd
Delaying remainer
2:0 start
Running: exec stdbuf -i0 -o0 ./prog 3 <&63 >&60
3:0 2:0 start
1:0 3:0 2:0 start
2:1 1:0 3:0 2:0 start
3:1 2:1 1:0 3:0 2:0 start
1:1 3:1 2:1 1:0 3:0 2:0 start
2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start
1:2 3:2 2:2 1:1 3:1 2:1 1:0 3:0 2:0 start

นั่นมัน


5

Building Block ที่สะดวกสำหรับการเขียนท่อสองทิศทางนั้นเป็นสิ่งที่เชื่อมต่อ stdout และ stdin ของกระบวนการปัจจุบันเข้าด้วยกัน เราเรียกมันว่า ioloop หลังจากเรียกใช้ฟังก์ชันนี้คุณจะต้องเริ่มต้นไปป์ปกติเท่านั้น:

ioloop &&     # stdout -> stdin 
cmd1 | cmd2   # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)

หากคุณไม่ต้องการแก้ไข descriptors ของเชลล์ระดับบนให้รันในเชลล์ย่อย:

( ioloop && cmd1 | cmd2 )

นี่คือการใช้งานแบบพกพาของ ioloop โดยใช้ไพพ์ที่มีชื่อ:

ioloop() {
    FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
    trap "rm -f $FIFO" EXIT &&
    mkfifo $FIFO &&
    ( : <$FIFO & ) &&    # avoid deadlock on opening pipe
    exec >$FIFO <$FIFO
}

ไพพ์ที่มีชื่อนั้นมีอยู่ในระบบไฟล์สั้น ๆ เท่านั้นระหว่างการติดตั้ง ioloop ฟังก์ชั่นนี้ค่อนข้างไม่ POSIX เพราะ mktemp เลิกใช้แล้ว (และอาจเสี่ยงต่อการถูกโจมตีจากการแข่งขัน)

การใช้งานเฉพาะ linux โดยใช้ / proc / เป็นไปได้ที่ไม่ต้องการไพพ์ที่มีชื่อ แต่ฉันคิดว่าอันนี้ดีเกินพอ


ฟังก์ชั่นที่น่าสนใจ +1 อาจใช้ประโยคหรือเพิ่มอีก 2 คำอธิบาย( : <$FIFO & )ในรายละเอียดเพิ่มเติม ขอบคุณสำหรับการโพสต์
Alex Stragies

ฉันมองไปรอบ ๆ และทำให้มันว่างเปล่า ฉันจะหาข้อมูลเกี่ยวกับการคัดค้านได้mktempที่ไหน ฉันใช้มันอย่างกว้างขวางและหากมีเครื่องมือใหม่เข้ามาแทนที่ฉันต้องการเริ่มใช้งาน
DopeGhoti

อเล็กซ์: syscall ที่เปิด (2) บนท่อ naned กำลังปิดกั้น หากคุณพยายามที่จะ "exec <$ PIPE> $ PIPE" มันจะติดขัดรอกระบวนการอื่นเพื่อเปิดด้านอื่น ๆ คำสั่ง ": <$ FIFO &" ถูกดำเนินการใน subshell ในพื้นหลังและเปิดใช้งานการเปลี่ยนเส้นทางแบบสองทิศทางให้เสร็จสมบูรณ์
user873942

DopeGhoti: ฟังก์ชันไลบรารี mktemp (3) C เลิกใช้แล้ว ยูทิลิตี้ mktemp (1) ไม่ใช่
user873942

4

นอกจากนี้ยังมี

  • dpipeที่ "ท่อสองทิศทาง" รวมอยู่ในแพคเกจ VDE2 และรวมอยู่ในปัจจุบัน distro ระบบการจัดการแพคเกจ

    dpipe processA = processB

  • ดังนั้น , เครื่องมือเชื่อมต่อทุกอย่างเพื่อทุกอย่างเชื่อมต่อ.

    socat EXEC:Program1 EXEC:Program2

ในฐานะที่เป็น @ StéphaneChazelasอย่างถูกต้องบันทึกในความคิดเห็นที่ตัวอย่างข้างต้นคือ "รูปแบบฐาน" เขามีตัวอย่างที่ดีกับตัวเลือกในคำตอบของเขาสำหรับคำถามที่คล้ายกัน


โปรดทราบว่าโดยค่าเริ่มต้นให้socatใช้ซ็อกเก็ตแทนไพพ์ (คุณสามารถเปลี่ยนได้ด้วยcommtype=pipes) คุณอาจต้องการเพิ่มnoforkตัวเลือกเพื่อหลีกเลี่ยงกระบวนการ socat พิเศษที่ผลักข้อมูลระหว่างท่อ / ซ็อกเก็ต (ขอบคุณสำหรับการแก้ไขคำตอบของฉัน btw)
Stéphane Chazelas

0

มีคำตอบที่ยอดเยี่ยมมากมายที่นี่ ดังนั้นฉันแค่ต้องการเพิ่มบางสิ่งเพื่อให้ง่ายต่อการเล่นกับพวกเขา ฉันถือว่าstderrไม่ได้เปลี่ยนเส้นทางที่ใดก็ได้ สร้างสองสคริปต์ (สมมติว่า a.sh และ b.sh):

#!/bin/bash
echo "foo" # change to 'bar' in second file

for i in {1..10}; do
  read input
  echo ${input}
  echo ${i} ${0} got: ${input} >&2
done

จากนั้นเมื่อคุณเชื่อมต่อกับวิธีที่ดีคุณควรเห็นบนคอนโซล:

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