Comment puis-je implémenter un flux circulaire de données entre les commandes interconnectées?

19

Je connais de deux types comment les commandes peuvent être connectées les unes aux autres:

  1. en utilisant un Pipe (en mettant std-output dans std-input de la commande suivante).
  2. en utilisant un Tee (épissez la sortie en plusieurs sorties).

Je ne sais pas si c'est tout ce qui est possible, je dessine donc un type de connexion hypothétique:

entrez la description de l'image ici

Comment pourrait-il être possible d'implémenter un flux circulaire de données entre des commandes comme par exemple dans ce pseudo code, où j'utilise des variables au lieu de commandes:

pseudo-code:

a = 1    # start condition 

repeat 
{
b = tripple(a)
c = sin(b) 
a = c + 1 
}
Abdul Al Hazred
la source

Réponses:

16

Boucle d'E / S circulaire implémentée avec tail -f

Cela implémente une boucle d'E / S circulaire:

$ echo 1 >file
$ tail -f file | while read n; do echo $((n+1)); sleep 1; done | tee -a file
2
3
4
5
6
7
[..snip...]

Cela implémente la boucle circulaire d'entrée / sortie en utilisant l'algorithme sinus que vous avez mentionné:

$ echo 1 >file
$ tail -f file | while read n; do echo "1+s(3*$n)" | bc -l; sleep 1; done | tee -a file
1.14112000805986722210
.72194624281527439351
1.82812473159858353270
.28347272185896349481
1.75155632167982146959
[..snip...]

Ici, bcfait le calcul en virgule flottante et s(...)est la notation de bc pour la fonction sinus.

Implémentation du même algorithme en utilisant une variable à la place

Pour cet exemple mathématique particulier, l'approche E / S circulaire n'est pas nécessaire. On pourrait simplement mettre à jour une variable:

$ n=1; while true; do n=$(echo "1+s(3*$n)" | bc -l); echo $n; sleep 1; done
1.14112000805986722210
.72194624281527439351
1.82812473159858353270
.28347272185896349481
[..snip...]
John1024
la source
12

Vous pouvez utiliser un FIFO pour cela, créé avec mkfifo. Notez cependant qu'il est très facile de créer accidentellement une impasse. Permettez-moi d'expliquer cela - prenez votre hypothétique exemple «circulaire». Vous alimentez la sortie d'une commande à son entrée. Il y a au moins deux façons de bloquer cela:

  1. La commande a un tampon de sortie. Il est partiellement rempli, mais n'a pas encore été vidé (en fait écrit). Il le fera une fois rempli. Il revient donc à lire son entrée. Il restera là pour toujours, car l'entrée qu'il attend est en fait dans le tampon de sortie. Et il ne sera pas vidé tant qu'il n'aura pas cette entrée ...

  2. La commande a un tas de sortie à écrire. Il commence à l'écrire, mais le tampon du tube du noyau se remplit. Il se trouve donc là, attendant que leur espace soit dans le tampon. Cela se produira dès qu'il lira son entrée, c'est-à-dire jamais comme il ne le fera pas tant qu'il n'aura pas fini d'écrire quoi que ce soit sur sa sortie.

Cela dit, voici comment procéder. Cet exemple est avec od, pour créer une chaîne sans fin de vidages hexadécimaux:

mkfifo fifo
( echo "we need enough to make it actually write a line out"; cat fifo ) \ 
    | stdbuf -i0 -o0 -- od -t x1 | tee fifo

Notez que finalement s'arrête. Pourquoi? Il est dans l'impasse, n ° 2 ci-dessus. Vous pouvez également remarquer l' stdbufappel, pour désactiver la mise en mémoire tampon. Sans ça? Blocages sans aucune sortie.

derobert
la source
merci, je ne connaissais rien aux tampons dans ce contexte, connaissez-vous quelques mots clés pour en savoir plus?
Abdul Al Hazred
1
@AbdulAlHazred Pour la mise en mémoire tampon des entrées / sorties, recherchez la mise en mémoire tampon stdio . Pour le tampon du noyau dans un tube, le tampon de tube semble fonctionner.
derobert
4

En général, j'utiliserais un Makefile (commande make) et j'essaierais de mapper votre diagramme aux règles du makefile.

f1 f2 : f0
      command < f0 > f1 2>f2

Pour avoir des commandes répétitives / cycliques, nous devons définir une politique d'itération. Avec:

SHELL=/bin/bash

a.out : accumulator
    cat accumulator <(date) > a.out
    cp a.out accumulator

accumulator:
    touch accumulator     #initial value

chacun makeproduira une itération à la fois.

JJoao
la source
Abus mignon make, mais inutile: si vous utilisez un fichier intermédiaire, pourquoi ne pas simplement utiliser une boucle pour le gérer?
alexis
@alexis, les makefiles sont probablement exagérés. Je ne suis pas très à l'aise avec les boucles: je manque la notion d'horloge, l'état d'arrêt ou un exemple clair. Les diagrammes initiaux me rappelaient les workflows et les signatures de fonction. Pour les diagrammes complexes, nous finirons par avoir besoin de connexions de données ou de règles typées makefile. (ce n'est qu'une intuition abusive)
JJoao
@alexis, et bien sûr, je suis d'accord avec vous.
JJoao
Je ne pense pas que ce soit un abus - makec'est une question de macros qui est une application parfaite ici.
mikeserv
1
@mikeserv, oui. Et nous savons tous que l'abus des outils est la Magna Carta souterraine d'Unix :)
JJoao
4

