esyr: (Default)
[personal profile] esyr
Вероятно, многие знают, что при помощи shell built-in read можно построчно читать всякое; особенно это актуально при считывании списка файлов, которые потенциально могут содержать пробелы (например, в выдаче ls или find) или чего-то подобного. Обычно это делается так:

Построчное чтение из файла:
while read line
do
  # code
done < file

Построчное чтение результата выполнения команды (вариант с перенаправлением):
ls | while read line
do
  # code
done

Вроде бы всё хорошо. Проблемы могут начаться в случае, когда во время чтения нужно изменять значения переменных. А именно, последний (и, кстати, наиболее часто используемый в случае, когда нужно обработать вывод команды, а не читать из файла) вариант приводит к созданию дочернего процесса (pipe же!), что приводит к тому, что все изменения переменных внутри тела цикла выполняются в подпроцессе и, как следствие, на процессе, в котором выполняется скрипт, не попадают. Это можно легко увидеть на следующем примере:
i=0; seq 1 3| while read line; do i=$(( $i + 1 )); done; echo $i
В bash и dash в результате будет выдан 0, в zsh и ksh (которые исполняют последний элемент пайплайна в текущем процессе в случае, если это shell built-in) — 3.
Обойти эту проблему можно несколькими способами. Один из них — использовать heredoc:
while read a b
do
  i=$(( $i + 1 ))
done <<EOF
`seq 1 3 | sed 's/^/1 /'`
EOF
echo $i
При этом, как можно видеть из примера выше, можно использовать произвольный пайплайн.
Очевидный недостаток этого решения — прежде, чем результат выполнения будет передан в цикл, он полностью будет получен. Это можно попытаться обойти, например, путём создания временного FIFO-файла:
fifofile=`mktemp`
rm "$fifofile"
mkfifo "$fifofile"

ls | sed 's/^/1 /' > "$fifofile" & # random command which output we need to parse
while read a b
do
  i=$(( $i + 1 ))
done < "$fifofile"
echo $i

rm "$fifofile"
Очевидно, что в данном случае строчка rm "$fifofile" будет выполнена после завершения цикла, что, по идее (так как read ждёт закрытия потока ввода), произойдёт только после завершения процесса, пишущего в FIFO и считывания из него всех данных.
Но всё же ощущается неаккуратность в виде наличия временных файлов. Тем более, что упражнение вида mktemp—rm—mkfifo теоретически может породить рейс. Можно попробовать соорудить конструкцию с созданием дескрипторов (like, exec 3<&0>&1):
exec 3<&0>&1

ls | sed 's/^/1 /' >&3 & # random command which output we need to parse
while read -u 3 a b
do
  i=$(( $i + 1 ))
done
echo $i

exec 3<&->&-

И, казалось бы, счастье есть: мы можем запихать произвольный пайплайн из процессов в этот дескриптор, а потом читать из него (или с помощью специального ключика у read, как в примере выше, или же просто <&3. Осталась одна маленькая проблема: если попытаться прибить скрипт до окончания его работы, то это ему ничуть не помешает. Посему, нужно повесить trap, который убивает дочерний процесс и завершает работу:
trap 'kill -9 $cpid; exit' TERM INT

exec 3<&0>&1

ls | sed 's/^/1 /' >&3 & # random command which output we need to parse
cpid=$!
while read -u 3 a b
do
  i=$(( $i + 1 ))
done
echo $i

exec 3<&->&-

Более аккуратного варианта как-то в голову не пришло. Если кто знает — поделитесь.

UPD. Таки приведённый вариант с дескрипторами работает только в zsh (не в dash). Буду думать, как его таки заимплементить.

Date: 2010-03-15 09:45 am (UTC)
From: [identity profile] http://users.livejournal.com/_winnie/
баш клёвый и простой, пока стыкуешь готовые программы :)

в питоне есть i += 1, но вместо него используют zip/enumerate.
Можно попробвать так же. Попробовал paste <(seq) <(ls), но к сожалению paste печатает наиболее длинный столбец а не наиболее короткий.

Запихнул вывод ls как параметр seq. Потерял параллельность выполнения.
(paste <(seq 1 $(ls |wc -l)) <(ls))

Попытался после вывода распечатать количество строк (конечный echo $i). Опять фейл,
(paste <(seq 1 $(ls |wc -l)) <(ls)) | tee >(wc -l)
tee: /dev/fd/63: Bad file descriptor


ИМХО, когда надо знать инструмент очень тонко - значит используется не тот инструмент.
В контексте баша - можно юзать awk или его современного конкурента, python :)
ls | awk '{print NR, $0} END {print "counter at the end:", NR}'

Date: 2010-03-15 10:27 am (UTC)
From: [identity profile] esyr.livejournal.com
Ну, собственно, я awk и использую для text processing в других местах. Шелл как раз и нужен, чтобы что-то повызывать на основе внимательного рассматривания строчек (в этом качестве с ним ни питон, ни awk не сравнится).

Date: 2010-03-18 09:39 pm (UTC)
From: [identity profile] http://users.livejournal.com/_gq_/
если сабшелл должен изменить только одну переменную - то в бэктики его. Еще можно по разному играться с потоками, могу показать код из продакшна.

Date: 2011-09-18 06:03 pm (UTC)
From: [identity profile] hadsok.livejournal.com
Это интересно. Подскажите, где я могу об этом прочитать?

Date: 2012-02-16 06:47 pm (UTC)
From: [identity profile] mlawskyirufy.livejournal.com
Пост неплохой, закину блог в закладки.Image (http://zimnyayaobuv.ru/)Image (http://zimnyaya-obuv.ru/)

Profile

esyr: (Default)
esyr

October 2010

S M T W T F S
     12
3456789
10111213141516
17181920212223
24252627282930
31      

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jul. 20th, 2017 04:26 pm
Powered by Dreamwidth Studios