Vous pouvez utiliser une combinaison de GNU stdbuf et peede moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
pipi popen(3)s ces 3 lignes de commande shell, puis freads l'entrée et fwrites 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 pcloseles trois commandes seront séquentiellement.
À chaque fois pclose, peevide le tampon de la commande et attend sa fin. Cela garantit que tant que ces cmdxcommandes 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 peecomme 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 zshqui é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, cmd1commence à grignoter des données srcdè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<&0s'agit de contourner le fait que les &redirections stdindepuis /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 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'
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 cmd1fin. Et, comme dans le cas tr b Bci - 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 teese 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). teea lu cet octet supplémentaire, il l'a alimenté cmd1, mais
- il est maintenant bloqué d'écrire sur le tuyau 3 en attendant
cmd2de le vider
cmd2ne peut pas le vider car il est bloqué en train d'écrire sur le pipe 6, en attendant catde le vider
cat ne peut pas le vider car il attend qu'il n'y ait plus d'entrée sur le tuyau 5.
cmd1ne peut pas dire catqu'il n'y a plus d'entrée car il attend lui-même plus d'entrée tee.
- et
teene peut pas dire cmd1qu'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 srcla sortie de) le feraient. Nous pourrions le faire par exemple en insérant pv -qB 1Gentre teeet cmd2/3où pvpourrait stocker jusqu'à 1G de données en attente cmd2et cmd3en 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
cmd2ne 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 cmd2et en cmd3produisant 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 teequi 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