10.1. Циклы


Цикл — это блок команд, который исполняется многократно до тех пор, пока не будет выполнено условие выхода из цикла.

циклы for

for (in)

Это одна из основных разновидностей циклов. И она значительно отличается от аналога в языке C.

for arg in [list]
do
 команда(ы)...
done



Note

На каждом проходе цикла, переменная-аргумент цикла arg последовательно, одно за другим, принимает значения из списка list.

  1. for arg in "$var1" "$var2" "$var3" ... "$varN"
  2. # На первом проходе, $arg = $var1
  3. # На втором проходе, $arg = $var2
  4. # На третьем проходе, $arg = $var3
  5. # ...
  6. # На N-ном проходе, $arg = $varN
  7. # Элементы списка заключены в кавычки для того, чтобы предотвратить возможное разбиение их на отдельные аргументы (слова).


Элементы списка могут включать в себя шаблонные символы.

Есл ключевое слово do находится в одной строке со словом for, то после списка аргументов (перед do) необходимо ставить точку с запятой.

for arg in [list] ; do



Пример 10-1. Простой цикл for

  1. #!/bin/bash
  2. # Список планет.
  3. for planet in Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон
  4. do
  5.   echo $planet
  6. done
  7. echo
  8. # Если 'список аргументов' заключить в кавычки, то он будет восприниматься как единственный аргумент .
  9. for planet in "Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон"
  10. do
  11.   echo $planet
  12. done
  13. exit 0
Note

Каждый из элементов [списка] может содержать несколько аргументов. Это бывает полезным при обработке групп параметров. В этом случае, для принудительного разбора каждого из аргументов в списке, необходимо использовать инструкцию set (см. Пример 11-14).

Пример 10-2. Цикл for с двумя параметрами в каждом из элементов списка

  1. #!/bin/bash
  2. # Список планет.
  3. # Имя кажой планеты ассоциировано с расстоянием от планеты до Солнца (млн. миль).
  4. for planet in "Меркурий 36" "Венера 67" "Земля 93"  "Марс 142" "Юпитер 483"
  5. do
  6.   set$planet  # Разбиение переменной "planet" на множество аргументов (позиционных параметров).
  7.   # Конструкция "--" предохраняет от неожиданностей, если $planet "пуста" или начинается с символа "-".
  8.   # Если каждый из аргументов потребуется сохранить, поскольку на следующем проходе они будут "забиты" новыми значениями,
  9.   # То можно поместить их в массив,
  10.   #        original_params=("$@")
  11.   echo "$1      в $2,000,000 миль от Солнца"
  12.   #----две табуляции---к параметру $2 добавлены нули
  13. done
  14. # (Спасибо S.C., за разъяснения.)
  15. exit 0

В качестве списка, в цикле for, можно использовать переменную.

Пример 10-3. Fileinfo: обработка списка файлов, находящегося в переменной

  1. #!/bin/bash
  2. # fileinfo.sh
  3. FILES="/usr/sbin/privatepw
  4. /usr/sbin/pwck
  5. /usr/sbin/go500gw
  6. /usr/bin/fakefile
  7. /sbin/mkreiserfs
  8. /sbin/ypbind"     # Список интересующих нас файлов.
  9.                   # В список добавлен фиктивный файл /usr/bin/fakefile.
  10. echo
  11. for file in $FILES
  12. do
  13.   if [ ! -e "$file" ]       # Проверка наличия файла.
  14.   then
  15.     echo "Файл $file не найден."; echo
  16.     continue                # Переход к следующей итерации.
  17.   fi
  18.   ls -l $file | awk '{ print $8 "         размер: " $5 }'  # Печать 2 полей.
  19.   whatis `basename $file`   # Информация о файле.
  20.   echo
  21. done  
  22. exit 0

В [списке] цикла for могут быть использованы имена файлов, которые в свою очередь могут содержать символы-шаблоны.

Пример 10-4. Обработка списка файлов в цикле for

  1. #!/bin/bash
  2. # list-glob.sh: Создание список файлов в цикле for с использованием
  3. # операции подстановки имен файлов ("globbing").
  4. echo
  5. for file in *
  6. do
  7.   ls -l "$file"  # Список всех файлов в $PWD (текущем каталоге).
  8.   # Напоминаю, что символу "*" соответствует любое имя файла,
  9.   # однако, в операциях подстановки имен файлов ("globbing"),
  10.   # имеются исключения — имена файлов, начинающиеся с точки.
  11.   # Если в каталоге нет ни одного файла, соответствующего шаблону,
  12.   # то за имя файла принимается сам шаблон.
  13.   # Чтобы избежать этого, используйте ключ nullglob
  14.   # (shopt -s nullglob).
  15.   # Спасибо S.C.
  16. done
  17. echo; echo
  18. for file in [jx]*
  19. do
  20.   rm -f $file    # Удаление файлов, начинающихся с "j" или "x" в $PWD.
  21.   echo "Удален файл \"$file\"".
  22. done
  23. echo
  24. exit 0

