Można użyć kombinacji GNU stdbuf i pee
od moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
sika popen(3)
te 3 wiersze poleceń powłoki, a następnie fread
s dane wejściowe i fwrite
s 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
pclose
trzy polecenia będą kolejno.
Po każdej pclose
, pee
opróżnia bufor do polecenia i czeka na jego zakończenie. To gwarantuje, że dopóki te cmdx
polecenia 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ć pee
jako 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ż zsh
zawiodł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, cmd1
zaczynają munchować dane, src
gdy 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<&0
to obejść, że &
przekierowania stdin
z /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 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'
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 cmd1
nie zostaną zakończone. I, podobnie jak w tr b B
powyż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ż tee
przesył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). tee
przeczytał ten dodatkowy bajt, nakarmił go cmd1
, ale
- teraz blokuje pisanie na potoku 3, ponieważ czeka na
cmd2
jego opróżnienie
cmd2
nie można go opróżnić, ponieważ jest zablokowany zapis na potoku 6, czekając na cat
jego opróżnienie
cat
nie można go opróżnić, ponieważ czeka, aż nie będzie już żadnych danych wejściowych w rurze 5.
cmd1
nie mogę powiedzieć, cat
że nie ma już danych wejściowych, ponieważ sam oczekuje na więcej danych wejściowych tee
.
- i
tee
nie 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 src
wyjściowych). Możemy to zrobić na przykład, wstawiając pv -qB 1G
pomiędzy tee
i cmd2/3
gdzie pv
można przechowywać do 1G danych oczekujących cmd2
i cmd3
odczytują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ż
cmd2
w 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 cmd2
i cmd3
wytwarzają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, tee
któ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