Циклы и ветвления

Что повторять?

—Шекспир, Отелло

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

Циклы

Цикл это блок команд, который повторяется (итерируется) [46] до тех пор, пока не будет выполнено условие выхода из цикла .

Цикл for

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

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

Замечание

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

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

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

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

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

for arg in [list] ; do

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

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

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

echo; echo

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

echo; echo "Упс! Плутон больше не планета!"
#См. здесь (прим.перев.)
exit 0

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

Пример 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

Переменная может замещать список [list] в цикле for.

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

#!/bin/bash
# fileinfo.sh

FILES="/usr/sbin/accept
/usr/sbin/pwck
/usr/sbin/chroot
/usr/bin/fakefile
/sbin/badblocks
/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`   # Информация о файле.
  # Заметим, что нужно создать индекс базы данных для whatis для того, чтобы предыдущая команда работала.
  # Чтобы сделать это, из-под пользователя root запустите /usr/bin/makewhatis.
  # (Для Debian/Ubuntu используйте команду /usr/bin/mandb - примеч. перев.)

  echo
done  

exit 0

Если список [list] цикла for содержит шаблонные символы (* и ?), используемые в раскрытии имён файлов (pathname expansion), то имеет место процесс, называемый глоббинг (globbing)

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

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

echo

for file in *
#           ^  Bash выполняет раскрытие имён файлов
#+             в выражениях, которые являются результатом глоббинга.
do
  ls -l "$file"  # Отсортированный список всех файлов в $PWD (текущем каталоге).
  #  # Напоминаю, что символу "*" соответствует любое имя файла,
  # однако, в для глоббинга
  # имеется исключение - имена файлов, начинающиеся с точки.
  # (Такие файлы по умолчанию не отображаются в списке совпадающих с шаблоном имен файлов, см. man bash. Спасибо Dr.Batty за это замечание - прим. перев.)

  #  Если шаблону не соответствует ни один файл, то за имя файла принимается сам шаблон.
  #  Чтобы избежать этого, используйте ключ 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

Если список [list] в цикле for не задан, то в качестве оного используется переменная $@ — список всех аргументов командной строки (позиционных параметров). Очень остроумно эта особенность проиллюстрирована в Пример A.15, «Generating prime numbers using the modulo operator».

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

#!/bin/bash

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

for a
do
 echo -n "$a "
done

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

echo

exit 0

Также имеется возможность использования подстановки команд, чтобы сгенерировать список [list] для цикла for. См. также Пример 10.

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

#!/bin/bash
#  for-loopcmd.sh: Цикл 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


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

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

#!/bin/bash
# bin-grep.sh: Вывод строк, содержащих указанную подстроку в бинарном файле.

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

E_BADARGS=65
E_NOFILE=66

if [ $# -ne 2 ]
then
  echo "Использование: `basename $0` искомая_строка имя_файла"
  exit $E_BADARGS
fi

if [ ! -f "$2" ]
then
  echo "Файл \"$2\" не существует."
  exit $E_NOFILE
fi  

IFS=$'\012'       # С подачи Антона Филиппова (23 строка)
                  # было:  IFS="\n"
for word in $( strings "$2" | grep "$1" )
# Команда "strings" возвращает список строк в двоичных файлах.
# Который затем передается по конвейеру команде "grep" для выполнения поиска.
do
  echo $word
done # (30 строка)

# Как указывает S.C., строки 23 - 30 могут быть заменены более простым
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'

#  Попробуйте проверить работу этого скрипта с помощью чего-то наподобие  "./bin-grep.sh mem /bin/ls"

exit 0


Снова пример на ту же тему.

Пример 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

#  Вопрос:
#  --------
#  Что вы думаете о том, что обыкновенный пользователь (или запускаемый скрипт с правами этого пользователя)
#+ может читать из файла /etc/passwd?
#  Не является ли это дырой в безопасности? Почему да либо почему нет?


Еще пример с подстановкой команд.

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

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

directory=/usr/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,
  #+ необходимо заменить обычный разделитель "/",
  #+ так как "/" встречается среди отфильтровываемых символов.
  # Использование такого символа порождает сообщение об ошибке (попробуйте).
done  

exit $?

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

И заключительный пример использования подстановки команд при создании списка [list], но в этот раз «команда» — это функция.

generate_list ()
{
  echo "one two three"
}

for word in $(generate_list)  # Переменная "word" получает возвращаемое значение функции.
do
  echo "$word"
done

# one
# two
# three

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

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

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

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

echo "symbolic links in directory \"$directory\""

for file in "$( find $directory -type l )"   # -type l = символические ссылки
do
  echo "$file"
done | sort                                  # Иначе получится неотсортированный список.
#  Строго говоря, в действительности цикл не нужен здесь,
#+ так как каждая выводимая командой "find" строка раскрывается в одиночное слово.
#  Тем не менее, этот способ легко понять и проиллюстрировать.

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

exit 0

# --------------------------------------------------------
# Жан Хелу предлагает следующую альтернативу:

echo "symbolic links in directory \"$directory\""
# Сохранить текущее значение IFS в другой переменной. Никогда не помешает быть чуточку осмотрительным.
OLDIFS=$IFS
IFS=:

for file in $(find $directory -type l -printf "%p$IFS")
do     #                              ^^^^^^^^^^^^^^^^
       echo "$file"
       done|sort

# И, наконец, Джеймс "Mike" Конли предложил изменить код Хелу таким образом:

OLDIFS=$IFS
IFS='' # Пустое значение IFS означает, что нет разрыва между словами
for file in $( find $directory -type l )
do
  echo $file
  done | sort

#  Это работает в "патологическом" случае - если имя каталога содержит
#+ двоеточие.
#  "Это также исправляет патологический случай имени каталога, содержащего
#+  двоеточие (или пробел в примере выше)."

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

Пример 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. Для этого используются двойные круглые скобки.

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

#!/bin/bash
# Несколько способов посчитать до 10.

echo

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

echo; echo

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

# Используя команду "seq" ...
for a in `seq 10`
do
  echo -n "$a "
done  

echo; echo

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

# Используя раскрытие скобок (brace expansion) ...
# Требуется bash версии 3 и выше.
for a in {1..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

А теперь сценарий с использованием цикла for из «реальной жизни».

Пример 13. Использование команды efax в пакетном режиме

#!/bin/bash
# Отправка факса (пакет 'efax' должен быть установлен).

EXPECTED_ARGS=2
E_BADARGS=85
MODEM_PORT="/dev/ttyS2"   # Порт может быть другим на вашей машине.
#                ^^^^^      Порт по умолчанию PCMCIA-модема.

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

if [ ! -f "$2" ]
then
  echo "Файл $2 не является текстовым файлом."
  #     Не обычный (regular) файл, либо файл не существует.
  exit $E_BADARGS
fi

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

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

efax -d "$MODEM_PORT"  -t "T$1" $fil   # Отправить.
# Попробуйте добавить опцию  "-o1", если команда выше завершилась неудачно.

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

exit $?   # efax также отправляет диагностические сообщения на стандартный вывод (stdout).
while
Оператор while проверяет условие перед началом каждой итерации и если условие истинно (если код возврата равен 0), то управление передается в тело цикла. В отличие от цикла for, цикл while используется в тех случаях, когда количество итераций заранее не известно.

while [ условие ]
do
 команда(-ы)
done

Конструкция с квадратными скобками в цикле while ничего более, чем наши старая знакомая — команда, используемая для проверки условия if/then. На самом деле цикл while вполне может использовать более универсальную конструкцию с двойными квадратными скобками (while [[ условие ]]).

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

while [ условие ] ; do

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

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

#!/bin/bash

var0=0
LIMIT=10

while [ "$var0" -lt "$LIMIT" ]
#      ^                    ^
# Пробелы, так как используются квадратные скобки - аналог команды test . . .
do
  echo -n "$var0 "        # -n подавляет перевод строки.
  #             ^           Пробел, чтобы разделить выводимые числа.

  var0=`expr $var0 + 1`   # var0=$(($var0+1))  также допустимо.
                          # var0=$((var0 + 1)) также допустимо.
                          # let "var0 += 1"    также допустимо.
done                      # Различные допустимые варианты.

echo

exit 0

Пример 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 может иметь несколько условий. Но только последнее из них определяет возможность продолжения выполнения цикла. Это требует несколько другого синтаксиса.

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

#!/bin/bash

var1=unset
previous=$var1

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

# Попробуйте разобраться, как это все работает.
# Сценарий немножко каверзный.

exit 0

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

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

#!/bin/bash
# wh-loopc.sh: Считаем до 10, используя цикл "while".

LIMIT=10
a=1

while [ "$a" -le $LIMIT ]
do
  echo -n "$a "
  let "a+=1"
done           # До сих пор ничего особенного.

echo; echo

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

# А теперь напишем, используя C-подобный синтаксис.

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

echo

# Программисты на C  могут чувствовать себя в Bash как дома.

exit 0

Вместо конструкции с квадратными скобками в условии, цикл while может вызывать функцию.

t=0

condition ()
{
  ((t++))

  if [ $t -lt 5 ]
  then
    return 0  # true
  else
    return 1  # false
  fi
}

while condition
#     ^^^^^^^^^
#     Вызов функции, после чего произойдет четыре итерации цикла.
do
  echo "Цикл все еще выполняется: t = $t"
done

# Цикл все еще выполняется: t = 1
# Цикл все еще выполняется: t = 2
# Цикл все еще выполняется: t = 3
# Цикл все еще выполняется: t = 4

Как и для конструкции if-test , в подобных случаях для цикла while скобки в условии можно опустить.

while условие
do
   команда(ы) ...
done

Соединяя мощь команды read с циклом while, мы получаем удобную конструкцию while-read, полезную для чтения и синтаксического разбора файлов.

cat $filename |   # Построчный вывод из файла.
while read line   # Продолжать до тех пор, пока есть следующая строка для чтения . . .
do
  ...
done

# =========== Фрагмент из скрипта-примера "sd.sh" ========== #

  while read value   # Читать одну строку с данными за один раз.
  do
    rt=$(echo "шкала=$SC; $rt + $value" | bc)
    (( ct++ ))
  done

  am=$(echo "шкала=$SC; $rt / $ct" | bc)

  echo $am; return $ct   # Эта функция "возвращает" ДВА значения!
  #  Внимание: Эта маленькая уловка не будет работать, если $ct > 255!
  #  Чтобы обрабатывать большее число результатов обработки данных,
  #+ просто закомментируйте строку "return $ct" выше.
} <"$datafile"   # Получение данных из файла.

Замечание

Цикл while может перенаправить данные из файла на свой стандартный ввод (stdin) с помощью символа < в конце цикла.

Цикл while может получать данные через конвейер (pipe) на свой стандартный ввод (stdin).

until
Эта конструкция проверяет условие в начале цикла и выполняет итерации, пока условие ложно (в отличие от цикла while).

until [ условие ]
do
 команда(ы)
done

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

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

until [ условие] ; do

Пример 18. until loop

#!/bin/bash

END_CONDITION=end

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

# ------------------------------------------- #

#  Как и циклы "for" и "while",
#+ цикл "until" позволяет C-подобные конструкции в условии.

LIMIT=10
var=0

until (( var > LIMIT ))
do  # ^^ ^     ^     ^^   Никаких квадратных скобок, никакого символа "$" перед переменными.
  echo -n "$var "
  (( var++ ))
done    # 0 1 2 3 4 5 6 7 8 9 10 

exit 0

Как понять, что выбрать: цикл for, while или until? В языке C, вы обычно используете цикл for тогда, когда количество итераций цикла известно заранее. В Bash, однако, ситуация не четко определена . В Bash цикл for — более свободно структурирован и более гибок, чем его эквиваленты в других языках. Поэтому не стесняйтесь использовать любой тип цикла, выполняющий свою задачу самым простым способом.

Оригинал : bash-scripting.ru/abs/chunks/ch10.html

Запись опубликована в рубрике *Unix,*Linux, Интересные заметки. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Я не спамер This plugin created by Alexei91