Глава 10. Циклы и ветвления

Управление ходом исполнения -- один из ключевых моментов структурной организации сценариев на языке командной оболочки. Циклы и преходы являются теми инструментальными средствами, которые обеспечивают управление порядком исполнения команд.

10.1. Циклы

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

циклы for

for (in)

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

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



Note

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

for arg in "$var1" "$var2" "$var3" ... "$varN"
# На первом проходе, $arg = $var1
# На втором проходе, $arg = $var2
# На третьем проходе, $arg = $var3
# ...
# На N-ном проходе, $arg = $varN

# Элементы списка заключены в кавычки для того, чтобы предотвратить возможное разбиение их на отдельные аргументы (слова).


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

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

for arg in [list] ; do



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

#!/bin/bash
# Список планет.

for planet in Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон
do
  echo $planet
done

echo

# Если 'список аргументов' заключить в кавычки, то он будет восприниматься как единственный аргумент .
for planet in "Меркурий Венера Земля Марс Юпитер Сатурн Уран Нептун Плутон"
do
  echo $planet
done

exit 0
Note

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

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

#!/bin/bash
# Список планет.

# Имя кажой планеты ассоциировано с расстоянием от планеты до Солнца (млн. миль).

for planet in "Меркурий 36" "Венера 67" "Земля 93"  "Марс 142" "Юпитер 483"
do
  set -- $planet  # Разбиение переменной "planet" на множество аргументов (позиционных параметров).
  # Конструкция "--" предохраняет от неожиданностей, если $planet "пуста" или начинается с символа "-".

  # Если каждый из аргументов потребуется сохранить, поскольку на следующем проходе они будут "забиты" новыми значениями,
  # То можно поместить их в массив,
  #        original_params=("$@")

  echo "$1      в $2,000,000 миль от Солнца"
  #----две табуляции---к параметру $2 добавлены нули
done

# (Спасибо S.C., за разъяснения.)

exit 0

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

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

#!/bin/bash
# fileinfo.sh

FILES="/usr/sbin/privatepw
/usr/sbin/pwck
/usr/sbin/go500gw
/usr/bin/fakefile
/sbin/mkreiserfs
/sbin/ypbind"     # Список интересующих нас файлов.
                  # В список добавлен фиктивный файл /usr/bin/fakefile.

echo

for file in $FILES
do

  if [ ! -e "$file" ]       # Проверка наличия файла.
  then
    echo "Файл $file не найден."; echo
    continue                # Переход к следующей итерации.
  fi

  ls -l $file | awk '{ print $8 "         размер: " $5 }'  # Печать 2 полей.
  whatis `basename $file`   # Информация о файле.
  echo
done  

exit 0

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

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

#!/bin/bash
# list-glob.sh: Создание список файлов в цикле for с использованием
# операции подстановки имен файлов ("globbing").

echo

for file in *
do
  ls -l "$file"  # Список всех файлов в $PWD (текущем каталоге).
  # Напоминаю, что символу "*" соответствует любое имя файла,
  # однако, в операциях подстановки имен файлов ("globbing"),
  # имеются исключения -- имена файлов, начинающиеся с точки.

  # Если в каталоге нет ни одного файла, соответствующего шаблону,
  # то за имя файла принимается сам шаблон.
  # Чтобы избежать этого, используйте ключ nullglob
  # (shopt -s nullglob).
  # Спасибо S.C.
done

echo; echo

for file in [jx]*
do
  rm -f $file    # Удаление файлов, начинающихся с "j" или "x" в $PWD.
  echo "Удален файл \"$file\"".
done

echo

exit 0

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

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

#!/bin/bash

# Попробуйте вызвать этот сценарий с аргументами и без них и посмотреть на результаты.

for a
do
 echo -n "$a "
done

#  Список аргументов не задан, поэтому цикл работает с переменной '$@'
#+ (список аргументов командной строки, включая пробельные символы).

echo

exit 0

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

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

#!/bin/bash
# Цикл for со [списком], созданным с помощью подстановки команд.

