Практическое написание сценариев командной оболочки Bash: различия между версиями

Содержимое удалено Содержимое добавлено
Новая страница: «В этом учебнике мы рассмотрим язык сценариев командной оболочки Bash. Отличительной особенностью этого учебника заключается в том, что мы не будем сухо пересказывать документацию к командной оболочке, а попытаемся рассказать о языке с практической сто...»
 
Строка 228:
 
== Ветвления ==
Ветвления в Bash оформляются так, как это было придумано в оригинальном Bourne Shell:
<source lang=Bash>
if <командный список>; then
<командный список>
elif <командный список>; then
<командный список>
else
<командный список>
fi
</source>
 
'''Командным списком''' в Bash называется последовательность, состоящая минимум из одной команды или одного конвейера. Интерпретатор распознает командный список по следующим синтаксическим якорям:
* строка заканчивается точкой с запятой (<code>;</code>), амперсандом (<code>&</code>) или символом новой строки;
* предыдущее условие плюс, если команд несколько, они отделяются символом точки с запятой (<code>;</code>), амперсандом (<code>&</code>), двойным амперсандом (<code>&&</code>), либо двумя вертикальными линиями (<code>||</code>).
 
;Обратим внимание на следующие важные моменты
* Выражение после ключевых слов <code>if</code> и <code>elif</code> является ''командным списком'', т.е. при желании вы можете написать в условии целую подпрограмму в соответствии с правилами оформления командных списков. Эта особенность отличает язык командной оболочки от многих языков программирования, например языка Си, где есть понятие ''условное выражение''.
* По умолчанию, в командном списке анализируется только код самой последней команды этого списка. Для конвейеров анализируется только результат последней команды конвейера. ИСТИНОЙ в Bash считается нулевой возвращаемый код, а все другие являются ЛОЖЬЮ.
* Командный список, исполняемый на одной из ветвей не должен быть пустым. Пустой список является синтаксической ошибкой.
* Для <code>if</code> и <code>elif</code> признаком начала тела ветвления является ключевое слово <code>then</code>, и это обязательная часть синтаксиса языка. Концом ответвления является начало следующего, ключевое слово <code>fi</code>, либо <code>else</code>. Следующее за <code>then</code> слово интерпретируется как команда, поэтому при желании часть командного списка тела ветвления (или весь командный список), может находиться с <code>then</code> на одной строке и никак специально не разделяться.
* Ответвление <code>else</code> должно находиться всегда последним и следующая за <code>else</code> строка интерпретируется как команда. Для этого ответвления признаком конца является ключевое слово <code>fi</code>.
* Как и в любом другом языке программирования, с похожим оформлением ветвлений, ответвления <code>elif</code> и <code>else</code> являются не обязательными.
 
Абстрагируясь от командного наполнения, следующая запись демонстрирует синтаксически верную запись одной строкой.
 
<source lang=Bash>
if : ; then : ; elif : ; then : else : ; fi
 
# Многострочная запись №1
if :; then
:
elif :; then
:
else
:
fi
# Многострочная запись №2 (классическая)
if :
then
:
elif :
then
:
else
:
fi
</source>
 
Для демонстрации мы используем команду-пустышку <code>:</code>, которая всегда возвращает нулевой код и ничего не делает. По смыслу эта команда соответствует <code>pass</code> в Python. Мы вынуждены ее использовать в конструкциях ветвления, потому что командные списки не могут быть пустыми.
 
Ввиду того, что синтаксис ветвления в Bourne Shell был вдохновлен языком Алгол-68, а первые языки (такие как Алгол-68) испытывали тенденции быть похожими на английский язык, используется ключевое слово <code>then</code>, чтобы обозначить начало блока ветвления. В современных реалиях это уже кажется неудобным архаизмом, но исторически сложилось так, как сложилось. Чтобы немного облегчить ситуацию, рекомендуется писать командный список, являющийся условием, и <code>then</code> на одной строке, в частности, потому что о <code>then</code> легко забыть сразу после нажатия кнопки перевода строки.
 
<source lang=Bash>
if command_1; then
command
elif command_2; then
command
fi
</source>
 
=== Ветвление case...esac ===
 
