Table of Contents
bash Управление потоком выполнения: цикл for
Цикл for отличается от циклов while и until поддержкой средств обработки последовательностей. Это очень полезная возможность. Как следствие, цикл for пользуется большой популярностью среди создателей сценариев для bash.
Цикл for реализован, что вполне естественно, в виде команды for. В современных версиях bash поддерживается две формы команды for.
for: традиционная форма
Оригинальный синтаксис команды for имеет следующий вид:
for переменная [in слова]; do команды done
где переменная – это имя переменной, значение которой будет увеличиваться в ходе выполнения цикла, слова — необязательный список элементов, которые последовательно будут присваиваться переменной, и команды – это команды, выполняемые в каждой итерации.
Команду for удобно использовать в командной строке. Рассмотрим, как она работает:
for i in A B C D; do echo $i; done A B C D
В этом примере команда for получает список из четырех слов: A, B, C и D. Для обхода этого списка выполняется четыре итерации цикла. В начале каждой итерации переменной i присваивается очередное слово. Внутри цикла находится команда echo, она выводит значение i, чтобы показать, что присваивание действительно выполняется. Так же как в случае с циклами while и until, цикл for заканчивается ключевым словом done.
По-настоящему мощной особенностью for является разнообразие способов формирования списка слов. Например, можно использовать подстановку в фигурных скобках:
for i in {A..D}; do echo $i; done A B C D
или подстановку имен файлов:
for i in distros*.txt; do echo "$i"; done distros-by-date.txt distros-date.txt distros-dates.txt distros-key-names.txt distros-key-vernum.txt distros-keyvernum.txt distros-name.txt distros-vernums.txt distros-versions.txt distros.txt
Механизм подстановки имен файлов позволяет получить четкий список с именами файлов, который можно обработать в цикле. Единственное, о чем следует позаботиться, – убедиться, что механизм подстановки вернул действительные совпадения. По умолчанию, если операция подстановки не найдет ни одного файла, соответствующего шаблону, она вернет сам шаблон (в данном случае <html>distros*.txt</html>). Чтобы защититься от этой проблемы, предыдущий код можно преобразовать в сценарий, как показано ниже:
for i in distros*.txt; do if [[ -e "$i" ]]; then echo "$i" fi done
Добавив проверку существования файла, мы обезопасили себя от ситуации, когда механизм подстановки не нашел ни одного файла. Другой распространенный метод получения списка слов – подстановка команд.
#!/bin/bash # longest-word : поиск самой длинной строки в файле while [[ -n "$1" ]]; do if [[ -r "$1" ]]; then max_word= max_len=0 for i in $(strings "$1"); do len="$(echo "$i" | wc -c)" if (( len > max_len )); then max_len="$len" max_word="$i" fi done echo "$1: '$max_word' ($max_len characters)" fi shift done
Этот пример осуществляет поиск самой длинной строки в файле. Когда в командной строке указано несколько имен файлов, сценарий вызывает процедуру strings (входит в состав пакета GNU binutils), чтобы получить список «слов» из каждого файла. Цикл for обрабатывает каждое слово по очереди и определяет, является ли оно самым длинным из встречавшихся до сих пор. По завершении
цикла сценарий выводит самое длинное слово.
Обратите внимание, что здесь, вопреки обычной практике, мы не заключили подстановку команд <html>$(strings “$1”)</html> в кавычки. Объясняется это просто: здесь мы действительно хотим разбить строку на слова, чтобы получить список. Заключив подстановку команд в кавычки, мы получили бы одно слово, содержащее все строки из файла. А это не совсем то, что нам нужно.
Если необязательный компонент слова в команде for отсутствует, она по умолчанию обрабатывает позиционные параметры. Чтобы показать использование этого способа, изменим сценарий <html>longest-word</html>:
#!/bin/bash # longest-word2 : поиск самой длинной строки в файле for i; do if [[ -r "$i" ]]; then max_word= max_len=0 for j in $(strings "$i"); do len="$(echo "$j" | wc -c)" if (( len > max_len )); then max_len="$len" max_word="$j" fi done echo "$i: '$max_word' ($max_len characters)" fi done
Как видите, мы заменили внешний цикл while циклом for. Так как список слов в команде for отсутствует, она перебирает позиционные параметры. Во внутреннем цикле вместо переменной i теперь используется переменная j. Кроме того, нам больше не нужна команда shift.
<callout type=“primary” icon=“true” title=“ПОЧЕМУ I?”>Вы могли заметить, что во всех примерах цикла for выше использовалась переменная i. Почему? В действительности за этим выбором не стоят какие-то определенные причины, кроме стремления следовать традициям. В команде for можно использовать любую допустимую переменную, но чаще всего используется переменная i, а также j и k.
Своими корнями эта традиция уходит в язык программирования Fortran. В Fortran необъявленные переменные, начинающиеся с букв I, J, K, L и M, автоматически становились целочисленными, тогда как переменные, начинающиеся с любой
другой буквы, – действительными, или вещественными (способными хранить числа с дробной частью). Эта особенность вынуждала программистов использовать переменные I, J и K в качестве переменных цикла, так как использование их в качестве временных переменных (чем переменные цикла в действительности и являются) требовало меньших усилий.
Из-за этого даже в среде программистов на Fortran ходила острота: «GOD is real, unless declared integer» (Бог действителен, пока явно не объявлен целым).</callout>
for: форма в стиле языка C
В последние версии bash добавлена вторая форма синтаксиса команды for, напоминающая одноименный оператор в языке программирования C, которая поддерживается также многими другими языками.
for (( выражение1; выражение2; выражение3 )); do команды done
выражение1 инициализирует цикл, выражение2 определяет условие завершения цикла, выражение3 выполняется в конце каждой итерации.
Ниже приводится пример типичного применения:
#!/bin/bash # simple_counter : демонстрация команды for в стиле языка C for (( i=0; i<5; i=i+1 )); do echo $i done
Этот сценарий произведет следующий вывод:
simple_counter 0 1 2 3 4
Здесь выражение1 инициализирует переменную i значением 0, выражение2 позволяет продолжать итерации, пока значение i остается меньше 5, выражение3 увеличивает на единицу значение i в конце каждой итерации.
Форма команды for в стиле языка C выглядит предпочтительнее, если требуется работать с числовыми последовательностями.
sys_info_page
Познакомившись с командой for, внесем заключительное усовершенствование в наш сценарий sys_info_page. В настоящий момент функция report_home_space выглядит так:
report_home_space () { if [[ $(id -u) -eq 0 ]]; then cat <<- _EOF_ <h2>HOME SPACE UTILIZATION</h2> <pre>$(du -sh /home/*)</pre> _EOF_ else cat <<- _EOF_ <h2>HOME SPACE UTILIZATION</h2> <pre>$(du -sh $HOME)</pre> _EOF_ fi return }
Теперь мы можем переписать ее, добавив вывод информации о домашнем каталоге каждого пользователя и включив в вывод общее число файлов и подкаталогов в каждом из них:
report_home_space () { local format="%8s%10s%10s\n" local i dir_list total_files total_dirs total_size user_name if [[ $(id -u) -eq 0 ]]; then dir_list=/home/* user_name="All Users" else dir_list="$HOME" user_name="$USER" fi echo "<h2>Home Space Utilization</h2>" for i in $dir_list; do total_files="$(find "$i" -type f | wc -l)" total_dirs="$(find "$i" -type d | wc -l)" total_size="$(du -sh "$i" | cut -f 1)" echo "<h3>$i</h3>" echo "<pre>" printf "$format" "Dirs" "Files" "Size" printf "$format" "____" "_____" "____" printf "$format" "$total_dirs" "$total_files" "$total_size" done return }
В этой новой версии применено многое из того, что мы узнали к данному моменту. Она все еще проверяет наличие привилегий суперпользователя, но вместо того, чтобы выполнить полный набор операций в каждой из ветвей if, здесь устанавливаются некоторые переменные, которые затем используются в цикле for. В функции использованы несколько локальных переменных и команда printf для форматирования части вывода.