Można użyć kombinacji GNU stdbuf i peeod moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
sika popen(3)te 3 wiersze poleceń powłoki, a następnie freads dane wejściowe i fwrites to wszystkie trzy, które będą buforowane do 1M.
Chodzi o to, aby bufor był co najmniej tak duży jak dane wejściowe. W ten sposób, mimo że trzy polecenia są uruchamiane w tym samym czasie, będą widzieć wejście przychodzące tylko wtedy, gdy pee pclosetrzy polecenia będą kolejno.
Po każdej pclose, peeopróżnia bufor do polecenia i czeka na jego zakończenie. To gwarantuje, że dopóki te cmdxpolecenia nie zaczną wypisywać niczego, zanim nie otrzymają żadnych danych wejściowych (i nie rozwidlają procesu, który może kontynuować wysyłanie po powrocie ich rodzica), dane wyjściowe trzech poleceń nie będą przeplatane.
W rzeczywistości przypomina to użycie pliku tymczasowego w pamięci, z tą wadą, że 3 polecenia są uruchamiane jednocześnie.
Aby uniknąć jednoczesnego uruchamiania poleceń, możesz napisać peejako funkcję powłoki:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Ale uwaga, że powłoki inne niż zshzawiodłyby dla wejścia binarnego ze znakami NUL.
Pozwala to uniknąć używania plików tymczasowych, ale oznacza to, że całe wejście jest przechowywane w pamięci.
W każdym razie będziesz musiał gdzieś zapisać dane wejściowe, w pamięci lub pliku tymczasowym.
W rzeczywistości jest to dość interesujące pytanie, ponieważ pokazuje limit idei uniksowej polegającej na współpracy kilku prostych narzędzi w jednym zadaniu.
W tym miejscu chcielibyśmy mieć kilka narzędzi współpracujących z zadaniem:
- polecenie źródłowe (tutaj
echo)
- polecenie dyspozytora (
tee)
- Niektóre polecenia filtrów (
cmd1, cmd2, cmd3)
- oraz polecenie agregujące (
cat).
Byłoby miło, gdyby wszyscy mogli pracować razem w tym samym czasie i ciężko pracować na danych, które mają przetwarzać, gdy tylko będą dostępne.
W przypadku jednego polecenia filtru jest to łatwe:
src | tee | cmd1 | cat
Wszystkie polecenia są uruchamiane jednocześnie, cmd1zaczynają munchować dane, srcgdy tylko będą dostępne.
Teraz, dzięki trzem poleceniom filtrowania, nadal możemy zrobić to samo: uruchomić je jednocześnie i połączyć za pomocą potoków:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Co możemy stosunkowo łatwo zrobić za pomocą nazwanych potoków :
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'
(powyżej } 3<&0to obejść, że &przekierowania stdinz /dev/null, i użyć <>w celu uniknięcia otwarcia rury do bloku aż do drugiego końca ( cat) jest otwarty, a)
Lub, aby uniknąć nazwanych potoków, nieco bardziej boleśnie z zshcoproc:
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'
Teraz pytanie brzmi: kiedy wszystkie programy zostaną uruchomione i połączone, czy dane przepłyną?
Mamy dwa przeciwwskazania:
tee przesyła wszystkie swoje dane wyjściowe z tą samą prędkością, dzięki czemu może wysyłać dane tylko z prędkością najwolniejszej rury wyjściowej.
cat zacznie czytać od drugiej rury (rura 6 na powyższym rysunku) dopiero po odczytaniu wszystkich danych z pierwszej (5).
Oznacza to, że dane nie będą płynąć w rurze 6, dopóki cmd1nie zostaną zakończone. I, podobnie jak w tr b Bpowyższym przypadku, może to oznaczać, że dane również nie będą płynąć w rurze 3, co oznacza, że nie będzie płynąć w żadnej z rur 2, 3 lub 4, ponieważ teeprzesyła je najwolniej ze wszystkich 3.
W praktyce rury te mają niepustą wielkość, więc niektóre dane zdołają się przedostać, a przynajmniej w moim systemie mogę sprawić, aby działał do:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Poza tym z
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Mamy impas, w którym znajdujemy się w takiej sytuacji:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Wypełniliśmy rury 3 i 6 (64 kB każda). teeprzeczytał ten dodatkowy bajt, nakarmił go cmd1, ale
- teraz blokuje pisanie na potoku 3, ponieważ czeka na
cmd2jego opróżnienie
cmd2nie można go opróżnić, ponieważ jest zablokowany zapis na potoku 6, czekając na catjego opróżnienie
cat nie można go opróżnić, ponieważ czeka, aż nie będzie już żadnych danych wejściowych w rurze 5.
cmd1nie mogę powiedzieć, catże nie ma już danych wejściowych, ponieważ sam oczekuje na więcej danych wejściowych tee.
- i
teenie mogę powiedzieć, cmd1że nie ma już danych wejściowych, ponieważ jest zablokowany ... i tak dalej.
Mamy pętlę zależności, a zatem impas.
Jakie jest rozwiązanie? Zrobiłyby to większe rury 3 i 4 (wystarczająco duże, aby pomieścić całość danych srcwyjściowych). Możemy to zrobić na przykład, wstawiając pv -qB 1Gpomiędzy teei cmd2/3gdzie pvmożna przechowywać do 1G danych oczekujących cmd2i cmd3odczytujących je. Oznaczałoby to dwie rzeczy:
- to zużywa potencjalnie dużo pamięci, a ponadto powiela ją
- to nie współpracuje ze wszystkimi 3 poleceniami, ponieważ
cmd2w rzeczywistości zacząłby przetwarzać dane dopiero po zakończeniu cmd1.
Rozwiązaniem drugiego problemu byłoby zwiększenie również rur 6 i 7. Zakładając to cmd2i cmd3wytwarzając tyle danych, ile zużywają, nie zużyłoby to więcej pamięci.
Jedynym sposobem uniknięcia duplikowania danych (w pierwszym problemie) byłoby zaimplementowanie zatrzymywania danych w samym dyspozytorze, czyli wprowadzenie wariantu, teektóry może przesyłać dane z prędkością najszybszego wyjścia (przechowywanie danych w celu dostarczenia wolniejsze we własnym tempie). Niezbyt trywialne.
Ostatecznie najlepsze, co możemy rozsądnie uzyskać bez programowania, to prawdopodobnie coś w rodzaju (składnia 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