tee + cat: utilisez plusieurs fois une sortie, puis concaténez les résultats

18

Si j'appelle une commande, par exemple, echoje peux utiliser les résultats de cette commande dans plusieurs autres commandes avec tee. Exemple:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

Avec cat, je peux collecter les résultats de plusieurs commandes. Exemple:

cat <(command1) <(command2) <(command3)

Je voudrais pouvoir faire les deux choses en même temps, afin que je puisse utiliser teepour appeler ces commandes sur la sortie de quelque chose d'autre (par exemple, echoj'ai écrit), puis collecter tous leurs résultats sur une seule sortie avec cat.

Il est important de garder les résultats dans l'ordre, cela signifie que les lignes dans la sortie de command1, command2et command3ne doivent pas être entrelacées, mais ordonnées comme les commandes sont (comme cela arrive avec cat).

Il peut y avoir de meilleures options que catet , teemais ce sont ceux que je connais jusqu'à présent.

Je veux éviter d'utiliser des fichiers temporaires car la taille de l'entrée et de la sortie peut être importante.

Comment pourrais-je faire ça?

PD: un autre problème est que cela se produit en boucle, ce qui rend la gestion des fichiers temporaires plus difficile. C'est le code actuel que j'ai et il fonctionne pour les petits tests, mais il crée des boucles infinies lors de la lecture et de l'écriture à partir du fichier auxiliaire d'une manière que je ne comprends pas.

somefunction()
{
  if [ $1 -eq 1 ]
  then
    echo "Hello world!"
  else
    somefunction $(( $1 - 1 )) > auxfile
    cat <(command1 < auxfile) \
        <(command2 < auxfile) \
        <(command3 < auxfile)
  fi
}

Les lectures et les écrits dans auxfile semblent se chevaucher, ce qui fait tout exploser.

Trylks
la source
2
De quelle taille parlons-nous? Vos exigences obligent tout à être gardé en mémoire. Garder les résultats dans l'ordre signifie que la commande1 doit terminer en premier (donc elle a vraisemblablement lu l'entrée entière et imprimé la sortie entière), avant que la commande2 et la commande3 puissent même commencer le traitement (sauf si vous souhaitez collecter leur sortie en mémoire au début aussi).
frostschutz
vous avez raison, les entrées et sorties de command2 et command3 sont trop grandes pour être gardées en mémoire. Je m'attendais à ce que l'utilisation de swap fonctionne mieux que l'utilisation de fichiers temporaires. Un autre problème que j'ai, c'est que cela se produit en boucle, ce qui rend la gestion des fichiers encore plus difficile. J'utilise un seul fichier mais en ce moment pour une raison quelconque, il y a un chevauchement dans la lecture et l'écriture du fichier qui le fait croître à l'infini. Je vais essayer de mettre à jour la question sans vous ennuyer avec trop de détails.
Trylks
4
Vous devez utiliser des fichiers temporaires; soit pour l'entrée echo HelloWorld > file; (command1<file;command2<file;command3<file)soit pour la sortie echo | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output. C'est comme ça que ça fonctionne - tee ne peut fourcher des entrées que si toutes les commandes fonctionnent et sont traitées en parallèle. si une commande est en sommeil (parce que vous ne voulez pas d'entrelacement), elle bloquera simplement toutes les commandes, afin d'éviter de remplir la mémoire en entrée ...
frostschutz

Réponses:

27

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/3pvpourrait stocker jusqu'à 1G de données en attente cmd2et cmd3en lecture. Cela signifierait cependant deux choses:

  1. qui utilise potentiellement beaucoup de mémoire, et en plus, la dupliquer
  2. 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
Stéphane Chazelas
la source
Vous avez raison, le blocage est le plus gros problème que j'ai trouvé jusqu'à présent pour éviter d'utiliser des fichiers temporaires. Ces fichiers semblent être assez rapides, cependant, je ne sais pas s'ils sont mis en cache quelque part, j'avais peur des temps d'accès au disque, mais ils semblent raisonnables jusqu'à présent.
Trylks
6
Un extra +1 pour le bel art ASCII :-)
Kurt Pfeifle
3

