Vous pouvez utiliser une combinaison de GNU stdbuf et pee
de moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
pipi popen(3)
s ces 3 lignes de commande shell, puis fread
s l'entrée et fwrite
s tous les trois, qui seront mis en mémoire tampon jusqu'à 1M.
L'idée est d'avoir un tampon au moins aussi grand que l'entrée. De cette façon, même si les trois commandes sont démarrées en même temps, elles ne verront l'entrée entrer que lorsque pee
pclose
les trois commandes seront séquentiellement.
À chaque fois pclose
, pee
vide le tampon de la commande et attend sa fin. Cela garantit que tant que ces cmdx
commandes ne commenceront rien à produire avant d'avoir reçu une entrée (et ne déclenchent pas un processus qui peut continuer à sortir après le retour de leur parent), la sortie des trois commandes ne sera pas entrelacé.
En effet, c'est un peu comme utiliser un fichier temporaire en mémoire, avec l'inconvénient que les 3 commandes sont démarrées simultanément.
Pour éviter de démarrer les commandes simultanément, vous pouvez écrire pee
comme une fonction shell:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Mais attention, les shells autres que ceux zsh
qui échoueraient pour une entrée binaire avec des caractères NUL.
Cela évite d'utiliser des fichiers temporaires, mais cela signifie que toute l'entrée est stockée en mémoire.
Dans tous les cas, vous devrez stocker l'entrée quelque part, en mémoire ou dans un fichier temporaire.
En fait, c'est une question assez intéressante, car elle nous montre la limite de l'idée Unix d'avoir plusieurs outils simples coopérant à une seule tâche.
Ici, nous aimerions que plusieurs outils coopèrent à la tâche:
- une commande source (ici
echo
)
- une commande dispatcher (
tee
)
- certaines commandes de filtre (
cmd1
, cmd2
, cmd3
)
- et une commande d'agrégation (
cat
).
Ce serait bien s'ils pouvaient tous fonctionner ensemble en même temps et faire leur travail acharné sur les données qu'ils sont censés traiter dès qu'elles sont disponibles.
Dans le cas d'une commande de filtre, c'est simple:
src | tee | cmd1 | cat
Toutes les commandes sont exécutées simultanément, cmd1
commence à grignoter des données src
dès qu'elles sont disponibles.
Maintenant, avec trois commandes de filtrage, nous pouvons toujours faire la même chose: démarrez-les simultanément et connectez-les avec des tuyaux:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Ce que nous pouvons faire relativement facilement avec des pipes nommées :
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'
(Au-dessus de, il } 3<&0
s'agit de contourner le fait que les &
redirections stdin
depuis /dev/null
, et nous utilisons <>
pour éviter l'ouverture des tuyaux à bloquer jusqu'à ce que l'autre extrémité ( cat
) soit également ouverte)
Ou pour éviter les pipes nommées, un peu plus douloureusement avec 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'
Maintenant, la question est: une fois tous les programmes démarrés et connectés, les données circuleront-elles?
Nous avons deux contraintes:
tee
alimente toutes ses sorties au même taux, il ne peut donc envoyer des données qu'au taux de son canal de sortie le plus lent.
cat
ne commencera la lecture à partir du deuxième tuyau (tuyau 6 dans le dessin ci-dessus) que lorsque toutes les données auront été lues à partir du premier (5).
Cela signifie que les données ne circuleront pas dans le tuyau 6 avant la cmd1
fin. Et, comme dans le cas tr b B
ci - dessus, cela peut signifier que les données ne circuleront pas non plus dans le tuyau 3, ce qui signifie qu'elles ne circuleront dans aucun des tuyaux 2, 3 ou 4, car elles tee
se nourrissent au débit le plus lent des 3.
En pratique, ces canaux ont une taille non nulle, donc certaines données réussiront à passer, et sur mon système au moins, je peux le faire fonctionner jusqu'à:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Au-delà, avec
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Nous avons une impasse, où nous sommes dans cette situation:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Nous avons rempli les tuyaux 3 et 6 (64 ko chacun). tee
a lu cet octet supplémentaire, il l'a alimenté cmd1
, mais
- il est maintenant bloqué d'écrire sur le tuyau 3 en attendant
cmd2
de le vider
cmd2
ne peut pas le vider car il est bloqué en train d'écrire sur le pipe 6, en attendant cat
de le vider
cat
ne peut pas le vider car il attend qu'il n'y ait plus d'entrée sur le tuyau 5.
cmd1
ne peut pas dire cat
qu'il n'y a plus d'entrée car il attend lui-même plus d'entrée tee
.
- et
tee
ne peut pas dire cmd1
qu'il n'y a plus d'entrée car elle est bloquée ... et ainsi de suite.
Nous avons une boucle de dépendance et donc un blocage.
Maintenant, quelle est la solution? De plus gros tuyaux 3 et 4 (assez gros pour contenir toute src
la sortie de) le feraient. Nous pourrions le faire par exemple en insérant pv -qB 1G
entre tee
et cmd2/3
où pv
pourrait stocker jusqu'à 1G de données en attente cmd2
et cmd3
en lecture. Cela signifierait cependant deux choses:
- qui utilise potentiellement beaucoup de mémoire, et en plus, la dupliquer
- qui ne parvient pas à faire coopérer les 3 commandes, car
cmd2
ne commencerait en réalité à traiter les données que lorsque cmd1 serait terminé.
Une solution au deuxième problème consisterait à agrandir également les tuyaux 6 et 7. En supposant cela cmd2
et en cmd3
produisant autant de sortie qu’ils consomment, cela ne consommerait pas plus de mémoire.
La seule façon d'éviter la duplication des données (dans le premier problème) serait d'implémenter la rétention des données dans le répartiteur lui-même, c'est-à-dire de mettre en œuvre une variante tee
qui peut alimenter les données au rythme de la sortie la plus rapide (conserver les données pour alimenter le les plus lents à leur rythme). Pas vraiment banal.
Donc, au final, le meilleur que nous pouvons raisonnablement obtenir sans programmation est probablement quelque chose comme (syntaxe 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