Vous savez, je ne suis pas convaincu que vous ayez nécessairement besoin d'une boucle de rétroaction répétitive comme le montrent vos diagrammes, autant que vous pourriez peut-être utiliser un pipeline persistant entre les coprocessus . Là encore, il se peut qu'il n'y ait pas trop de différence - une fois que vous ouvrez une ligne sur un coprocessus, vous pouvez implémenter des boucles de style typiques en écrivant et en lisant des informations sans rien faire de très inhabituel.

En premier lieu, il semblerait que ce bcsoit un candidat de choix pour un coprocessus pour vous. Dans bcvous pouvez des definefonctions qui peuvent faire à peu près ce que vous demandez dans votre pseudocode. Par exemple, certaines fonctions très simples pour ce faire pourraient ressembler à:

printf '%s()\n' b c a |
3<&0 <&- bc -l <<\IN <&3
a=1; b=0; c=0;
define a(){ "a="; return (a = c+1); }
define b(){ "b="; return (b = 3*a); }
define c(){ "c="; return (c = s(b)); }
IN

... qui imprimerait ...

b=3
c=.14112000805986722210
a=1.14112000805986722210

Mais bien sûr, cela ne dure pas . Dès que le sous-shell en charge de printfla pipe de 's se ferme (juste après avoir printfécrit a()\ndans la pipe) la pipe est détruite et bcl'entrée de' ferme 'et elle se ferme aussi. Ce n'est pas aussi utile qu'il pourrait l'être.

@derobert a déjà mentionné les FIFO comme cela peut être fait en créant un fichier de canal nommé avec l' mkfifoutilitaire. Ce ne sont essentiellement que des canaux, sauf que le noyau système relie une entrée de système de fichiers aux deux extrémités. Celles-ci sont très utiles, mais ce serait mieux si vous pouviez simplement avoir un tuyau sans risquer qu'il soit espionné dans le système de fichiers.

En fait, votre shell fait beaucoup cela. Si vous utilisez un shell qui implémente la substitution de processus, vous disposez d'un moyen très simple d'obtenir un canal durable - du type que vous pourriez attribuer à un processus en arrière-plan avec lequel vous pouvez communiquer.

Dans bash, par exemple, vous pouvez voir comment fonctionne la substitution de processus:

bash -cx ': <(:)'
+ : /dev/fd/63

Vous voyez, c'est vraiment une substitution . Le shell substitue une valeur lors de l'expansion qui correspond au chemin vers un lien vers un tuyau . Vous pouvez en profiter - vous n'avez pas besoin d'être contraint d'utiliser ce canal uniquement pour communiquer avec le processus exécuté dans la ()substitution elle-même ...

bash -c '
    eval "exec 3<>"<(:) "4<>"<(:)
    cat  <&4 >&3  &
    echo hey cat >&4
    read hiback  <&3
    echo "$hiback" here'

... qui imprime ...

hey cat here

Maintenant, je sais que différents shells font le coprocessus de différentes manières - et qu'il y a une syntaxe spécifique bashpour en configurer un (et probablement un pour zshaussi) - mais je ne sais pas comment ces choses fonctionnent. Je sais juste que vous pouvez utiliser la syntaxe ci-dessus pour faire pratiquement la même chose sans tout le rigmarole dans les deux bashet zsh- et vous pouvez faire une chose très similaire dans dashet busybox ashpour atteindre le même but avec ici-documents (parce que dashet busyboxfaites ici- documents avec des tuyaux plutôt que des fichiers temporaires comme les deux autres) .

Donc, lorsqu'il est appliqué à bc...

eval "exec 3<>"<(:) "4<>"<(:)
bc -l <<\INIT <&4 >&3 &
a=1; b=0; c=0;
define a(){ "a="; return (a = c+1); }
define b(){ "b="; return (b = 3*a); }
define c(){ "c="; return (c = s(b)); }
INIT
export BCOUT=3 BCIN=4 BCPID="$!"

... c'est la partie difficile. Et c'est la partie amusante ...

set --
until [ "$#" -eq 10 ]
do    printf '%s()\n' b c a >&"$BCIN"
      set "$@" "$(head -n 3 <&"$BCOUT")"
done; printf %s\\n "$@"

... qui imprime ...

b=3
c=.14112000805986722210
a=1.14112000805986722210
#...24 more lines...
b=3.92307618030433853649
c=-.70433330413228041035
a=.29566669586771958965

... et il fonctionne toujours ...

echo a >&"$BCIN"
read a <&"$BCOUT"
echo "$a"

... ce qui me donne juste la dernière valeur de bc's aplutôt que d'appeler la a()fonction pour l'incrémenter et l'imprimer ...

.29566669586771958965

Il continuera de fonctionner, en fait, jusqu'à ce que je le tue et que j'arrache ses tuyaux IPC ...

kill "$BCPID"; exec 3>&- 4>&-
unset BCPID BCIN BCOUT
mikeserv
la source
1
Très intéressant. Notez qu'avec bash et zsh récents, vous n'avez pas besoin de spécifier le descripteur de fichier, par exemple eval "exec {BCOUT}<>"<(:) "{BCIN}<>"<(:)fonctionne aussi
Thor