Если во всех ветках ветвления используется условие типа ''сравнение с образцом'', то лучшим способом выразить ветвление является конструкция <code>case...esac</code>. Ее базовый синтаксис
<source lang=bash>
case <сравниваемое значение> in
<маска 1>) <командный список 1> ;;
<маска 2>) <командный список 2> ;;
...
esac
</source>
Эта конструкция эквивалентна
<source lang=bash>
if [[ <сравниваемое значение> == <маска 1> ]]; then
<командный список 1>
elif [[ <сравниваемое значение> == <маска 2> ]]; then
<командный список 2>
...
fi
</source>
 
Переменная сравнивается всегда в порядке перечисления масок. Другими словами, маски с более частными вариантами всегда следует размещать раньше более общих вариантов.
 
Заметим, что сравниваемое значение сравнивается с маской всегда лексикографически. По этой причине вы можете задействовать механизм globbing в масках. В масках не должно быть пробелов, если они не являются частью строки, другими словами, строки с пробелами должны быть закавычены. Напомним, что стандартный globbing поддерживает следующие маскирующие символы:
* <code>[]</code> — аналогичен произвольной группе в регулярных выражениях;
* <code>*</code> — любая последовательность символов. Аналогичен использованию <code>.*</code> в регулярных выражениях;
* <code>?</code> — любой единичный символ. Аналогичен <code>.?</code> в регулярных выражениях, но требует, чтобы символ всегда раскрывался.
 
Разрешается использовать символ вертикальной черты (<code>|</code>) для разделения разных вариантов в пределах одного ответвления. Это полезно, когда для разных вариантов требуется выполнить одно и то же действие.
<source lang=bash>
case $var in
a | b | c) <командный список> ;;
esac
 
# Эквивалентно
 
if [[ $var == a || $var == b || $var == c ]]; then
<командный список>
fi
</source>
 
Обратите внимание, что признаком конца ответвления являются два символа точки с запятой (<code>;;</code>). В Bash разрешается оставлять командные списки пустыми.
 
В Bash 4 есть интересная особенность в интерпретации этой конструкции, а именно, можно опускать признак конца ветвления <code>;;</code> для последнего ответвления. В этом случае, вероятно, признаком конца является ключевое слово <code>esac</code>.
<source lang=bash>
# В Bash это не ошибка
case $var in
 
*)
 
esac
</source>
Тем не менее, в оригинальном синтаксисе подстановка <code>;;</code> для обозначения конца ветвления является обязательной, поэтому для портируемости сценария следует игнорировать эту особенность в Bash.
 
В Bash сравниваемая переменная всегда интерпретируется буквально со всеми пробелами, поэтому ее можно не закавычивать, но в более старых оболочках ее значение может быть разбито по пробелам.
 
 
Примеры
<source lang=bash>
for line in a \
aaa \
bubble \
akaf \
ataf \
abaf \
rule \
apple \
pear \
grapes \
"+36 654 456 564" \
15 \
' '
do
case $line in
[[:space:]]) echo "Space" ;; # Можно использовать специальные идентификаторы
?) echo "Character: $line" ;; # Подходит: 'a'
???) echo "Three characters: $line" ;; # Подходит: 'aaa'
b*) echo "$line" ;; # Подходит: 'bubble'
a[bkt]af) echo "$line" ;; # Подходит: 'akaf ataf abaf'
rule) echo "$line" ;; # Прямое сравнение с 'rule'
apple | pear | grapes) echo "Fruit: $line" ;; # Разные варианты
"+36 654"*) echo "Telephone: $line" ;; # Строка с пробелами
[0-9][0-9]) echo "Number: $line" ;; # Любое двузначное число
*) echo "else: $line" ;; # Если ничего не совпало
esac
done
 
# Вывод
# Character: a
# Three characters: aaa
# bubble
# akaf
# ataf
# abaf
# rule
# Fruit: apple
# Fruit: pear
# Fruit: grapes
# Telephone: +36 654 456 564
# Number: 15
# Space
</source>
 
=== Другие способы выразить ветвление ===
 
Благодаря тому что в языке командной оболочки все является командами, мы можем выразить условную конструкцию по иному, не используя <code>if...fi</code>. Этот способ не дает особых преимуществ с точки зрения производительности, но позволяет сократить время на написание выражения. Это особенно полезно, когда блок ветвления однострочный.
 
Рассмотрим такой пример:
<source lang=Bash>
# Проверяем существование конфигурации. Если ее нет, то продолжать нет смысла.
if [[ -f some_configuration.cfg ]]; then
exit 1
fi
</source>
 
