Как реализовать «генераторы» типа $ RANDOM?

10

Специальная переменная $RANDOMимеет новое значение каждый раз, когда к ней обращаются. В этом отношении он напоминает объекты-генераторы, встречающиеся в некоторых языках.

Есть ли способ реализовать что-то подобное в zsh?

Я пытался сделать это с именованными каналами, но я не нашел способа извлекать элементы из fifo контролируемым образом, не убивая процесс «генератора». Например:

% 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=$(( ...

Есть ли другой способ реализовать такой объект типа генератора в Zsh?


РЕДАКТИРОВАТЬ: Это не работает:

#!/usr/bin/env zsh

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

Если я помещу вышеупомянутое в сценарий и запусту его, на выходе редко будет ожидаемая строка

1

скорее он обычно состоит из нескольких целых чисел; например

1
2
3
4
5

Количество производимых линий варьируется от одного цикла к другому.

РЕДАКТИРОВАТЬ 2: Как указал Джиммий, переход echoк /bin/echoрешению проблемы.

KJo
источник

Ответы:

10

ksh93имеет дисциплины, которые обычно используются для такого рода вещей. С помощью zshвы можете захватить функцию динамического именованного каталога :

Определите, например:

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

И тогда вы можете использовать, ~[incr]чтобы получить приращение $incrкаждый раз:

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

Ваш подход терпит неудачу, потому что in head -1 /tmp/ints, head открывает fifo, читает полный буфер, печатает одну строку, а затем закрывает ее . После закрытия пишущий конец видит сломанную трубу.

Вместо этого вы можете сделать:

$ 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

Там мы оставляем конец чтения открытым на fd 3 и readчитаем по одному байту за раз, а не полный буфер, чтобы быть уверенным, что читаем ровно одну строку (до символа новой строки).

Или вы могли бы сделать:

$ 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

На этот раз мы создаем канал для каждого значения. Это позволяет возвращать данные, содержащие произвольное количество строк.

Тем не менее, в этом случае, как только catоткроется fifo, echoцикл «и» разблокируется, так что больше времени echoможно будет запустить к тому времени, catкогда содержимое прочитает и закроет канал (что приведет к следующему echoсозданию нового канала).

echoОбходной путь может заключаться в добавлении некоторой задержки, например, путем запуска внешнего, как предложено @jimmij, или добавления некоторых sleep, но это все равно не будет очень надежным, или вы можете воссоздать именованный канал после каждого echo:

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

Это по-прежнему оставляет короткие окна, в которых канал не существует (между unlink()выполненным rmи mknod()выполненным mkfifo), что приводит catк сбою, и очень короткие окна, в которых был создан экземпляр канала, но ни один процесс больше никогда не запишет в него (между write()и close()выполнено путем echo) catничего не возвращая, и короткие окна, где именованный канал все еще существует, но ничто никогда не откроет его для записи (между close()выполненным echoи unlink()выполненным rm), где catбудет висеть.

Вы можете удалить некоторые из этих окон , сделав это следующим образом:

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
) &

Таким образом, единственная проблема заключается в том, что вы запускаете несколько cat одновременно (все они открывают fifo до того, как наш цикл записи готов открыть его для записи), и в этом случае они будут делиться echoвыводом.

Я также не рекомендовал бы создавать фиксированные имена, читаемые во всем мире данные (или любой другой файл в этом отношении) в таких каталогах, которые доступны для записи, например, /tmpесли это не служба, доступная для всех пользователей в системе.

Стефан Шазелас
источник
Спасибо. Если я не ошибся, последний рецепт, который вы дадите, не всегда работает. Смотрите мой РЕДАКТИРОВАТЬ.
kjo
1
@kjo Попробуйте command echoили /bin/echoвместо встроенного echo. Кроме того - вы можете сделать эту команду немного короче: repeat 999 /bin/echo $((++incr)) > /tmp/int &.
Джимми
1
@kjo, см. редактировать.
Стефан Шазелас
4

Если вы хотите выполнять код всякий раз, когда читается значение переменной, вы не можете сделать это внутри самого zsh. RANDOMПеременной (как и другие подобные специальные переменные) жестко закодировано в исходном коде ЗШ. Однако вы можете определить аналогичные специальные переменные, написав модуль на C. Многие стандартные модули определяют специальные переменные.

Вы можете использовать сопроцесс для создания генератора.

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

Однако это довольно ограничено, потому что вы можете иметь только один сопроцесс. Другим способом постепенного получения выходных данных процесса является перенаправление из подстановки процесса .

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

Обратите внимание, что head -1это не работает, потому что он читает весь буфер, печатает то, что ему нравится, и выходит. Данные, которые были прочитаны из канала, остаются прочитанными; это внутреннее свойство каналов (вы не можете вставить данные обратно). readВстроенная избегает этого вопроса, прочитав один байт в то время, что позволяет ему остановиться , как только он находит первый символ новой строки , но очень медленно (конечно , что не имеет значения , если вы просто читаете несколько сотен байт).

Жиль "ТАК - перестань быть злым"
источник
2
Есть только один сопроцесс за раз в Zsh? Я удивлен - не часто я вижу место, где bash более гибок. :)
Чарльз Даффи
@CharlesDuffy, у вас может быть больше чем один сопроцесс в zsh . сопроцессы были добавлены совсем недавно bash, см. раздел bash по этой ссылке.
Стефан Шазелас
@ StéphaneChazelas Как вы взаимодействуете с несколькими процессами в Zsh? ( coprocсопроцессы, я имею в виду, а не зпты)
Жиль "
Как и в случае с ksh, как описано в этой ссылке. coproc cmd1; exec 3>&p 4<&p; coproc cmd2 3>&- 4<&-...
Стефан Шазелас
1

Я думаю, что я сделал бы это с сигналом некоторого вида.

(   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
)&

Во всяком случае, у меня это работает.


$ 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

На только немного связанной ноте вот что странное я обнаружил на днях:

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

Это становится еще более странным:

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
источник
Что в этом странного ?
Стефан Шазелас
@ StéphaneChazelas - просто казалось странным, что ссылки будут самовосстанавливающимися. И так легко. Я думал, что это было странно. И круто. Я также подумал, что должен был быть некоторый предел глубины рекурсии - кажется, что оболочка вызовет это - или ему действительно нужно сделать 40 ссылок в одном пути?
mikeserv
@ StéphaneChazelas - Это хорошо. Но, может быть bash, поведение изменилось? Я считаю неверным утверждение о том, что pwdне нужно проверять и ссылаться только на $PWD. mkdir /tmp/dir; cd $_; PS4='$OLDPWD, $PWD + '; set -x; OLDPWD=$OLDPWD PWD=$PWD command eval ' cd ..; cd ..; cd ~; pwd'; pwd; cd .; pwdможет показать вам, что я имею в виду Это проблема, которая беспокоила меня с этой ns()вещью.
mikeserv