Вероятно, многие знают, что при помощи shell built-in read можно построчно читать всякое; особенно это актуально при считывании списка файлов, которые потенциально могут содержать пробелы (например, в выдаче ls или find) или чего-то подобного. Обычно это делается так:
Построчное чтение из файла:
Построчное чтение результата выполнения команды (вариант с перенаправлением):
Вроде бы всё хорошо. Проблемы могут начаться в случае, когда во время чтения нужно изменять значения переменных. А именно, последний (и, кстати, наиболее часто используемый в случае, когда нужно обработать вывод команды, а не читать из файла) вариант приводит к созданию дочернего процесса (pipe же!), что приводит к тому, что все изменения переменных внутри тела цикла выполняются в подпроцессе и, как следствие, на процессе, в котором выполняется скрипт, не попадают. Это можно легко увидеть на следующем примере:
Обойти эту проблему можно несколькими способами. Один из них — использовать heredoc:
Очевидный недостаток этого решения — прежде, чем результат выполнения будет передан в цикл, он полностью будет получен. Это можно попытаться обойти, например, путём создания временного FIFO-файла:
Но всё же ощущается неаккуратность в виде наличия временных файлов. Тем более, что упражнение вида mktemp—rm—mkfifo теоретически может породить рейс. Можно попробовать соорудить конструкцию с созданием дескрипторов (like, exec 3<&0>&1):
И, казалось бы, счастье есть: мы можем запихать произвольный пайплайн из процессов в этот дескриптор, а потом читать из него (или с помощью специального ключика у read, как в примере выше, или же просто <&3. Осталась одна маленькая проблема: если попытаться прибить скрипт до окончания его работы, то это ему ничуть не помешает. Посему, нужно повесить trap, который убивает дочерний процесс и завершает работу:
Более аккуратного варианта как-то в голову не пришло. Если кто знает — поделитесь.
UPD. Таки приведённый вариант с дескрипторами работает только в zsh (не в dash). Буду думать, как его таки заимплементить.
Построчное чтение из файла:
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). Буду думать, как его таки заимплементить.