Такой участок кода можно встретить в начале функций или в самом начале сценариев. Вроде бы все хорошо, но после написания не одного десятка таких коротких проверок ключевое слово <code>then</code> начинает раздражать. Было бы здорово иметь другой способ обозначить блок, например фигурные скобки как в Си.
 
Так как <code>[[</code> является командой, которая возвращает коды, мы можем использовать операторы <code>&&</code> и <code>||</code>, чтобы управлять следованием исполнения. Таким образом, предыдущую проверку можно выразить так
<source lang=Bash>
[[ -f some_configuration.cfg ]] || exit 1 # Здесь используется позитивная проверка: ожидается 0 от команды [[
# в большинстве ситуаций.
# Или так
 
[[ ! -f some_configuration.cfg ]] && exit 1 # Здесь используется инверсия позитивной проверки.
 
# Примечание:
# В большинстве ситуаций следует отдавать предпочтение варианту с оператором ||, так как, очевидно,
# выполняется меньше инструкций при том же результате.
</source>
 
В этом примере мы выполняем только одно действие, когда нет файла, но на практике следует предупредить пользователя о том, почему исполнение прервалось. Добавим к нашей инструкции сообщение об ошибке.
 
<source lang=Bash>
# Обратите внимание, что мы объединяем последующие инструкции в блок. Эта
# запись предпочтительна, потому что ее легче читать и понимать.
[[ -f some_configuration.cfg ]] || {
echo "Error: Not found 'some_configuration.cfg'."
exit 1
}
 
# Тем не менее, тоже самое можно выразить и без блока. Но в таком случае легко
# допустить ошибку в последующем, если забыть поставить нужный оператор.
[[ -f some_configuration.cfg ]] ||
echo "Error: Not found 'some_configuration.cfg'." &&
exit 1
</source>
 
Предыдущий метод имеет смысл использовать, когда нужно выполнить 1–3 действия. Если действий больше, то особых преимуществ данный метод не дает, и имеет смысл записать все условие традиционным способом через конструкцию <code>if...fi</code>.
 
Иногда возникает искушение записать все одной строкой, например так
<source lang=Bash>
[[ -f some_configuration.cfg ]] || { echo "Error: Not found 'some_configuration.cfg'."; exit 1;}
</source>
''Обратите внимание на две тонкости в работе интерпретатора'':
* Закрывающая фигурная скобка обозначает только конец блока, но не командного списка, поэтому точка с запятой после последней команды при однострочной записи '''обязательна'''.
* В Bash разрешено использовать фигурные скобки в именах функций, поэтому '''необходим''' хотя бы один пробел после открывающей фигурной скобки блока, иначе интерпретатор не найдет начало блока и будет синтаксическая ошибка.
 
Аналогичным образом мы можем поступить, когда появляются альтернативная ветвь, но только одна. Такие конструкции могут показаться запутанными, но если нужно выполнить всего 1-2 действия, то они очень даже удобны, когда проверки носят чисто технологический характер.
<source lang=Bash>
# Пример конструкции типа if...else с одной альтернативной веткой.
# Блок для второй ветки обязателен, иначе инструкция exit 1 будет исполняться
# всегда.
[[ -f confisuration.xml ]] && {
echo "Info: Found configuration in xml format."
echo "Info: Trying to parse."
} || { echo "Error: Not found configuration" && exit 1;}
</source>
 
Можно выразить таким способом несколько ветвлений, но это очень не практично; легко допустить ошибку при дальнейшем развитии таких выражений. Следующий пример '''только для академических целей'''.
<source lang=Bash>
[[ -f cfg.xml ]] && {
echo "Info: Found configuration in xml format."
echo "Info: Trying to parse."
__parsed=0
} || echo "Warn: Not found configuration in xml format. Trying to find another." &&
[[ ! -f cfg.xml && -f cfg.json ]] && {
echo "Info: Found configuration in json format."
echo "Info: Trying to parse."
__parsed=0
} && echo "Configuration has been parsed." || [[ ! -z $__parsed && $__parsed -eq 0 ]] ||
{ echo "Error: Configuration is not found."; exit 1;}
 
# Примечания:
# - Здесь используется условная переменная __parsed, чтобы отрезать вывод самого последнего сообщения,
# если одна из ветвей все же исполнилась. По другому, к сожалению, здесь сделать нельзя, если нам
# нужно выводить предупреждение.
# - В условных конструкциях мы должны исключать условия из предыдущих веток, чтобы пресечь исполнение
# следующих.
#
</source>
 
== Циклы ==