Ce que vous proposez ne peut pas être fait facilement avec une commande existante et n'a pas beaucoup de sens de toute façon. L'idée générale des canaux ( |sous Unix / Linux) est que dans cmd1 | cmd2la cmd1sortie d'écriture (au plus) jusqu'à ce qu'un tampon de mémoire se remplisse, puis cmd2exécute la lecture des données du tampon (au plus) jusqu'à ce qu'il soit vide. C'est-à-dire, cmd1et cmd2fonctionner en même temps, il n'est jamais nécessaire d'avoir plus qu'une quantité limitée de données "en vol" entre eux. Si vous souhaitez connecter plusieurs entrées à une seule sortie, si l'un des lecteurs est à la traîne des autres, soit vous arrêtez les autres (quel est l'intérêt de fonctionner en parallèle alors?) Soit vous cachez la sortie que le retardataire n'a pas encore lue (à quoi bon alors ne pas avoir de fichier intermédiaire?). plus complexe.

Dans mes presque 30 ans d'expérience sur Unix, je ne me souviens d'aucune situation qui aurait vraiment profité à un tel tube à sorties multiples.

Vous pouvez combiner plusieurs sorties en un seul aujourd'hui flux, tout en aucune façon entrelacée (comment les sorties cmd1et cmd2entrelacer? Une ligne à son tour? Tour de rôle d' écriture de 10 octets? Alternate « paragraphes » définis en quelque sorte? Et si l' on vient n » t écrire quoi que ce soit pendant longtemps - tout cela est complexe à gérer). Il est effectué par exemple par (cmd1; cmd2; cmd3) | cmd4les programmes cmd1, cmd2et cmd3est exécuté l'un après l'autre, la sortie est envoyée en entrée à cmd4.

vonbrand
la source
3

Pour votre problème de chevauchement, sur Linux (et avec bashou zshmais pas avec ksh93), vous pouvez le faire comme:

somefunction()
(
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    exec 3> auxfile
    rm -f auxfile
    somefunction "$(($1 - 1))" >&3 auxfile 3>&-
    exec cat <(command1 < /dev/fd/3) \
             <(command2 < /dev/fd/3) \
             <(command3 < /dev/fd/3)
  fi
)

Notez l'utilisation de (...)au lieu de {...}pour obtenir un nouveau processus à chaque itération afin que nous puissions avoir un nouveau fd 3 pointant vers un nouveau auxfile. < /dev/fd/3est une astuce pour accéder à ce fichier maintenant supprimé. Il ne fonctionnera pas sur des systèmes autres que Linux où < /dev/fd/3est similaire dup2(3, 0)et donc fd 0 serait ouvert en mode écriture seule avec le curseur à la fin du fichier.

Pour éviter le fork de la fonction imbriquée, vous pouvez l'écrire comme suit:

somefunction()
{
  if [ "$1" -eq 1 ]
  then
    echo "Hello world!"
  else
    {
      rm -f auxfile
      somefunction "$(($1 - 1))" >&3 auxfile 3>&-
      exec cat <(command1 < /dev/fd/3) \
               <(command2 < /dev/fd/3) \
               <(command3 < /dev/fd/3)
    } 3> auxfile
  fi
}

Le shell se chargerait de sauvegarder le fd 3 à chaque itération. Vous finiriez par manquer de descripteurs de fichiers plus tôt.

Bien que vous constatiez qu'il est plus efficace de le faire comme:

somefunction() {
  if [ "$1" -eq 1 ]; then
    echo "Hello world!" > auxfile
  else
    somefunction "$(($1 - 1))"
    { rm -f auxfile
      cat <(command1 < /dev/fd/3) \
          <(command2 < /dev/fd/3) \
          <(command3 < /dev/fd/3) > auxfile
    } 3< auxfile
  fi
}
somefunction 12; cat auxfile

Autrement dit, ne pas imbriquer les redirections.

Stéphane Chazelas
la source