Что повторять?
—Шекспир, Отелло
Операции с блоками кода — ключ к структурированным и упорядоченным сценариям оболочки. Циклы и ветвления являются теми инструментальными средствами, которые предоставляют возможность достигнуть этой цели.
Циклы
Цикл это блок команд, который повторяется (итерируется) [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.
Пример 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