Comment implémenter des «générateurs» comme $ RANDOM?

10

La variable spéciale $RANDOMa une nouvelle valeur à chaque accès. À cet égard, il rappelle les objets «générateurs» que l'on trouve dans certaines langues.

Existe-t-il un moyen de mettre en œuvre quelque chose comme ça dans zsh?

J'ai essayé de le faire avec des tuyaux nommés, mais je n'ai pas trouvé de moyen d'extraire des éléments du fifo de manière contrôlée sans tuer le processus "générateur". Par exemple:

% mkfifo /tmp/ints
% (index=0
   while ( true )
   do
       echo $index
       index=$(( index + 1 ))
   done) > /tmp/ints &
[1] 16309
% head -1 /tmp/ints
0
[1]  + broken pipe  ( index=0 ; while ( true; ); do; echo $index; index=$(( ...

Existe-t-il un autre moyen d'implémenter un tel objet de type générateur dans zsh?


EDIT: Cela ne fonctionne pas:

#!/usr/bin/env zsh

FIFO=/tmp/fifo-$$
mkfifo $FIFO
INDEX=0
while true; do echo $(( ++INDEX )) > $FIFO; done &
cat $FIFO

Si je mets ce qui précède dans un script et l'exécute, la sortie rarement la seule ligne attendue

1

il se compose plutôt de plusieurs entiers; par exemple

1
2
3
4
5

Le nombre de lignes produites varie d'une série à l'autre.

EDIT2: Comme l'a souligné jimmij, le passage echoà /bin/echoprend en charge le problème.

kjo
la source

Réponses:

10

ksh93a des disciplines qui sont généralement utilisées pour ce genre de chose. Avec zsh, vous pouvez détourner la fonction de répertoire nommé dynamique :

Définissez par exemple:

zsh_directory_name() {
  case $1 in
    (n)
      case $2 in
        (incr) reply=($((++incr)))
      esac
  esac
}

Et puis vous pouvez utiliser ~[incr]pour obtenir une incrémentation à $incrchaque fois:

$ echo ~[incr]
1
$ echo ~[incr] ~[incr]
2 3

Votre approche échoue car dans head -1 /tmp/ints, head ouvre le fifo, lit un tampon complet, imprime une ligne, puis le ferme . Une fois fermée, l'extrémité d'écriture voit un tuyau cassé.

Au lieu de cela, vous pouvez soit faire:

$ fifo=~/.generators/incr
$ (umask  077 && mkdir -p $fifo:h && rm -f $fifo && mkfifo $fifo)
$ seq infinity > $fifo &
$ exec 3< $fifo
$ IFS= read -rneu3
1
$ IFS= read -rneu3
2

Là, nous laissons la fin de lecture ouverte sur fd 3 et readlisons un octet à la fois, pas un tampon complet pour être sûr de lire exactement une ligne (jusqu'au caractère de nouvelle ligne).

Ou vous pourriez faire:

$ fifo=~/.generators/incr
$ (umask  077 && mkdir -p $fifo:h && rm -f $fifo && mkfifo $fifo)
$ while true; do echo $((++incr)) > $fifo; done &
$ cat $fifo
1
$ cat $fifo
2

Cette fois, nous instancions un tuyau pour chaque valeur. Cela permet de renvoyer des données contenant un nombre arbitraire de lignes.

Cependant, dans ce cas, dès que catle fifo est ouvert, la echoboucle et est débloquée, donc plus echopourrait être exécutée, au moment de catlire le contenu et de fermer le tuyau (ce qui oblige le prochain echoà instancier un nouveau tuyau).

Une solution de contournement pourrait être d'ajouter du retard, comme par exemple en exécutant un externe echocomme suggéré par @jimmij ou en ajouter sleep, mais cela ne serait toujours pas très robuste, ou vous pourriez recréer le canal nommé après chaque echo:

while 
  mkfifo $fifo &&
  echo $((++incr)) > $fifo &&
  rm -f $fifo
do : nothing
done &

Cela laisse toujours de courtes fenêtres où le tuyau n'existe pas (entre le unlink()fait par rmet le mknod()fait par mkfifo) provoquant l' catéchec, et des fenêtres très courtes où le tuyau a été instancié mais aucun processus n'y réécrira jamais (entre le write()et le close()done by echo) catne renvoyant rien, et des fenêtres courtes où le pipe nommé existe toujours mais rien ne l'ouvrira jamais pour écrire (entre le close()done by echoet le unlink()done by rm) où catva se bloquer.

Vous pouvez supprimer certaines de ces fenêtres en procédant comme suit:

fifo=~/.generators/incr
(
  umask  077
  mkdir -p $fifo:h && rm -f $fifo && mkfifo $fifo &&
  while
    mkfifo $fifo.new &&
    {
      mv $fifo.new $fifo &&
      echo $((++incr))
    } > $fifo
  do : nothing
  done
) &

De cette façon, le seul problème est que si vous exécutez plusieurs chats en même temps (ils ouvrent tous le fifo avant que notre boucle d'écriture soit prête à l'ouvrir pour l'écriture), auquel cas ils partageront la echosortie.

Je déconseille également la création de noms fixes, de fifos lisibles par tout le monde (ou de tout fichier important) dans des répertoires accessibles en écriture, à /tmpmoins que ce soit un service à exposer à tous les utilisateurs du système.

Stéphane Chazelas
la source
Merci. Sauf erreur, la dernière recette que vous donnez ne fonctionne pas toujours. Voir mon EDIT.
kjo
1
@kjo Essayez command echoou /bin/echoau lieu de intégré echo. De plus - vous pouvez faire cette commande un peu plus courte: repeat 999 /bin/echo $((++incr)) > /tmp/int &.
jimmij
1
@kjo, voir modifier.
Stéphane Chazelas
4

Si vous souhaitez exécuter du code chaque fois que la valeur d'une variable est lue, vous ne pouvez pas le faire à l'intérieur de zsh lui-même. La RANDOMvariable (comme d'autres variables spéciales similaires) est codée en dur dans le code source zsh. Vous pouvez cependant définir des variables spéciales similaires en écrivant un module en C. De nombreux modules standard définissent des variables spéciales.

Vous pouvez utiliser un coprocessus pour créer un générateur.

coproc { i=0; while echo $i; do ((++i)); done }
for ((x=1; x<=3; x++)) { read -p n; echo $n; }

Cependant, cela est assez limité car vous ne pouvez avoir qu'un seul coprocessus. Une autre façon d'obtenir progressivement la sortie d'un processus consiste à rediriger à partir d'une substitution de processus .

exec 3< <(i=0; while echo $i; do ((++i)); done)
for ((x=1; x<=3; x++)) { read n <&3; echo $n; }

Notez que head -1cela ne fonctionne pas ici, car il lit un tampon entier, affiche ce qu'il aime et quitte. Les données qui ont été lues dans le canal restent lues; il s'agit d'une propriété intrinsèque des canaux (vous ne pouvez pas replacer les données). Le readbuiltin évite ce problème en lisant un octet à la fois, ce qui lui permet de s'arrêter dès qu'il trouve la première nouvelle ligne, mais est très lent (bien sûr, cela n'a pas d'importance si vous ne lisez que quelques centaines d'octets).

Gilles 'SO- arrête d'être méchant'
la source
2
Il n'y a qu'un seul coprocessus à la fois dans zsh? Je suis surpris - ce n'est pas souvent que je vois un endroit où bash est plus flexible. :)
Charles Duffy
@CharlesDuffy, vous pouvez avoir plus d'un coprocessus dans zsh . les coprocessus n'ont été ajoutés que très récemment bash, voir la section bash sur ce lien.
Stéphane Chazelas
@ StéphaneChazelas Comment interagissez-vous avec plus d'un coprocessus dans zsh? ( coproccoprocesses, je veux dire, pas vides)
Gilles 'SO- arrête d'être méchant'
De la même manière qu'avec ksh comme expliqué sur ce lien. coproc cmd1; exec 3>&p 4<&p; coproc cmd2 3>&- 4<&-...
Stéphane Chazelas
1

Je pense que je le ferais avec un signal quelconque.

(   trap   "read zero </tmp/ints" PIPE
    while  kill -s PIPE -0
    do     i=$zero
           while echo $((i++))
           do :; done 2>/dev/null >/tmp/ints
    done
)&

Cela fonctionne pour moi, de toute façon.


$ echo  15 >/tmp/ints; head -n 5 </tmp/ints
15
16
17
18
19
$ echo  75 >/tmp/ints; head -n 5 </tmp/ints
75
76
77
78
79

Sur une note seulement légèrement liée, voici quelque chose de bizarre que j'ai découvert l'autre jour:

mkdir nums; cd nums
for n in 0 1 2 3 4 5 6 7
do  ln -s ./ "$n"; done
echo [0-3]/*/*

0/0/0 0/0/1 0/0/2 0/0/3 0/0/4 0/0/5 0/0/6 0/0/7 0/1/0 0/1/1 0/1/2 0/1/3 0/1/4 0/1/5 0/1/6 0/1/7 0/2/0 0/2/1 0/2/2 0/2/3 0/2/4 0/2/5 0/2/6 0/2/7 0/3/0 0/3/1 0/3/2 0/3/3 0/3/4 0/3/5 0/3/6 0/3/7 0/4/0 0/4/1 0/4/2 0/4/3 0/4/4 0/4/5 0/4/6 0/4/7 0/5/0 0/5/1 0/5/2 0/5/3 0/5/4 0/5/5 0/5/6 0/5/7 0/6/0 0/6/1 0/6/2 0/6/3 0/6/4 0/6/5 0/6/6 0/6/7 0/7/0 0/7/1 0/7/2 0/7/3 0/7/4 0/7/5 0/7/6 0/7/7 1/0/0 1/0/1 1/0/2 1/0/3 1/0/4 1/0/5 1/0/6 1/0/7 1/1/0 1/1/1 1/1/2 1/1/3 1/1/4 1/1/5 1/1/6 1/1/7 1/2/0 1/2/1 1/2/2 1/2/3 1/2/4 1/2/5 1/2/6 1/2/7 1/3/0 1/3/1 1/3/2 1/3/3 1/3/4 1/3/5 1/3/6 1/3/7 1/4/0 1/4/1 1/4/2 1/4/3 1/4/4 1/4/5 1/4/6 1/4/7 1/5/0 1/5/1 1/5/2 1/5/3 1/5/4 1/5/5 1/5/6 1/5/7 1/6/0 1/6/1 1/6/2 1/6/3 1/6/4 1/6/5 1/6/6 1/6/7 1/7/0 1/7/1 1/7/2 1/7/3 1/7/4 1/7/5 1/7/6 1/7/7 2/0/0 2/0/1 2/0/2 2/0/3 2/0/4 2/0/5 2/0/6 2/0/7 2/1/0 2/1/1 2/1/2 2/1/3 2/1/4 2/1/5 2/1/6 2/1/7 2/2/0 2/2/1 2/2/2 2/2/3 2/2/4 2/2/5 2/2/6 2/2/7 2/3/0 2/3/1 2/3/2 2/3/3 2/3/4 2/3/5 2/3/6 2/3/7 2/4/0 2/4/1 2/4/2 2/4/3 2/4/4 2/4/5 2/4/6 2/4/7 2/5/0 2/5/1 2/5/2 2/5/3 2/5/4 2/5/5 2/5/6 2/5/7 2/6/0 2/6/1 2/6/2 2/6/3 2/6/4 2/6/5 2/6/6 2/6/7 2/7/0 2/7/1 2/7/2 2/7/3 2/7/4 2/7/5 2/7/6 2/7/7 3/0/0 3/0/1 3/0/2 3/0/3 3/0/4 3/0/5 3/0/6 3/0/7 3/1/0 3/1/1 3/1/2 3/1/3 3/1/4 3/1/5 3/1/6 3/1/7 3/2/0 3/2/1 3/2/2 3/2/3 3/2/4 3/2/5 3/2/6 3/2/7 3/3/0 3/3/1 3/3/2 3/3/3 3/3/4 3/3/5 3/3/6 3/3/7 3/4/0 3/4/1 3/4/2 3/4/3 3/4/4 3/4/5 3/4/6 3/4/7 3/5/0 3/5/1 3/5/2 3/5/3 3/5/4 3/5/5 3/5/6 3/5/7 3/6/0 3/6/1 3/6/2 3/6/3 3/6/4 3/6/5 3/6/6 3/6/7 3/7/0 3/7/1 3/7/2 3/7/3 3/7/4 3/7/5 3/7/6 3/7/7

Cela devient de plus en plus étrange:

rm *
for a in  a b c d e f g h \
          i j k l m n o p \
          q r s t u v x y z
do 
    ln -s ./ "$a"
done
for a in *
do  echo "$a"/["$a"-z]
done

a/a a/b a/c a/d a/e a/f a/g a/h a/i a/j a/k a/l a/m a/n a/o a/p a/q a/r a/s a/t a/u a/v a/x a/y a/z
b/b b/c b/d b/e b/f b/g b/h b/i b/j b/k b/l b/m b/n b/o b/p b/q b/r b/s b/t b/u b/v b/x b/y b/z
c/c c/d c/e c/f c/g c/h c/i c/j c/k c/l c/m c/n c/o c/p c/q c/r c/s c/t c/u c/v c/x c/y c/z
d/d d/e d/f d/g d/h d/i d/j d/k d/l d/m d/n d/o d/p d/q d/r d/s d/t d/u d/v d/x d/y d/z
e/e e/f e/g e/h e/i e/j e/k e/l e/m e/n e/o e/p e/q e/r e/s e/t e/u e/v e/x e/y e/z
f/f f/g f/h f/i f/j f/k f/l f/m f/n f/o f/p f/q f/r f/s f/t f/u f/v f/x f/y f/z
g/g g/h g/i g/j g/k g/l g/m g/n g/o g/p g/q g/r g/s g/t g/u g/v g/x g/y g/z
h/h h/i h/j h/k h/l h/m h/n h/o h/p h/q h/r h/s h/t h/u h/v h/x h/y h/z
i/i i/j i/k i/l i/m i/n i/o i/p i/q i/r i/s i/t i/u i/v i/x i/y i/z
j/j j/k j/l j/m j/n j/o j/p j/q j/r j/s j/t j/u j/v j/x j/y j/z
k/k k/l k/m k/n k/o k/p k/q k/r k/s k/t k/u k/v k/x k/y k/z
l/l l/m l/n l/o l/p l/q l/r l/s l/t l/u l/v l/x l/y l/z
m/m m/n m/o m/p m/q m/r m/s m/t m/u m/v m/x m/y m/z
n/n n/o n/p n/q n/r n/s n/t n/u n/v n/x n/y n/z
o/o o/p o/q o/r o/s o/t o/u o/v o/x o/y o/z
p/p p/q p/r p/s p/t p/u p/v p/x p/y p/z
q/q q/r q/s q/t q/u q/v q/x q/y q/z
r/r r/s r/t r/u r/v r/x r/y r/z
s/s s/t s/u s/v s/x s/y s/z
t/t t/u t/v t/x t/y t/z
u/u u/v u/x u/y u/z
v/v v/x v/y v/z
x/x x/y x/z
y/y y/z
z/z
mikeserv
la source
Qu'est-ce qui est bizarre ?
Stéphane Chazelas
@ StéphaneChazelas - il semblait tout simplement étrange que les liens s'auto-récurrent. Et si facilement. Je pensais que c'était bizarre. Et super. Je pensais également qu'il devait y avoir une sorte de limite de récursivité en profondeur - semble que le shell déclencherait cela - ou doit-il réellement faire 40 liens dans un seul chemin?
mikeserv
@ StéphaneChazelas - C'est bien. Mais peut-être que bashle comportement a changé? Je pense que la déclaration de pwdne pas vérifier et de ne se référer qu'à $PWDest incorrecte. mkdir /tmp/dir; cd $_; PS4='$OLDPWD, $PWD + '; set -x; OLDPWD=$OLDPWD PWD=$PWD command eval ' cd ..; cd ..; cd ~; pwd'; pwd; cd .; pwdpourrait vous montrer ce que je veux dire. C'est un problème qui m'a mis sur écoute avec cette ns()chose.
mikeserv