Книга: Основы программирования в Linux
Управляющие структуры
Разделы на этой странице:
- Примечание
- if
- Примечание
- elif
- Упражнение 2.3. Выполнение проверок с помощью elif
- Проблема, связанная с переменными
- Примечание
- for
- Упражнение 2.4. Применение цикла for к фиксированным строкам
- Примечание
- Упражнение 2.5. Применение цикла for с метасимволами
- Примечание
- while
- until
- Примечание
- case
- Примечание
- Примечание
- Упражнение 2.6. Вариант 1: пользовательский ввод
- Упражнение 2.7. Вариант 3: объединение образцов
- Упражнение 2.8. Вариант 3: выполнение нескольких операторов
- Примечание
- Списки
- И-cписок
- Упражнение 2.9. И-списки
- ИЛИ-список
- Упражнение 2.10. ИЛИ-списки
- Операторные блоки
Управляющие структуры
В командной оболочке есть ряд управляющих структур или конструкций, похожих на аналогичные структуры в других языках программирования.
Примечание
В следующих разделах элемент синтаксической записи операторы — это последовательности команд, которые выполняются, когда или пока условие удовлетворяется или пока оно не удовлетворяется.
if
Управляющий оператор if
очень прост: он проверяет результат выполнения команды и затем в зависимости отусловия
выполняет ту или иную группу операторов.
if условие
then
операторы
else
операторы
fi
Наиболее часто оператор if
применяется, когда задается вопрос, и решение принимается в зависимости от ответа:
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
if [ $timeofday = "yes" ]; then
echo "Good morning"
else
echo "Good afternoon"
fi
exit 0
В результате будет получен следующий вывод на экран:
Is it morning? Please answer yes or no
yes
Good morning
$
В этом сценарии для проверки содержимого переменной timeofday
применяется команда [
. Результат оценивается оператором командной оболочки if
, который затем разрешает выполнять разные строки программного кода.
Примечание
Обратите внимание на дополнительные пробелы, используемые для формирования отступа внутри оператора if
. Это делается только для удобства читателя; командная оболочка игнорирует дополнительные пробелы.
elif
К сожалению, с этим простым сценарием связано несколько проблем. Во-первых, он принимает в значении no
(нет) любой ответ за исключением yes
(да). Можно помешать этому, воспользовавшись конструкцией elif
, которая позволяет добавить второе условие, проверяемое при выполнении части else
оператора if
(упражнение 2.3).
Упражнение 2.3. Выполнение проверок с помощью elif
Вы можете откорректировать предыдущий сценарий так, чтобы он выводил сообщение об ошибке, если пользователь вводит что-либо отличное от yes
или no
. Для этого замените ветку else
веткой elif
и добавьте еще одно условие:
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
if [ $timeofday = "yes" ]
then
echo "Good morning"
elif [ $timeofday = "no" ]; then
echo "Good afternoon"
else
echo "Sorry, $timeofday not recognized. Enter yes or no "
exit 1
fi
exit 0
Как это работает
Этот пример очень похож на предыдущий, но теперь, если первое условие не равно true
, оператор командной оболочки elif
проверяет переменную снова. Если обе проверки не удачны, выводится сообщение об ошибке, и сценарий завершается со значением 1, которое в вызывающей программе можно использовать для проверки успешного выполнения сценария.
Проблема, связанная с переменными
Данный сценарий исправляет наиболее очевидный дефект, а более тонкая проблема остается незамеченной. Запустите новый вариант сценария, но вместо ответа на вопрос просто нажмите клавишу <Enter> (или на некоторых клавиатурах клавишу <Return>). Вы получите сообщение об ошибке:
[: =: unary operator expected
Что же не так? Проблема в первой ветви оператора if
. Когда проверялась переменная timeofday
, она состояла из пустой строки. Следовательно, ветвь оператора if
выглядела следующим образом:
if [ = "yes" ]
и не представляла собой верное условие. Во избежание этого следует заключить имя переменной в кавычки:
if [ "$timeofday" = "yes" ]
Теперь проверка с пустой переменной будет корректной:
if [ "" = "yes" ]
Новый сценарий будет таким:
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
if [ "$timeofday" = "yes" ]
then
echo "Good morning"
elif [ "$timeofday" = "no" ]; then
echo "Good afternoon"
else
echo "Sorry, $timeofday not recognized. Enter yes or no "
exit 1
fi
exit 0
Этот вариант безопасен, даже если пользователь в ответ на вопрос просто нажмет клавишу <Enter>.
Примечание
Если вы хотите, чтобы команда echo
удалила новую строку в конце, наиболее легко переносимый вариант — применить команду printf
(см. разд. "printf" далее в этой главе) вместо команды echo
. В некоторых командных оболочках применяется команда echo -е
, но она поддерживается не всеми системами. В оболочке bash для запрета перехода на новую строку допускается команда echo -n
, поэтому, если вы уверены, что вашему сценарию придется трудиться только в оболочке bash, предлагаем вам использовать следующий синтаксис:
echo -n "Is it morning? Please answer yes or no: "
Помните о том, что нужно оставлять дополнительный пробел перед закрывающими кавычками, таким образом формируется зазор перед вводимым пользователем ответом, который в этом случае выглядит четче.
for
Применяйте конструкцию for
для обработки в цикле ряда значений, которые могут представлять собой любое множество строк. Строки могут быть просто перечислены в программе или, что бывает чаще, представлять собой результат выполненной командной оболочкой подстановки имен файлов.
Синтаксис этого оператора прост:
for переменная in значения
do
операторы
done
Выполните упражнения 2.4 и 2.5.
Упражнение 2.4. Применение цикла for
к фиксированным строкам
В командной оболочке значения обычно представлены в виде строк, поэтому можно написать следующий сценарий:
#!/bin/sh
for foo in bar fud 43
do
echo $foo
done
exit 0
В результате будет получен следующий вывод:
bar
fud
43
Примечание
Что произойдет, если вы измените первую строку с for foo in bar fud 43
на for foo in "bar fud 43"
? Напоминаем, что вставка кавычек заставляет командную оболочку считать все, что находится между ними, единой строкой. Это один из способов сохранения пробелов в переменной.
Как это работает
В данном примере создается переменная foo
и ей в каждом проходе цикла for
присваиваются разные значения. Поскольку оболочка считает по умолчанию все переменные строковыми, применять строку 43 так же допустимо, как и строку fud
.
Упражнение 2.5. Применение цикла for
с метасимволами
Как упоминалось ранее, цикл for
обычно используется в командной оболочке вместе с метасимволами или знаками подстановки для имен файлов. Это означает применение метасимвола для строковых значений и предоставление оболочке возможности подставлять все значения на этапе выполнения.
Вы уже видели этот прием в первом примере first. В сценарии применялись средства подстановки командной оболочки — символ *
для подстановки имен всех файлов из текущего каталога. Каждое из этих имен по очереди используется в качестве значения переменной $file
внутри цикла for
.
Давайте бегло просмотрим еще один пример подстановки с помощью метасимвола. Допустим, что вы хотите вывести на экран все имена файлов сценариев в текущем каталоге, начинающиеся с буквы "f", и вы знаете, что имена всех ваших сценариев заканчиваются символами .sh. Это можно сделать следующим образом:
#!/bin/sh
for file in $(ls f*.sh); do
lpr $file
done
exit 0
Как это работает
В этом примере показано применение синтаксической конструкции $(команда)
, которая будет подробно обсуждаться далее (в разделе, посвященном выполнению команд). Обычно список параметров для цикла for
задается выводом команды, включенной в конструкцию $()
.
Командная оболочка раскрывает f*.sh
, подставляя имена всех файлов, соответствующих данному шаблону.
Примечание
Помните о том, что все подстановки переменных в сценариях командной оболочки делаются во время выполнения сценария, а не в процессе их написания, поэтому все синтаксические ошибки в объявлениях переменных обнаруживаются только на этапе выполнения, как было показано ранее, когда мы заключали в кавычки пустые переменные.
while
Поскольку по умолчанию командная оболочка считает все значения строками, оператор for
хорош для циклической обработки наборов строк, но не слишком удобен, если вы не знаете заранее, сколько раз придется его выполнить.
Если нужно повторить выполнение последовательности команд, но заранее не известно, сколько раз следует их выполнить, вы, как правило, будете применять цикл while
со следующей синтаксической записью:
while условие
do
операторы
done
Далее приведен пример довольно слабой программы проверки паролей.
#!/bin/sh
echo "Enter password"
read trythis
while [ "$trythis" != "secret" ]; do
echo "Sorry, try again"
read trythis
done
exit 0
Следующие строки могут служить примером вывода данного сценария:
Enter password
password
Sorry, try again
secret
$
Ясно, что это небезопасный способ выяснения пароля, но он вполне подходит для демонстрации применения цикла while
. Операторы, находящиеся между операторами do
и done
, выполняются бесконечное число раз до тех пор, пока условие остается истинным (true
). В данном случае вы проверяете, равно ли значение переменной trythis
строке secret
. Цикл будет выполняться, пока $trythis
не равно secret
. Затем выполнение сценария продолжится с оператора, следующего сразу за оператором done
.
until
У цикла until
следующая синтаксическая запись:
until условие
do
операторы
done
Она очень похожа на синтаксическую запись цикла while
, но с обратным проверяемым условием. Другими словами, цикл продолжает выполняться, покаусловие
не станет истинным (true).
Примечание
Как правило, если нужно выполнить цикл хотя бы один раз, применяют цикл while
; если такой необходимости нет, используют цикл until
.
Как пример цикла until
можно установить звуковой сигнал предупреждения, инициируемый во время регистрации нового пользователя, регистрационное имя которого передается в командную строку.
#!/bin/bash
until who | grep "$1" > /dev/null
do
sleep 60
done
# Теперь звонит колокольчик и извещает о новом пользователе
echo -е 'а'
echo "**** $1 has just logged in ****"
exit 0
Если пользователь уже зарегистрировался в системе, выполнять цикл нет необходимости. Поэтому естественно выбрать цикл until
, а не цикл while
.
case
Оператор case
немного сложнее уже рассмотренных нами операторов. У него следующая синтаксическая запись:
case переменная in
образец [ | образец] ...) операторы;;
образец [ | образец] ...) операторы;;
esac
Конструкция оператора case
выглядит слегка устрашающей, но она позволяет довольно изощренным способом сопоставлять содержимое переменной с образцами и затем выполнять разные операторы в зависимости от того, с каким образцом найдено соответствие. Это гораздо проще, чем проверять несколько условий, применяемых во множественных операторах if
, elif
и else
.
Примечание
Обратите внимание на то, что каждая ветвь с образцами завершается удвоенным символом "точка с запятой" (;;). В каждой ветви оператора case
можно поместить несколько операторов, поэтому сдвоенная точка с запятой необходима для отметки завершения очередного оператора и начала следующей ветви с новым образцом в операторе case
.
Возможность сопоставлять многочисленные образцы и затем выполнять множественные связанные с образцом операторы делают конструкцию case
очень удобной для обработки пользовательского ввода. Лучше всего увидеть, как работает конструкция case
на примерах. Мы будем применять ее в упражнениях 2.6–2.8, каждый раз совершенствуя сопоставление с образцами.
Примечание
Применяя конструкцию case
с метасимволами в образцах, такими как *
, будьте особенно внимательны. Проблема заключается в том, что принимается во внимание первое найденное соответствие образцу, несмотря на то, что в последующих ветвях могут быть образцы с более точным соответствием.
Упражнение 2.6. Вариант 1: пользовательский ввод
Вы можете написать новую версию сценария проверки пользовательского ввода с помощью конструкции case
, сделав сценарий немного более избирательным и терпимым к неожиданным вариантам ввода.
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
case "$timeofday" in
yes) echo "Good Morning";;
no ) echo "Good Afternoon";;
y ) echo "Good Morning";;
n ) echo "Good Afternoon";;
* ) echo "Sorry, answer not recognized";;
esac
exit 0
Как это работает
Когда выполняется оператор case
, он берет содержимое переменной timeofday
и сравнивает его поочередно с каждой строкой-образцом. Как только строка совпадает с введенной информацией, оператор case
выполняет код, следующий за ), и завершается.
Оператор case
выполняет обычную подстановку в строках, которые он использует для сравнения. Следовательно, вы можете задать часть строки с последующим метасимволом *
. Применение единственного символа *
будет соответствовать совпадению с любой введенной строкой, поэтому поместите этот вариант после всех остальных образцов строк для того, чтобы задать некоторое стандартное поведение оператора case
, если не будут найдены совпадения с другими строками-образцами. Это возможно, потому что оператор case
сравнивает с каждой строкой-образцом поочередно. Он не ищет наилучшее соответствие, а всего лишь первое встретившееся. Условие, принятое по умолчанию, часто оказывается невыполнимым, поэтому применение метасимвола *
может помочь в отладке сценариев.
Упражнение 2.7. Вариант 3: объединение образцов
Предыдущая версия конструкции case, безусловно, элегантнее варианта с множественными операторами if
, но, объединив все образцы, можно создать более красивую версию.
#!/bin/sh
echo "Is it morning? Please answer yes or no "
read timeofday
case "$timeofday" in
yes | y | Yes | YES ) echo "Good Morning";;
n* | N*) echo "Good Afternoon";;
* ) echo "Sorry, answer not recognized";;
esac
exit 0
Как это работает
Данный сценарий в операторе case
использует несколько строк-образцов в каждой ветви, таким образом, case
проверяет несколько разных строк для каждого возможного оператора. Этот прием делает сценарий короче и, как показывает практика, облегчает его чтение. Приведенный программный код также показывает, как можно использовать метасимвол *
, несмотря на то, что он может соответствовать непредусмотренным образцам. Например, если пользователь введет строку never
, она будет соответствовать образцу n*
, и на экран будет выведено приветствие Good Afternoon (Добрый день), хотя такое поведение в сценарии не предусматривалось. Учтите также, что заключенный в кавычки знак подстановки *
не действует.
Упражнение 2.8. Вариант 3: выполнение нескольких операторов
В заключение, для того чтобы сделать сценарий многократно используемым, вам необходимо использовать другое значение кода завершения в том случае, когда применяется образец по умолчанию для непонятного варианта ввода.
#!/bin/sh
echo "Is it -morning? Please answer yes or no"
read timeofday
case "$timeofday" in
yes | y | Yes | YES )
echo "Good Morning"
echo "Up bright and early this morning"
;;
[nN]*)
echo "Good Afternoon"
;;
*)
echo "Sorry, answer not recognized"
echo "Please answer yes or no"
exit 1
;;
esac
exit 0
Как это работает
Для демонстрации другого способа определения соответствия образцу в этом программном коде изменен вариант определения соответствия для ветви no
. Также видно, как в каждой ветви оператора case может выполняться несколько операторов. Следует быть внимательным и располагать в операторе самые точные образцы строк первыми, а самые общие варианты образцов последними. Это очень важно, потому что оператор case
выполняется, как только найдено первое, а не наилучшее соответствие. Если вы поставите ветвь *)
первой, совпадение с этим образцом будет определяться всегда, независимо от варианта введенной строки.
Примечание
Учтите, что сдвоенная точка с запятой ;; перед служебным словом esac
необязательна. В отличие от программирования на языке С, в котором пропуск маркера завершения считается плохим стилем программирования, пропуск ;; не создает проблем, если последняя ветвь оператора case
— это вариант, принятый по умолчанию, поскольку другие образцы уже не будут анализироваться.
Для того чтобы сделать средства установления соответствия образцам более мощными, можно применять следующие строки-образцы:
[yY] | [Yy][Ее][Ss])
В них ограничен набор разрешенных букв, но при этом допускаются разнообразные ответы и предлагается более строгий контроль, чем при применении метасимвола *
.
Списки
Иногда может понадобиться сформировать последовательность команд. Например, вы хотите выполнить оператор, только если удовлетворяется несколько условий.
if [ -f this_file ]; then
if [ -f that_file ]; then
if [ -f the_other_file ]; then
echo "All files present, and correct"
fi
fi
fi
Или вы хотите, чтобы хотя бы одно условие из последовательности условий было истинным.
if [ -f this_file ]; then
foo="True"
elif [ -f that_file ]; then
foo="True"
elif [ -f the_other_file ];
then foo="True"
else
foo="False"
fi
if ["$foo" = "True" ]; then
echo "One of the files exists"
fi
Несмотря на то, что это можно реализовать с помощью нескольких операторов if
, как видите, результаты получаются очень громоздкими. В командной оболочке есть пара специальных конструкций для работы со списками команд: И-список (AND list) и ИЛИ-список (OR list). Обе они часто применяются вместе, но мы рассмотрим синтаксическую запись каждой из них отдельно.
И-cписок
Эта конструкция позволяет выполнять последовательность команд, причем каждая последующая выполняется только при успешном завершении предыдущей. Синтаксическая запись такова:
оператор1 && оператор2 && оператор3 && ...
Выполнение операторов начинается с самого левого, если он возвращает значение true
(истина), выполняется оператор, расположенный справа от первого оператора. Выполнение продолжается до тех пор, пока очередной оператор не вернет значение false
(ложь), после чего никакие операторы списка не выполняются. Операция &&
проверяет условие предшествующей команды.
Каждый оператор выполняется независимо, позволяя соединять в одном списке множество разных команд, как показано в приведенном далее сценарии. И-список успешно обрабатывается, если все команды выполнены успешно, в противном случае его обработка заканчивается неудачно.
Выполните упражнение 2.9.
Упражнение 2.9. И-списки
В следующем сценарии вы обращаетесь к файлу file_one (для проверки его наличия, и если файл не существует, создаете его) и затем удаляете файл file_two. Далее И-список проверяет наличие каждого файла и между делом выводит на экран кое-какой текст.
#!/bin/sh
touch file_one
rm -f file_two
if [ -f file_one ] && echo "hello" [ -f file_two ] && echo " there"
then
echo "in if"
else
echo "in else"
fi
exit 0
Попробуйте выполнить сценарий, и вы получите следующий вывод:
hello
in else
Как это работает
Команды touch
и rm
гарантируют, что файлы в текущем каталоге находятся в известном состоянии. Далее И-список выполняет команду [ -f file one ]
, которая возвращает значение true
, потому что вы только что убедились в наличии файла. Поскольку предыдущий оператор завершился успешно, теперь выполняется команда echo
. Она тоже завершается успешно (echo
всегда возвращает true
). Затем выполняется третья проверка [ -f file_two ]
. Она возвращает значение false
, т.к. файл не существует. Поскольку последняя команда вернула false
, заключительная команда echo
не выполняется. В результате И-список возвращает значение false
, поэтому в операторе if
выполняется вариант else
.
ИЛИ-список
Эта конструкция позволяет выполнять последовательность команд до тех пор, пока одна из них не вернет значение true, и далее не выполняется ничего более. У нее следующая синтаксическая запись:
оператор1 || оператор2 || оператор3 || ...
Операторы выполняются слева направо. Если очередной оператор возвращает значение false
, выполняется следующий за ним оператор. Это продолжается до тех пор, пока очередной оператор не вернет значение true
, после этого никакие операторы уже не выполняются.
ИЛИ-список очень похож на И-список, за исключением того, что правило для выполнения следующего оператора — выполнение предыдущего оператора со значением false
.
Рассмотрим упражнение 2.10.
Упражнение 2.10. ИЛИ-списки
Скопируйте сценарий из предыдущего упражнения и измените затененные строки следующим образом.
#!/bin/sh
rm -f file_one
if [ -f file_one ] || echo "hello" || echo " there" then
echo "in if"
else
echo "in else"
fi
exit 0
В результате выполнения данного сценария будет получен следующий вывод:
hello
in if
Как это работает
В первых двух строках просто задаются файлы для остальной части сценария. Первая команда списка [ -f file one ]
возвращает значение false
, потому что файла в каталоге нет. Далее выполняется команда echo
. Вот это да — она возвращает значение true
, и больше в ИЛИ-списке не выполняются никакие команды. Оператор if
получает из списка значение true, поскольку одна из команд ИЛИ-списка (команда echo
) вернула это значение.
Результат, возвращаемый обоими этими списками, — это результат последней выполненной команды списка.
Описанные конструкции списков выполняются так же, как аналогичные конструкции в языке С, когда проверяются множественные условия. Для определения результата выполняется минимальное количество операторов. Операторы, не влияющие на конечный результат, не выполняются. Обычно этот подход называют оптимизацией вычислений (short circuit evaluation).
Комбинирование этих двух конструкций — высшее блаженство для любителей логических задач. Попробуйте проанализировать следующий список:
[ -f file_one ] && команда в случае true || команда в случае false
В нем будет выполняться первая команда в случае истинности проверки и вторая команда в противном случае. Всегда лучше всего поэкспериментировать с этими довольно необычными списками, и, как правило, вам придется использовать скобки для изменения порядка вычислений.
Операторные блоки
Если вы хотите применить несколько операторов в том месте программного кода, где разрешен только один, например в ИЛИ-списке или И-списке, то можете сделать это, заключив операторы в фигурные скобки {}
и создав тем самым операторный блок. Например, в приложении, представленном далее в этой главе, вы увидите следующий фрагмент программного кода:
get_confirm && {
grep -v "$cdcatnum" $tracks_file > $temp_file
cat $temp_file > $tracks_file
echo
add record_tracks
}