NUMBERS="9 7 3 8 37.53"

for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
do
  echo -n "$number "
done

echo 
exit 0

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

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

#!/bin/bash
# bin-grep.sh: Поиск строк в двоичных файлах.

# замена "grep" для бинарных файлов.
# Аналогично команде "grep -a"

E_BADARGS=65
E_NOFILE=66

if [ $# -ne 2 ]
then
  echo "Порядок использования: `basename $0` string filename"
  exit $E_BADARGS
fi

if [ ! -f "$2" ]
then
  echo "Файл \"$2\" не найден."
  exit $E_NOFILE
fi


for word in $( strings "$2" | grep "$1" )
# Инструкция "strings" возвращает список строк в двоичных файлах.
# Который затем передается по конвейеру команде "grep", для выполнения поиска.
do
  echo $word
done

# Как указывает S.C., вышепрведенное объявление цикла for может быть упрощено
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'


# Попробуйте что нибудь подобное:  "./bin-grep.sh mem /bin/ls"

exit 0

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

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

#!/bin/bash
# userlist.sh

PASSWORD_FILE=/etc/passwd
n=1           # Число пользователей

for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
# Разделитель полей = :  ^^^^^^
# Вывод первого поля              ^^^^^^^^
# Данные берутся из файла паролей            ^^^^^^^^^^^^^^^^^
do
  echo "Пользователь #$n = $name"
  let "n += 1"
done


# Пользователь #1 = root
# Пользователь #2 = bin
# Пользователь #3 = daemon
# ...
# Пользователь #30 = bozo

exit 0

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

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

#!/bin/bash
# findstring.sh:
# Поиск заданной строки в двоичном файле.

directory=/usr/local/bin/
fstring="Free Software Foundation"  # Поиск файлов от FSF.

for file in $( find $directory -type f -name '*' | sort )
do
  strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  #  Команде "sed" передается выражение (ключ -e),
  #+ для того, чтобы изменить обычный разделитель "/" строки поиска и строки замены
  #+ поскольку "/" - один из отфильтровываемых символов.
  #  Использование такого символа порождает сообщение об ошибке (попробуйте).
done

exit 0

#  Упражнение:
#  ---------------
#  Измените сценарий таким образом, чтобы он брал
#+ $directory и $fstring из командной строки.

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

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

#!/bin/bash
# symlinks.sh: Список символических ссылок в каталоге.


directory=${1-`pwd`}
#  По-умолчанию в текущем каталоге,
#  Блок кода, который выполняет аналогичные действия.
# ----------------------------------------------------------
# ARGS=1                 # Ожидается один аргумент командной строки.
#
# if [ $# -ne "$ARGS" ]  # Если каталог поиска не задан...
# then
#   directory=`pwd`      # текущий каталог
# else
#   directory=$1
# fi
# ----------------------------------------------------------

echo "символические ссылки в каталоге \"$directory\""

for file in "$( find $directory -type l )"   # -type l = символические ссылки
do
  echo "$file"
done | sort             # В противном случае получится неотсортированный список.

#  Как отмечает Dominik 'Aeneas' Schnitzer,
#+ в случае отсутствия кавычек для $( find $directory -type l )
#+ сценарий "подавится" именами файлов, содержащими пробелы.

exit 0

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

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

#!/bin/bash
# symlinks.sh: Список символических ссылок в каталоге.

OUTFILE=symlinks.list                         # файл со списком

directory=${1-`pwd`}
#  По-умолчанию -- текущий каталог,

echo "символические ссылки в каталоге \"$directory\"" > "$OUTFILE"
echo "---------------------------" >> "$OUTFILE"

for file in "$( find $directory -type l )"    # -type l = символические ссылки
do
  echo "$file"
done | sort >> "$OUTFILE"                     # перенаправление вывода
#           ^^^^^^^^^^^^^                       в файл.

exit 0

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

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

#!/bin/bash
# Два вапианта оформления цикла.

echo

# Стандартный синтаксис.
for a in 1 2 3 4 5 6 7 8 9 10
do
  echo -n "$a "
done

echo; echo

# +==========================================+

# А теперь C-подобный синтаксис.

LIMIT=10

for ((a=1; a <= LIMIT ; a++))  # Двойные круглые скобки и "LIMIT" без "$".
do
  echo -n "$a "
done                           # Конструкция заимствована из 'ksh93'.

echo; echo

# +=========================================================================+

# Попробуем и C-шный оператор "запятая".

for ((a=1, b=1; a <= LIMIT ; a++, b++))  # Запятая разделяет две операции, которые выполняются совместно.
do
  echo -n "$a-$b "
done

echo; echo

exit 0

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

---

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

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

#!/bin/bash

EXPECTED_ARGS=2
E_BADARGS=65

if [ $# -ne $EXPECTED_ARGS ]
# Проверка наличия аргументов командной строки.
then
   echo "Порядок использования: `basename $0` phone# text-file"
   exit $E_BADARGS
fi


if [ ! -f "$2" ]
then
  echo "Файл $2 не является текстовым файлом"
  exit $E_BADARGS
fi


fax make $2              # Создать fax-файлы из текстовых файлов.

for file in $(ls $2.0*)  # Все файлы, получившиеся в результате преобразования.
                         # Используется шаблонный символ в списке.
do
  fil="$fil $file"
done

efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # отправить.


# Как указывает S.C., в цикл for может быть вставлена сама команда отправки в виде:
#    efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
# но это не так поучительно [;-)].

exit 0
while

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

while [condition]
do
 command...
done



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

while [condition] ; do



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

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

#!/bin/bash

var0=0
LIMIT=10

while [ "$var0" -lt "$LIMIT" ]
do
  echo -n "$var0 "        # -n подавляет перевод строки.
  var0=`expr $var0 + 1`   # допускается var0=$(($var0+1)).
done

echo

exit 0

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

#!/bin/bash

echo

while [ "$var1" != "end" ]     # возможна замена на while test "$var1" != "end"
do
  echo "Введите значение переменной #1 (end - выход) "
  read var1                    # Конструкция 'read $var1' недопустима (почему?).
  echo "переменная #1 = $var1" # кавычки обязательны, потому что имеется символ "#".
  # Если введено слово 'end', то оно тоже выводится на экран.
  # потому, что проверка переменной выполняется в начале итерации (перед вводом).
  echo
done  

exit 0

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

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

#!/bin/bash

var1=unset
previous=$var1

while echo "предыдущее значение = $previous"
      echo
      previous=$var1     # запомнить предыдущее значение
      [ "$var1" != end ]
      # В операторе "while" присутствуют 4 условия, но только последнее управляет циклом.
      # *последнее* условие - единственное, которое вычисляется.
do
echo "Введите значение переменной #1 (end - выход) "
  read var1
  echo "текущее значение = $var1"
done

# попробуйте самостоятельно разобраться в сценарии works.

exit 0

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

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

#!/bin/bash
# wh-loopc.sh: Цикл перебора от 1 до 10.

LIMIT=10
a=1

while [ "$a" -le $LIMIT ]
do
  echo -n "$a "
  let "a+=1"
done           # Пока ничего особенного.

echo; echo

# +=================================================================+

# А теперь оформим в стиле языка C.

((a = 1))      # a=1
# Двойные скобки допускают наличие лишних пробелов в выражениях.

while (( a <= LIMIT ))   # В двойных скобках символ "$" перед переменными опускается.
do
  echo -n "$a "
  ((a += 1))   # let "a+=1"
  # Двойные скобки позволяют наращивание переменной в стиле языка C.
done

echo

# Теперь, программисты, пишущие на C, могут чувствовать себя в Bash как дома.

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

#!/bin/bash

until [ "$var1" = end ] # Проверка условия производится в начале итерации.
do
  echo "Введите значение переменной #1 "
  echo "(end - выход)"
  read var1
  echo "значение переменной #1 = $var1"
done  

exit 0