Bạn có thể sử dụng kết hợp GNU stdbuf và pee
từ moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
pee popen(3)
s 3 dòng lệnh shell đó và sau đó fread
s đầu vào và fwrite
s cả ba dòng, sẽ được đệm đến 1M.
Ý tưởng là có một bộ đệm ít nhất là lớn như đầu vào. Theo cách này, mặc dù ba lệnh được khởi động cùng một lúc, chúng sẽ chỉ thấy đầu vào đến khi pee
pclose
s ba lệnh liên tục.
Sau mỗi lần pclose
, hãy pee
xóa bộ đệm cho lệnh và chờ kết thúc. Điều đó đảm bảo rằng miễn là các cmdx
lệnh đó không bắt đầu xuất bất cứ thứ gì trước khi chúng nhận được bất kỳ đầu vào nào (và không rẽ nhánh một quá trình có thể tiếp tục xuất ra sau khi cha mẹ chúng quay trở lại), đầu ra của ba lệnh sẽ không xen kẽ.
Trên thực tế, đó là một chút giống như sử dụng tệp tạm thời trong bộ nhớ, với nhược điểm là 3 lệnh được bắt đầu đồng thời.
Để tránh bắt đầu các lệnh đồng thời, bạn có thể viết pee
dưới dạng hàm shell:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Nhưng hãy cẩn thận, các shell khác với zsh
sẽ không thành công cho đầu vào nhị phân với các ký tự NUL.
Điều đó tránh sử dụng các tệp tạm thời, nhưng điều đó có nghĩa là toàn bộ đầu vào được lưu trữ trong bộ nhớ.
Trong mọi trường hợp, bạn sẽ phải lưu trữ đầu vào ở đâu đó, trong bộ nhớ hoặc tệp tạm thời.
Trên thực tế, đây là một câu hỏi khá thú vị, vì nó cho chúng ta thấy giới hạn của ý tưởng Unix về việc có một số công cụ đơn giản hợp tác với một nhiệm vụ duy nhất.
Ở đây, chúng tôi muốn có một số công cụ hợp tác với nhiệm vụ:
- một lệnh nguồn (ở đây
echo
)
- một lệnh điều phối (
tee
)
- một số lệnh lọc (
cmd1
, cmd2
, cmd3
)
- và một lệnh tổng hợp (
cat
).
Sẽ thật tuyệt nếu tất cả họ có thể chạy cùng một lúc và làm việc chăm chỉ với dữ liệu mà họ dự định sẽ xử lý ngay khi có sẵn.
Trong trường hợp có một lệnh lọc, thật dễ dàng:
src | tee | cmd1 | cat
Tất cả các lệnh được chạy đồng thời, cmd1
bắt đầu nhai dữ liệu src
ngay khi có sẵn.
Bây giờ, với ba lệnh lọc, chúng ta vẫn có thể làm tương tự: khởi động chúng đồng thời và kết nối chúng với các đường ống:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Mà chúng ta có thể làm tương đối dễ dàng với các đường ống được đặt tên :
pee() (
mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
{ tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
eval "$1 < tee-cmd1 1<> cmd1-cat &"
eval "$2 < tee-cmd2 1<> cmd2-cat &"
eval "$3 < tee-cmd3 1<> cmd3-cat &"
exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
(ở trên } 3<&0
là để giải quyết vấn đề &
chuyển hướng stdin
từ /dev/null
và chúng tôi sử dụng <>
để tránh việc mở các đường ống để chặn cho đến khi đầu kia ( cat
) cũng mở ra)
Hoặc để tránh các đường ống được đặt tên, đau đớn hơn một chút với zsh
coproc:
pee() (
n=0 ci= co= is=() os=()
for cmd do
eval "coproc $cmd $ci $co"
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
Bây giờ, câu hỏi là: một khi tất cả các chương trình được khởi động và kết nối, liệu luồng dữ liệu sẽ?
Chúng tôi có hai điều trái ngược:
tee
cung cấp cho tất cả các đầu ra của nó ở cùng một tốc độ, vì vậy nó chỉ có thể gửi dữ liệu ở tốc độ của ống đầu ra chậm nhất của nó.
cat
sẽ chỉ bắt đầu đọc từ ống thứ hai (ống 6 trong bản vẽ trên) khi tất cả dữ liệu đã được đọc từ ống thứ nhất (5).
Điều đó có nghĩa là dữ liệu sẽ không chảy trong ống 6 cho đến khi cmd1
kết thúc. Và, giống như trong trường hợp tr b B
ở trên, điều đó có thể có nghĩa là dữ liệu sẽ không chảy trong ống 3, điều đó có nghĩa là nó sẽ không chảy trong bất kỳ ống 2, 3 hoặc 4 nào vì tee
thức ăn có tốc độ chậm nhất trong cả 3.
Trong thực tế, các đường ống đó có kích thước không rỗng, vì vậy một số dữ liệu sẽ quản lý để vượt qua và ít nhất trên hệ thống của tôi, tôi có thể làm cho nó hoạt động tới:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Ngoài ra, với
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Chúng ta đã có một bế tắc, trong đó chúng ta đang ở trong tình huống này:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Chúng tôi đã lấp đầy các ống 3 và 6 (mỗi ống 64kiB). tee
đã đọc thêm byte đó, nó đã đưa nó vào cmd1
, nhưng
- Bây giờ nó bị chặn viết trên ống 3 vì nó đang chờ để
cmd2
làm trống nó
cmd2
không thể làm trống nó bởi vì nó bị chặn viết trên ống 6, chờ để cat
làm trống nó
cat
không thể làm trống nó bởi vì nó chờ cho đến khi không còn đầu vào trên ống 5.
cmd1
không thể nói cat
không có thêm đầu vào vì nó đang chờ thêm đầu vào từ đó tee
.
- và
tee
không thể nói cmd1
không có thêm đầu vào vì nó bị chặn ... vân vân.
Chúng ta đã có một vòng lặp phụ thuộc và do đó bế tắc.
Bây giờ, giải pháp là gì? Các ống lớn hơn 3 và 4 (đủ lớn để chứa tất cả src
đầu ra) sẽ làm điều đó. Chúng ta có thể làm điều đó chẳng hạn bằng cách chèn pv -qB 1G
giữa tee
và cmd2/3
nơi pv
có thể lưu trữ tới 1G dữ liệu đang chờ cmd2
và cmd3
đọc chúng. Điều đó có nghĩa là hai điều mặc dù:
- đó là sử dụng rất nhiều bộ nhớ và hơn thế nữa, sao chép nó
- điều đó không có cả 3 lệnh hợp tác vì
cmd2
trong thực tế sẽ chỉ bắt đầu xử lý dữ liệu khi cmd1 kết thúc.
Một giải pháp cho vấn đề thứ hai là làm cho ống 6 và 7 lớn hơn. Giả sử rằng cmd2
và cmd3
tạo ra nhiều sản lượng như họ tiêu thụ, điều đó sẽ không tiêu tốn nhiều bộ nhớ hơn.
Cách duy nhất để tránh trùng lặp dữ liệu (trong vấn đề đầu tiên) là thực hiện việc lưu giữ dữ liệu trong chính bộ điều phối, đó là thực hiện một biến thể trên tee
đó có thể cung cấp dữ liệu ở tốc độ đầu ra nhanh nhất (giữ dữ liệu để cung cấp dữ liệu những người chậm hơn ở tốc độ của riêng họ). Không thực sự tầm thường.
Vì vậy, cuối cùng, thứ tốt nhất chúng ta có thể có được mà không cần lập trình có lẽ là một cái gì đó giống như (cú pháp Zsh):
max_hold=1G
pee() (
n=0 ci= co= is=() os=()
for cmd do
if ((n)); then
eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
else
eval "coproc $cmd $ci $co"
fi
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c