Если [список] в цикле for не задан, то в качестве оного используется переменная $@ — список аргументов командной строки. Оень остроумно эта особенность проиллюстрирована в Пример A-18.

Пример 10-5. Цикл for без списка аргументов

  1. #!/bin/bash
  2. # Попробуйте вызвать этот сценарий с аргументами и без них и посмотреть на результаты.
  3. for a
  4. do
  5.  echo -n "$a "
  6. done
  7. #  Список аргументов не задан, поэтому цикл работает с переменной '$@'
  8. #+ (список аргументов командной строки, включая пробельные символы).
  9. echo
  10. exit 0

При создании списка аргументов, в цикле for допускается пользоваться подстановкой команд. См. Пример 12-42, Пример 10-10 и Пример 12-36.

Пример 10-6. Создание списка аргументов в цикле for с помощью операции подстановки команд

  1. #!/bin/bash
  2. # Цикл for со [списком], созданным с помощью подстановки команд.
  3. NUMBERS="9 7 3 8 37.53"
  4. for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
  5. do
  6.   echo -n "$number "
  7. done
  8. echo
  9. exit 0

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

Пример 10-7. grep для бинарных файлов

  1. #!/bin/bash
  2. # bin-grep.sh: Поиск строк в двоичных файлах.
  3. # замена "grep" для бинарных файлов.
  4. # Аналогично команде "grep -a"
  5. E_BADARGS=65
  6. E_NOFILE=66
  7. if [ $# -ne 2 ]
  8. then
  9.   echo "Порядок использования: `basename $0` string filename"
  10.   exit $E_BADARGS
  11. fi
  12. if [ ! -f "$2" ]
  13. then
  14.   echo "Файл \"$2\" не найден."
  15.   exit $E_NOFILE
  16. fi
  17. for word in $( strings "$2" | grep "$1" )
  18. # Инструкция "strings" возвращает список строк в двоичных файлах.
  19. # Который затем передается по конвейеру команде "grep", для выполнения поиска.
  20. do
  21.   echo $word
  22. done
  23. # Как указывает S.C., вышепрведенное объявление цикла for может быть упрощено
  24. #    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'
  25. # Попробуйте что нибудь подобное:  "./bin-grep.sh mem /bin/ls"
  26. exit 0

Еще один пример.

Пример 10-8. Список всех пользователей системы

  1. #!/bin/bash
  2. # userlist.sh
  3. PASSWORD_FILE=/etc/passwd
  4. n=1           # Число пользователей
  5. for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
  6. # Разделитель полей = :  ^^^^^^
  7. # Вывод первого поля              ^^^^^^^^
  8. # Данные берутся из файла паролей            ^^^^^^^^^^^^^^^^^
  9. do
  10.   echo "Пользователь #$n = $name"
  11.   let "n += 1"
  12. done
  13. # Пользователь #1 = root
  14. # Пользователь #2 = bin
  15. # Пользователь #3 = daemon
  16. # ...
  17. # Пользователь #30 = bozo
  18. exit 0

И заключительный пример использования подстановки команд при создании [списка].

Пример 10-9. Проверка авторства всех бинарных файлов в текущем каталоге

  1. #!/bin/bash
  2. # findstring.sh:
  3. # Поиск заданной строки в двоичном файле.
  4. directory=/usr/local/bin/
  5. fstring="Free Software Foundation"  # Поиск файлов от FSF.
  6. for file in $( find $directory -type f -name '*' | sort )
  7. do
  8.   strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  9.   #  Команде "sed" передается выражение (ключ -e),
  10.   #+ для того, чтобы изменить обычный разделитель "/" строки поиска и строки замены
  11.   #+ поскольку "/" - один из отфильтровываемых символов.
  12.   #  Использование такого символа порождает сообщение об ошибке (попробуйте).
  13. done
  14. exit 0
  15. #  Упражнение:
  16. #  ---------------
  17. #  Измените сценарий таким образом, чтобы он брал
  18. #+ $directory и $fstring из командной строки.

Результат работы цикла for может передаваться другим командам по конвейеру.

Пример 10-10. Список символических ссылок в каталоге

  1. #!/bin/bash
  2. # symlinks.sh: Список символических ссылок в каталоге.
  3. directory=${1-`pwd`}
  4. #  По-умолчанию в текущем каталоге,
  5. #  Блок кода, который выполняет аналогичные действия.
  6. # ----------------------------------------------------------
  7. # ARGS=1                 # Ожидается один аргумент командной строки.
  8. #
  9. # if [ $# -ne "$ARGS" ]  # Если каталог поиска не задан...
  10. # then
  11. #   directory=`pwd`      # текущий каталог
  12. # else
  13. #   directory=$1
  14. # fi
  15. # ----------------------------------------------------------
  16. echo "символические ссылки в каталоге \"$directory\""
  17. for file in "$( find $directory -type l )"   # -type l = символические ссылки
  18. do
  19.   echo "$file"
  20. done | sort             # В противном случае получится неотсортированный список.
  21. #  Как отмечает Dominik 'Aeneas' Schnitzer,
  22. #+ в случае отсутствия кавычек для $( find $directory -type l )
  23. #+ сценарий "подавится" именами файлов, содержащими пробелы.
  24. exit 0

Вывод цикла может быть перенаправлен со stdout в файл, ниже приводится немного модифицированный вариант предыдущего примера, демонстрирующий эту возможность.

Пример 10-11. Список символических ссылок в каталоге, сохраняемый в файле

  1. #!/bin/bash
  2. # symlinks.sh: Список символических ссылок в каталоге.
  3. OUTFILE=symlinks.list                         # файл со списком
  4. directory=${1-`pwd`}
  5. #  По-умолчанию — текущий каталог,
  6. echo "символические ссылки в каталоге \"$directory\"" > "$OUTFILE"
  7. echo "---------------------------" >> "$OUTFILE"
  8. for file in "$( find $directory -type l )"    # -type l = символические ссылки
  9. do
  10.   echo "$file"
  11. done | sort >> "$OUTFILE"                     # перенаправление вывода
  12. #           ^^^^^^^^^^^^^                       в файл.
  13. exit 0

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

Пример 10-12. C-подобный синтаксис оператора цикла for

  1. #!/bin/bash
  2. # Два вапианта оформления цикла.
  3. echo
  4. # Стандартный синтаксис.
  5. for a in 1 2 3 4 5 6 7 8 9 10
  6. do
  7.   echo -n "$a "
  8. done
  9. echo; echo
  10. # +==========================================+
  11. # А теперь C-подобный синтаксис.
  12. LIMIT=10
  13. for ((a=1; a <= LIMIT ; a++))  # Двойные круглые скобки и "LIMIT" без "$".
  14. do
  15.   echo -n "$a "
  16. done                           # Конструкция заимствована из 'ksh93'.
  17. echo; echo
  18. # +=========================================================================+
  19. # Попробуем и C-шный оператор "запятая".
  20. for ((a=1, b=1; a <= LIMIT ; a++, b++))  # Запятая разделяет две операции, которые выполняются совместно.
  21. do
  22.   echo -n "$a-$b "
  23. done
  24. echo; echo
  25. exit 0

См. так же Пример 25-15, Пример 25-16 и Пример A-7.

---

А сейчас пример сценария, который может найти "реальное" применение.

Пример 10-13. Работа с командой efax в пакетном режиме

  1. #!/bin/bash
  2. EXPECTED_ARGS=2
  3. E_BADARGS=65
  4. if [ $# -ne $EXPECTED_ARGS ]
  5. # Проверка наличия аргументов командной строки.
  6. then
  7.    echo "Порядок использования: `basename $0` phone# text-file"
  8.    exit $E_BADARGS
  9. fi
  10. if [ ! -f "$2" ]
  11. then
  12.   echo "Файл $2 не является текстовым файлом"
  13.   exit $E_BADARGS
  14. fi
  15. fax make $2              # Создать fax-файлы из текстовых файлов.
  16. for file in $(ls $2.0*)  # Все файлы, получившиеся в результате преобразования.
  17.                          # Используется шаблонный символ в списке.
  18. do
  19.   fil="$fil $file"
  20. done
  21. efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # отправить.
  22. # Как указывает S.C., в цикл for может быть вставлена сама команда отправки в виде:
  23. #    efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
  24. # но это не так поучительно [;-)].
  25. exit 0
while

Оператор while проверяет условие перед началом каждой итерации и если условие истинно (если код возврата равен 0), то управление передается в тело цикла. В отличие от циклов for, циклы while используются в тех случаях, когда количество итераций заранее не известно.

while [condition]
do
 command...
done



Как и в случае с циклами for/in, при размещении ключевого слова do в одной строке с объявлением цикла, необходимо вставлять символ ";" перед do.

while [condition] ; do



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

Пример 10-14. Простой цикл while

  1. #!/bin/bash
  2. var0=0
  3. LIMIT=10
  4. while [ "$var0" -lt "$LIMIT" ]
  5. do
  6.   echo -n "$var0 "        # -n подавляет перевод строки.
  7.   var0=`expr $var0 + 1`   # допускается var0=$(($var0+1)).
  8. done
  9. echo
  10. exit 0

Пример 10-15. Другой пример цикла while

  1. #!/bin/bash
  2. echo
  3. while [ "$var1" != "end" ]     # возможна замена на while test "$var1" != "end"
  4. do
  5.   echo "Введите значение переменной #1 (end - выход) "
  6.   read var1                    # Конструкция 'read $var1' недопустима (почему?).
  7.   echo "переменная #1 = $var1" # кавычки обязательны, потому что имеется символ "#".
  8.   # Если введено слово 'end', то оно тоже выводится на экран.
  9.   # потому, что проверка переменной выполняется в начале итерации (перед вводом).
  10.   echo
  11. done  
  12. exit 0

Оператор while может иметь несколько условий. Но только последнее из них определяет возможность продолжения цикла. В этом случае синтаксис оператора цикла должен быть несколько иным.

Пример 10-16. Цикл while с несколькими условиями

  1. #!/bin/bash
  2. var1=unset
  3. previous=$var1
  4. while echo "предыдущее значение = $previous"
  5.       echo
  6.       previous=$var1     # запомнить предыдущее значение
  7.       [ "$var1" != end ]
  8.       # В операторе "while" присутствуют 4 условия, но только последнее управляет циклом.
  9.       # *последнее* условие - единственное, которое вычисляется.
  10. do
  11. echo "Введите значение переменной #1 (end - выход) "
  12.   read var1
  13.   echo "текущее значение = $var1"
  14. done
  15. # попробуйте самостоятельно разобраться в сценарии works.
  16. exit 0

Как и в случае с for, цикл while может быть записан в C-подобной нотации, с использованием двойных круглых скобок (см. так же Пример 9-29).

Пример 10-17. C-подобный синтаксис оформления цикла while

  1. #!/bin/bash
  2. # wh-loopc.sh: Цикл перебора от 1 до 10.
  3. LIMIT=10
  4. a=1
  5. while [ "$a" -le $LIMIT ]
  6. do
  7.   echo -n "$a "
  8.   let "a+=1"
  9. done           # Пока ничего особенного.
  10. echo; echo
  11. # +=================================================================+
  12. # А теперь оформим в стиле языка C.
  13. ((a = 1))      # a=1
  14. # Двойные скобки допускают наличие лишних пробелов в выражениях.
  15. while (( a <= LIMIT ))   # В двойных скобках символ "$" перед переменными опускается.
  16. do
  17.   echo -n "$a "
  18.   ((a += 1))   # let "a+=1"
  19.   # Двойные скобки позволяют наращивание переменной в стиле языка C.
  20. done
  21. echo
  22. # Теперь, программисты, пишущие на C, могут чувствовать себя в Bash как дома.
  23. exit 0
Note

Стандартное устройство ввода stdin, для цикла while, можно перенаправить на файл с помощью команды перенаправления < в конце цикла.

until

Оператор цикла until проверяет условие в начале каждой итерации, но в отличие от while итерация возможна только в том случае, если условие ложно.

until [condition-is-true]
do
 command...
done



Обратите внимание: оператор until проверяет условие завершения цикла ПЕРЕД очередной итерацией, а не после, как это принято в некоторых языках программирования.

Как и в случае с циклами for/in, при размещении ключевого слова do в одной строке с объявлением цикла, необходимо вставлять символ ";" перед do.

until [condition-is-true] ; do



Пример 10-18. Цикл until

  1. #!/bin/bash
  2. until [ "$var1" = end ] # Проверка условия производится в начале итерации.
  3. do
  4.   echo "Введите значение переменной #1 "
  5.   echo "(end - выход)"
  6.   read var1
  7.   echo "значение переменной #1 = $var1"
  8. done  
  9. exit 0