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

Содержимое удалено Содержимое добавлено
Нет описания правки
Переделано в многостраничный учебник
Метка: замена
Строка 1:
{{{{Book template}}/Содержание}}
{{wikipedia|bash}}
__NOTOC__
{{Заглавие книги|Практическое написание сценариев командной оболочки Bash}}
В этом учебнике мы рассмотрим язык сценариев командной оболочки Bash. Отличительной особенностью этого учебника заключается в том, что мы не будем сухо пересказывать документацию к командной оболочке, а попытаемся рассказать о языке с практической стороны.
Строка 25 ⟶ 27 :
 
Еще Bash имеет плохую производительность и не может полноценно выполнять арифметические операции, но учитывая специфику задач, которые он решает, это не так уж и важно. Если вам нужны более производительные сценарии, которые требуют также обработки сложных структур данных, следует отдавать предпочтение более продвинутым скриптовым языкам, например ''Perl''.
 
== Команда test ==
 
Команда ''test'' служит для того, чтобы эмулировать условные выражения в языке командной оболочки. Самая первая реализация test была отдельной программой, которой передавалась условная конструкция. По договоренности, эта программа возвращала 0, если переданное условие истинно, и 1 — если ложно.
 
Со временем команда test стала встроенной в интерпретатор. Так как все сценарии изобилуют условными проверками, команда получила более короткий псевдоним в виде квадратной скобки — <code>[</code>. В таком виде команда описана в POSIX. Однако у оригинальной команды test специфичный перегруженный синтаксис, который часто приводит к ошибкам у начинающих программистов. В Bash поддерживается POSIX-совместимый test и вводится его усовершенствованная версия в виде оператора <code>[[</code>. Далее по тексту, под командой test мы будем понимать как <code>[</code>, так и <code>[[</code>, если не требуется уточнение.
 
Если вы пишите не портируемые сценарии, исключительно для оболочки Bash, то следует отдавать предпочтение модернизированной версии test, так как она в целом удобнее. Тем не менее, следует помнить, что в портируемых сценариях вы должны пользоваться только POSIX совместимым вариантом. Вариант <code>[[</code> кроме Bash так же поддерживается в Ksh и Zsh.
 
В этом разделе мы рассмотрим особенности работы модернизированной команды test.
 
=== Сравнение [ и [[ ===
{| style="width:100%"
|- style="background-color: #ffe9a8;"
![
!<nowiki>[[</nowiki>
|-
|style="width: 50%;"|
<source lang=bash>
# Сравнивание строк
[ a \> b ]
[ a \< b ]
[ a = b ]
[ a != b ]
</source>
|style="width: 50%;"|
<source lang=bash>
# Сравнивание строк
[[ a > b ]]
[[ a < b ]]
[[ a = b ]] или [[ a == b ]]
[[ a != b ]]
</source>
|-
|style="width: 50%;"|
<source lang=bash>
# Сравнивание чисел
[ 5 -gt 10 ]
[ 5 -lt 10 ]
[ 5 -ge 10 ]
[ 5 -le 10 ]
[ 5 -eq 10 ]
[ 5 -ne 10 ]
</source>
|style="width: 50%;"|
<source lang=bash>
# Сравнивание чисел
[[ 5 -gt 10 ]] # больше
[[ 5 -lt 10 ]] # меньше
[[ 5 -ge 10 ]] # больше или равно
[[ 5 -le 10 ]] # меньше или равно
[[ 5 -eq 10 ]] # равно
[[ 5 -ne 10 ]] # не равно
</source>
|-
|style="width: 50%;"|
<source lang=bash>
# Конъюнкция и дизъюнкция
[ -n "$var" -a -f "$var" ]
[ -n "$var" -o -f "$var" ]
 
# Примечание:
# Важно ставить кавычки для подставляемых
# переменных, если в их значениях есть
# пробелы.
</source>
|style="width: 50%;"|
<source lang=bash>
# Конъюнкция и дизъюнкция
[[ -n $var && -f $var ]]
[[ -n $var || -f $var ]]
 
# Примечание:
# Кавычки ставить можно, но не обязательно:
# работает одинаково.
#
</source>
|-
|style="width: 50%;"|
<source lang=bash>
# Группировка условий
[ "$var" -eq 5 -a \( "$var1" -eq 6 -o "$var1" -eq 7 \) ]
</source>
|style="width: 50%;"|
<source lang=bash>
# Группировка условий
[[ $var -eq 5 && ( $var1 -eq 6 || $var1 -eq 7 ) ]]
</source>
|-
|style="width: 50%;"|
<source lang=bash>
# Регулярные выражения и маскирование
# не поддерживаются
#
</source>
|style="width: 50%;"|
<source lang=bash>
# Регулярные выражения и маскирование
[[ $name = a* ]] или [[ $name == a* ]]
[[ $name =~ ^John$ ]]
</source>
|}
 
;Обратите внимание
* Для старой версии test следует осторожно использовать операторы лексикографического сравнивания <code>\></code> и <code>\<</code>, так как они являются одним из расширений стандарта POSIX. Другими словами, команда их может не поддерживать.
* Для старой версии test также следует осторожно использовать операторы <code>-a</code>, <code>-o</code> и группировку, так как в POSIX помечено, что это устаревшие возможности. Вместо них POSIX рекомендует использовать несколько вызовов test с объединением их операторами <code>&&</code> и/или <code>||</code>.
* Внутри <code>[[</code> не производится разбиение по разделителю <code>IFS</code> и ''Globbing'', поэтому подстановку переменных не обязательно закавычивать.
 
=== Особенности использования test ===
 
В старых сценариях часто можно встретить такую запись
<source lang=bash>
[ x"$var1" = x"$var2" ]
 
# или
 
[ "x$var1" = "x$var2" ]
</source>
 
Это обусловлено тем, что если переменная не определена, то она раскрывается в пустоту. С точки зрения команды test пустота не является аргументом, и если мы опустим символ <code>x</code> (или любой другой), то в результате команда будет выглядеть так после подстановки значений (в следующем примере мы предполагаем, что не определена <code>var1</code>)
<source lang=bash>
var2=a_word
[ = a_word ] # если не определена var1
</source>
 
Это является синтаксической ошибкой, поэтому раньше использовали некоторый символ, который оставался на позиции, если переменная не раскрывалась в какое-то значение. В современных интерпретаторах допустимо опускать этот произвольный символ, но нужно обязательно кавычить переменные.
 
<source lang=bash>
[ "$var1" = "$var2" ] # Правильно, но с очень старыми интерпретаторами может не заработать,
# если одна из переменных окажется не проинициализирована.
</source>
 
При использовании модернизированной версии test можно не кавычить обращения к переменной, но при этом нужно гарантировать, что переменная будет проинициализирована до исполнения проверки условия
 
<source lang=bash>
[[ $var1 == $var2 ]] # Правильно, если инициализация гарантирована
 
# Примечание:
# Это конструкция работает всегда в Bash, по меньшей мере с версии 4.4.19(1)-release.
</source>
 
Если потенциально возможно такое, что переменная в условии может быть раскрыта в пустоту, следует использовать подстановку со значением по умолчанию
<source lang=bash>
[[ ${var1:-} == ${var2:-} ]] # Работает всегда
</source>
 
В Bash 4 модернизированный test умеет самостоятельно обращаться к переменным по их именам, если над ними не выполняются лексикографические операции.
<source lang=bash>
NUMBER=5
 
# В этом примере команда test сама попытается раскрыть переменную NUMBER.
if [[ NUMBER -ge 0 ]]; then
echo "The number is $NUMBER"
fi
# Это аналогично следующей записи.
if [[ $NUMBER -ge 0 ]]; then
echo "The number is $NUMBER"
fi
# Если переменная не определена, то она раскроется в пустоту.
# Но для операторов сравнений чисел пустота считается нулем, поэтому следующий
# код отработает.
unset NUMBER
if [[ NUMBER -ge 0 ]]; then
echo "The number is $NUMBER"
fi
# Вывод:
# The number is
</source>
Этой возможностью следует пользоваться аккуратно. Вообще, рекомендуется быть последовательным и не забывать про символ доллара для любых переменных.
 
=== Сравнивание строк и чисел ===
 
В языке командной оболочки всего один тип данных — строковый. За интерпретацию аргументов как чисел полностью отвечает команда, которой они передаются.
 
Вы можете видеть, что для сравнивания чисел у test есть свои операторы. Без этих операторов числа сравниваются как строки, т.е. используются коды символов ASCII кодировки, а не сами числа. Приведем несколько поучительных примеров.
 
Следующее выражение будет выполняться всегда корректно
<source lang=bash>
[[ 5 == 5 ]]
</source>
 
Тем не менее, сравниваются не сами числа, а код символа 5 в таблице кодировки ASCII. Таким образом, оператор <code>==</code> будет всегда работать так же как <code>-eq</code> для чисел, однако, формально так делать не желательно.
 
Теперь рассмотрим такое выражение
<source lang=bash>
[[ 99 > 888 ]] # Возвращает ИСТИНУ
</source>
Казалось бы очевидно, что 99 меньше 888, но такая проверка будет возвращать ИСТИНУ. Связано это с тем, что лексикографически строка 99 больше 888 (код символа 9 в таблице кодировки ASCII больше 8).
 
Если вы хотите сравнивать числа как числа, а не строки, следует использовать один из операторов команды test: <code>-gt, -ge, -lt, -le, -eq, -ne</code>. Таким образом предыдущий пример следует писать так
<source lang=bash>
[[ 99 -gt 888 ]] # Возвращает ЛОЖЬ, как и ожидалось
</source>
 
В Ksh, Bash и Zsh также есть еще одна разновидность команды test, которая может работать только с числами. В этой версии все операторы работают с числами, как это ожидается для чисел, но в портируемых скриптах этой возможностью пользоваться нельзя.
<source lang=bash>
(( 5 == 5 ))
(( 4 > 3 ))
(( 3 < 4 ))
(( 21 >= 25 ))
(( 100 <= 255 ))
(( 6 != 5 ))
</source>
 
== Ветвления ==
Ветвления в 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>
 
== Циклы ==
 
В Bash имеется 4 вида циклов:
* цикл <code>for</code>;
* цикл <code>while</code>;
* цикл <code>until</code>;
* цикл <code>select</code>.
 
Цикл <code>for</code> удобен для перебора конечных множеств (списков слов; простых массивов, которые могут быть преобразованы в списки слов; ассоциативных массивов через их ключи).
 
Циклы <code>while</code> и <code>until</code> используются, когда число итераций нам заранее не известно, но известно условие остановки, которое должно рано или поздно выполниться от действий в теле цикла. Как частный случай, эти циклы могут быть использованы для создания ''бесконечного цикла''.
 
Цикл <code>select</code> является не портируемым циклом, который облегчает создание меню выбора в интерактивных сценариях. В принципе, он может не использоваться, так как его функциональность можно запрограммировать с помощью <code>while</code> и <code>until</code>, но в нем уже реализованы многие проверки, что немного экономит время.
 
Вместе с циклами идут два управляющих слова: <code>break</code> и <code>continue</code>. Команда <code>break</code> позволяет вам прервать цикл в текущей итерации в некоторой точке цикла и перенести точку следования программы на строку, следующую за циклом. Команда <code>continue</code> позволяет прервать исполнение текущей итерации и перенести точку исполнения в начало цикла. В Bash у этих команд нет аргументов.
 
Ниже мы рассмотрим некоторые полезные приемы использования этих циклов.
 
=== Цикл for ===
 
Задокументированный синтаксис цикла имеет следующий вид
<source lang=bash>
for <NAME> in <WORDS>; do
<LIST>
done
</source>
Здесь <code><NAME></code> имя переменной, которая работает как ссылка на текущий элемент из списка слов <code><WORDS></code>. После каждого прогона список смещается на одну позицию влево, удаляя предыдущий элемент. Цикл будет продолжаться до тех пор, пока в результате смещения цикл не обнаружит следующего элемента.
 
Обратим внимание, что переменная будет создана циклом в глобальной области видимости, т.е. потенциально она может затереть уже существующую переменную. По этой причине мы рекомендуем именовать ее всегда в нижнем регистре, так как это убережет вас от скрытых ошибок, если вы придерживаетесь правила именовать глобальные переменные в верхнем регистре, а локальные в нижнем.
 
Списком слов является строка из слов, разделенных (по умолчанию) пробелом и/или символом табуляции и/или символом переноса строки, причем символы-разделители должны быть частью этого списка. Вообще список слов разделяется по символам, записанным в переменной окружения <code>IFS</code>. Временно редактируя эту переменную, вы можете управлять процедурой получения списка слов.
 
Список не обязательно должен быть заранее известен: его может подготавливать некоторая команда.
<source lang=bash>
declare counter=0
 
# Передача списка прямым образом
for entry in word1 word2 word3 word4; do
echo "$(( counter += 1 )): '$entry'"
done
# Вывод:
# 1: 'word1'
# 2: 'word2'
# 3: 'word3'
# 4: 'word4'
 
# Передача списка скобочной подстановкой
for entry in {word1,word2,word3,word4}; do
echo "$(( counter += 1 )): '$entry'"
done
# Вывод:
# 5: 'word1'
# 6: 'word2'
# 7: 'word3'
# 8: 'word4'
 
 
# Список передает команда find
for entry in $(find / -maxdepth 1 -type d); do
echo "$(( counter += 1 )): '$entry'"
done
 
# Вывод:
# 9: '/'
# 10: '/bin'
# 11: '/boot'
# 12: '/dev'
# 13: '/etc'
# 14: '/home'
# 15: '/lib'
# 16: '/lib64'
# 17: '/media'
# 18: '/mnt'
# 19: '/opt'
# 20: '/proc'
# 21: '/root'
# 22: '/run'
# 23: '/sbin'
# 24: '/snap'
# 25: '/srv'
# 26: '/sys'
# 27: '/tmp'
# 28: '/usr'
# 29: '/var'
</source>
 
Можно опускать список слов, тогда <code>for</code> по умолчанию просматривает <code>$@</code>.
 
<source lang=bash>
func() {
# Эквивалентно for arg in "$@"; do ...
for arg; do
declare -i counter
echo "arg $(( counter += 1 )): '$arg'"
done
}
 
func a b c d
# Вывод:
# arg 1: 'a'
# arg 2: 'b'
# arg 3: 'c'
# arg 4: 'd'
 
# Примечание:
# Разумеется, если цикл запускается не в функции, то будут браться аргументы всего сценария.
#
</source>
 
Цикл <code>for</code> не одинаково обрабатывает <code>$@</code> и <code>$*</code>, если они подставлены явно. Напомним, что <code>$@</code> это массив, составленный из аргументов сценария/функции, а <code>$*</code> это строка, составленная из аргументов сценария/функции. Если в одном из аргументов есть символы, по которым <code>for</code> будет разбивать список (например пробелы), то <code>$*</code> этого не заметит, в результате чего у вас появятся мнимые аргументы. Напротив, <code>"$@"</code> (двойные кавычки обязательны), такие ситуации будет различать. Без двойных кавычек <code>$@</code> будет работать как <code>$*</code>.
 
Сравните:
<source lang=bash>
correct() {
for arg in "$@"; do
declare -i counter
echo "arg $(( counter += 1 )): '$arg'"
done
}
 
wrong() {
for arg in $*; do
declare -i counter
echo "arg $(( counter += 1 )): '$arg'"
done
}
 
correct one two 'forty three'
echo '---------------------'
wrong one two 'forty three '
</source>
Результат:
<source lang="html4strict">
arg 1: 'one'
arg 2: 'two'
arg 3: 'forty three'
---------------------
arg 1: 'one'
arg 2: 'two'
arg 3: 'forty'
arg 4: 'three' <-- мнимый аргумент
</source>
 
 
Цикл <code>for</code> возвращает статус последней команды командного списка, если произошла хотя бы одна итерация, иначе возвращается ноль. Этим конечно сложно пользоваться для проверок, но костыльный способ есть (здесь он приводится только для примера)
<source lang=bash>
func() {
declare -i counter
for arg; do
echo "arg $(( couter += 1 )): '$arg'"
! :
done && echo "Function got nothing" || echo "Function got some arguments"
}
 
func
echo '-------------------------'
func a b c
 
# Вывод:
# Function got nothing
# -------------------------
# arg 1: 'a'
# arg 2: 'b'
# arg 3: 'c'
# Function got some arguments
</source>
 
==== Работа с переменной IFS ====
 
Переменная <code>IFS</code> (''Input Field Separator'') используется встроенными командами интерпретатора, чтобы разбивать входящий поток символов на отдельные слова. В языке командной оболочки можно задать сразу несколько символов, которые будут считаться разделителями слов и все они должны храниться в этой переменной. По умолчанию, переменная инициализирована тремя идущими подряд символами: символ пробела (<code>0x20</code>), символ табуляции (<code>0x09</code>) и символ переноса строки (<code>0x0A</code>).
 
Разные команды по-разному используют эту переменную. Например, цикл <code>for</code> разделяет строку на подстроки по всем символам, указанным в <code>IFS</code> и дополнительно режет их вокруг каждой подстроки (''trimming''); команда <code>read</code> использует последний символ <code>IFS</code> как разделитель строк, а все остальные для процедуры ''trimming''.
 
Часто в пользовательских строках используются иные разделители, по которым строку нужно разбивать: например переменная <code>PATH</code> отделяет свои элементы двоеточием. Если вы попытаетесь передать <code>PATH</code> на место списка как есть, то ничего само не разделится.
 
Иногда вам придется редактировать переменную <code>IFS</code> и это очень опасная операция, так как она может приводить к большому количеству скрытых ошибок, так как все последующие команды сценария будут работать с учетом текущего значения <code>IFS</code>. Большинство команд защищено от <code>unset IFS</code> и используют встроенные в реализацию значения по умолчанию.
 
Тем не менее, при любом изменении этой важной переменной не забывайте сохранять ее текущее значение и вовремя восстанавливать, например так
<source lang=bash>
OLD_IFS=$IFS
IFS=':'
# ... ваш код
IFS=$OLD_IFS
</source>
 
Давайте попробуем обработать каждый элемент переменной <code>PATH</code> по отдельности
<source lang=bash>
OLD_IFS=$IFS
IFS="${IFS}:"
 
for entry in $PATH; do
echo $entry
done
 
IFS=$OLD_IFS
 
# Вывод:
# /usr/local/sbin
# /usr/local/bin
# /usr/sbin
# /usr/bin
# /sbin
# /bin
#
# ... и так далее
#
</source>
 
В этом примере мы добавили еще один разделитель <code>:</code> в конец существующего списка разделителей. Обратите внимание, что после того как нас удовлетворил результат, мы вернули переменной <code>IFS</code> прежнее значение.
 
==== Перебор массивов ====
 
Так как массивы имеют конечный размер, их перебор циклом <code>for</code> является наилучшим решением. Перебор простых массивов может быть реализован через индексы, обычно, когда вам не нужно перебирать их целиком.
 
<source lang=bash>
declare -a fruits=('apple' 'pear' 'banana' 'Peruvian cherry' 'orange' 'grapes' 'pineapple')
 
# Перебор массива целиком
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
 
# Перебор массива целиком, но с применением индексов
for index in ${!fruits[@]}; do
echo ${fruits[$index]}
done
 
# Перебор части массива
# Напомним, что нумерация в массивах начинается с 0, поэтому 2
# здесь соответствует 'banana', а $(( ${#fruits[@]}-2 )) соответствует 'grapes'
for index in $(seq 2 $(( ${#fruits[@]}-2 )) ); do
echo ${fruits[$index]}
done
 
# Примечание:
# Команда seq генерирует список из чисел.
</source>
 
Массив можно преобразовать в список двумя способами: <code>${fruits[@]}</code> и <code>${fruits[*]}</code>. Без кавычек разницы нет никакой, но вариант <code>"${fruits[@]}"</code> позволяет учесть пробельные символы внутри самих элементов массива. Так, в нашем примере в массиве с фруктами без кавычек, элемент <code>'Peruvian cherry'</code> разделился бы на два, что неправильно. Вариант <code>"${fruits[*]}"</code> вернет массив одной строкой без разделения.
 
Для перебора ассоциативных массивов необходимо преобразовать в список их ключи.
<source lang=bash>
declare -A mesg=(
[date]=$(date --rfc-3339='seconds')
[message]="Hello."
[author]="John Smith"
[additional comment]="urgent"
)
 
# Примечание: ключи автоматически сортируются.
 
for key in "${!mesg[@]}"; do
echo "Key: $key Value=${mesg[$key]}"
done
</source>
 
Опять же, чтобы сохранить пробелы в именах ключей вы должны использовать вариант с кавычками <code>"${!mesg[@]}"</code>, иначе в списке окажется два несуществующих ключа. На практике обычно пробелы в ключах не используют, но лучше лишний раз перестраховаться.
 
==== Цикл с заданным числом повторений ====
 
Цикл <code>for</code> может быть использован, чтобы сделать что-нибудь несколько раз подряд. Например, можно сделать функцию опроса, у которой ограниченное число попыток. Другой пример: иногда генератор случайных чисел сбрасывают через передачу ему запроса на генерацию случайного числа несколько раз подряд. В этом случае такой цикл мог бы пригодиться.
 
Для генерации повторений обычно просто генерируют список из чисел одним из следующих способов.
 
<source lang=bash>
declare RETRY_TIMES=5
 
do_something() {
echo "retry: $1"
}
 
# Генерация списка через скобочную подстановку
for retry in {1..5}; do
do_something $retry
done
 
echo "------------------"
 
# Генерация списка через скобочную подстановку, когда одна из границ
# меняется. Этот метод небрежный: рекомендуется использовать функцию seq.
for retry in $(eval echo {1..$RETRY_TIMES}); do
do_something $retry
done
 
echo "------------------"
 
# Генерация списка через функцию seq.
for retry in $(seq 1 $RETRY_TIMES); do
do_something $retry
done
</source>
 
==== Запись цикла for в стиле языка Си ====
 
В Bash есть еще один вариант записи цикла <code>for</code>, который он унаследовал от Ksh. Этот стиль используется, чтобы записать инкрементируемую переменную и условие выхода из цикла в одну строку, как это было придумано в языке Си. До этого, чтобы такое проделывать, использовался цикл <code>while</code>.
 
Общий синтаксис таков:
<source lang=bash>
for (( <EXPR1> ; <EXPR2> ; <EXPR3> )); do
<LIST>
done
</source>
 
Точку с запятой после закрывающей скобки можно опускать, потому что <code>(( <EXPR1> ; <EXPR2> ; <EXPR3> ))</code> полностью самостоятельная конструкция, а не командный список. Тем не менее, для единообразия этого лучше не делать.
 
* на позиции <code><EXPR1></code> пишется одна или несколько инициализирующих переменных, перечисленных через запятую. Эта часть выполняется один раз до самой первой итерации;
* на позиции <code><EXPR2></code> пишется условие продолжения цикла, т.е. пока условие истинно, то цикл продолжается. Эта часть выполняется перед каждой итерацией;
* на позиции <code><EXPR3></code> пишется выражение, влияющее на условие, которое выполняется в конце каждой итерации.
 
<source lang=bash>
# Посчитать от 0 до 4
for (( i=0; i < 5; i++ )); do
echo "$i"
done
 
# Посчитать от 10 до 0
for (( i = 10; i >= 0; i-- )); do
echo "$i"
done
 
# Вывести только четные числа в промежутке от 0 до 10
for (( i=0; i <= 10; i+=2 )); do
echo "$i"
done
 
# Вывести числа вертикальной змейкой
for (( incr = 1, n=0, times = ${2:-4}, step = ${1:-5}; (n += incr) % step || (incr *= -1, --times);)); do
printf '%*s\n' "$((n+1))" "$n"
done
 
#Вывод:
# 1
# 2
# 3
# 4
# 5
# 4
# 3
# 2
# 1
#0
# 1
# 2
# 3
# 4
# 5
# 4
# 3
# 2
# 1
</source>
 
Эта запись цикла автоматически делает ваш сценарий не портируемым. Данная запись поддерживается в Ksh, Bash и Zsh. Во всех этих интерпретаторах у нее одинаковый синтаксис.
 
=== Циклы while и until ===
 
Циклы <code>while</code> и <code>until</code> похожи. Они используются, когда число повторений выражается условием остановки. Базовый синтаксис обоих циклов в целом похож, разница состоит только в интерпретации кода возврата последней команды командного списка условия.
 
<source lang=bash>
[ while | until ] <LIST1> ; do
<LIST2>
done
</source>
* Цикл <code>while</code> выполняется (т.е. исполняет командный список <code><LIST2></code>) до тех пор, пока последняя команда списка <code><LIST1></code> возвращает нулевой код, т.е. ИСТИНУ. Напротив, цикл <code>until</code> выполняется до тех пор, пока последняя команда списка <code><LIST1></code> возвращает не нулевой код, т.е. ЛОЖЬ. Можно использовать такой мнемонический прием: <code>while</code> повторяет свои действия пока не сломается, а <code>until</code> — пока не получится.
* Оба цикла возвращают 0, если ни одной итерации не происходило, иначе они возвращают код последней команды списка <code><LIST2></code> последней исполненной итерации.
* Исполнением этих циклов часто управляют из <code><LIST2></code> с помощью команд <code>continue</code> и <code>break</code>.
 
Следующий пример сложен для понимания, но в нем показано, что в условии может использоваться целая процедура.
 
<source lang=bash>
declare RETRY_TIMES=6
 
while : $(( RETRY_TIMES -= 1 )); [[ $RETRY_TIMES -gt 0 ]] &&
echo -n "Retry $RETRY_TIMES: " ||
echo "While has finished its work."; [[ $RETRY_TIMES -gt 0 ]]
do
echo "Do very important things"
done
 
# Вывод:
# Retry 5: Do very important things
# Retry 4: Do very important things
# Retry 3: Do very important things
# Retry 2: Do very important things
# Retry 1: Do very important things
# While has finished its work.
</source>
 
В предыдущем примере в <code><LIST1></code> записано три команды:
* <code>: $(( RETRY_TIMES -= 1 ));</code> — уменьшает счетчик повторений в начале новой итерации.
* <code>[[ $RETRY_TIMES -gt 0 ]] && echo -n "Retry $RETRY_TIMES: " || echo "While has finished its work.";</code> — делает печать в начале каждой итерации.
* <code>[[ $RETRY_TIMES -gt 0 ]]</code> — именно код этой команды анализируется, чтобы прервать цикл.
 
Аналогично тот же алгоритм можно реализовать через цикл <code>until</code>.
<source lang=bash>
declare RETRY_TIMES=6
 
until : $(( RETRY_TIMES -= 1 )); [[ $RETRY_TIMES -gt 0 ]] &&
echo -n "Retry $RETRY_TIMES: " ||
echo "While has finished its work."; [[ $RETRY_TIMES -le 0 ]]
do
echo "Do very important things"
done
 
# Примечание:
# Мы просто поменяли последнюю команду на [[ $RETRY_TIMES -le 0 ]].
</source>
 
Обычно придумывать условие в негативном ключе <code>until</code> сложнее, чем в позитивном для <code>while</code>, поэтому цикл <code>until</code> встречается реже.
 
==== Бесконечный цикл ====
 
Бесконечный цикл обычно используется, когда сценарий прерывается асинхронным событием (например сигналом), либо когда условие выхода строго не детерминировано по времени. Также когда условий выхода несколько или само условие слишком сложное, чтобы его выразить в <code><LIST1></code>.
 
Сделать бесконечный цикл можно как на основе <code>while</code>, так и на основе <code>until</code>.
 
<source lang=bash>
# Первый способ
while :; do
# Программа, выполняемая бесконечно
done
 
# Второй способ
# Можно вызвать команду true (эквивалентно ':')
while true; do
# Программа, выполняемая бесконечно
done
 
# Третий способ
until ! :; do
# Программа, выполняемая бесконечно
done
 
# Четвертый способ
# Можно вызвать команду false
until false; do
# Программа, выполняемая бесконечно
done
</source>
 
При программировании бесконечных циклов следует быть предельно внимательным во время написания условий выхода из них. Вы должны помнить, что процесс, попавший в петлю ненарочно, потребляет ненужное процессорное время.
 
 
Бесконечный цикл можно оформить также циклом <code>for</code> в стиле Си
<source lang=bash>
for (( ; ; )); do
# Программа, выполняемая бесконечно
done
</source>
но такой вариант '''крайне не рекомендуется'''.
 
==== Цикл в стиле do...while ====
 
В Bash нет цикла в стиле <code>do...while</code>, но его при желании можно эмулировать. Есть несколько подходов сделать это.
 
Первый, самый очевидный, делать проверку условия в самом низу тела цикла, а сам цикл сделать бесконечным.
 
<source lang=bash>
while :; do
do_something
# ...
[[ condition ]] || break
done
</source>
 
Пример
 
<source lang=bash>
do_something() { echo "$*"; : $(( it += 1 ));}
 
it=0
while :; do
do_something "$it"
(( it < 5 )) || break
done
</source>
 
Второй метод более экзотический, но тем не менее рабочий. Достаточно <code><LIST2></code> поместить на место <code><LIST1></code>, а тело цикла сделать пустым.
 
<source lang=bash>
do_something() { echo "$*"; : $(( it += 1 ));}
 
it=0
while
do_something "$it"
(( it < 5 ))
do
:
done
</source>
 
В обоих примерах результат будет одинаковый.
 
=== Цикл select ===
 
Цикл <code>select</code> используется, чтобы облегчить составление меню выбора в различных интерактивных сценариях. Сейчас эту функцию выполняют диалоговые окна в современных оконных системах.
 
Этот цикл вы будете использовать не особо часто, потому что в сценариях стараются заложить максимум автоматизма, а нужные параметры следует передавать не через пользователя, а через конфигурацию. Тем не менее, когда конфигурации нет, то можно попытаться что-то спросить у пользователя.
 
Общий цикл команды такой
<source lang=bash>
select <NAME> in <WORDS>; do
<LIST>
done
</source>
* В параметре <code>WORDS</code> вы передаете список из возможных вариантов ответов. Этот список можно опустить, тогда будет использован <code>"$@"</code> подобно тому, как это делается в цикле <code>for</code>. Если список будет пустым, то цикл целиком пропустится.
* Когда дело доходит до цикла, то исполнение сценария блокируется в ожидании пользовательского ввода, при этом цикл сам выведет меню из возможных вариантов ответов в виде списка и приглашение ввода.
* Выход из цикла нужно предусмотреть в его теле. Номер варианта из меню, который ввел пользователь, сохраняется в переменной <code>$REPLY</code>. Если этот номер валидный, то в переменную <code>NAME</code> будет записано фактическое значение варианта, иначе цикл перезапустится. Цикл перезапускается даже, если пользователь выбрал валидный номер, но с той разницей, что <code>NAME</code> инициализируется валидным значением. Именно поэтому выход из цикла должен программироваться в его теле.
* Варианты нумеруются начиная с единицы, слева направо.
 
Ниже представлен небольшой пример.
<source lang=bash>
#!/bin/bash
# Файл: restart.sh
echo "Do you want restart your system?"
select answer in "yes" "no"; do
answer=${answer,,} # чтобы не зависеть от регистра
if [[ $answer == 'yes' ]]; then
echo "Starting to restart..."
elif [[ $answer == 'no' ]]; then
echo "Continue."
fi
case "$REPLY" in
[1-2]) break ;;
*) echo "Wrong choice. Please, try again." ;;
esac
done
</source>
 
<source lang=html4strict>
$ restart.sh
Do you want restart your system?
1) yes
2) no
#? oops
Wrong choice. Please, try again.
#? 1
Starting to restart...
 
$ restart.sh
Do you want restart your system?
1) yes
2) no
#? 2
Continue.
</source>
 
== Функции ==
 
Функция в языке командной оболочки это именованная часть кода, к которой можно обратиться в любой точке сценария после ее объявления. Функция в сценарии может быть объявлена только один раз, при этом повторное определение считается синтаксической ошибкой, а не переопределением. Существует техника, при которой можно переопределить функцию (при желании несколько раз) в одном сценарии, о которой будет рассказано ниже.
 
Каждая функция должна иметь уникальное имя, которое не должно начинаться с цифры и не должно содержать в своем имени круглых скобок, а также совпадать с зарезервированными словами интерпретатора. Современные интерпретаторы не накладывают ограничений даже на кодировку, поэтому вы можете объявлять функции хоть на кириллице.
 
Код объявленной функции никак не интерпретируется до момента её явного вызова, т.е. интерпретатор, когда встречает определение функции, резервирует за ней идентификатор и помечает точку перехода в ее начало. Тем не менее, в теле функций не должно быть явных синтаксических ошибок (как бы выразились в компилируемых языках — в функции не должно быть ошибок времени компиляции).
 
Разные shell-интерпретаторы по разному парсят функции. Например, при всей похожести правил объявления функций в Ksh и Bash, в Ksh фигурные скобки после имени функции это часть синтаксиса интерпретатора, тогда как в Bash они, вообще говоря, не обязательны и служат только, чтобы поместить командный список в блок.
 
=== Объявление функций ===
 
В Bash заложена обратная совместимость с Ksh, поэтому в Bash существует минимум три способа объявить функцию. Тем не менее, рекомендуется всегда быть последовательным и в пределах одного сценария придерживаться одного варианта, либо использовать только POSIX-совместимый вариант.
<source lang=Bash>
# POSIX-совместимый синтаксис. Рекомендован к постоянному использованию из-за портируемости.
 
func_name() {
return 0
}
# Примечания:
# * тело функции не должно быть пустым, иначе это
# синтаксическая ошибка.
# * Из практических соображений рекомендуется открывающую фигурную
# скобку оставлять на той же строке, что и имя функции.
#
 
# Следующий синтаксис появился в Ksh и в Bash он поддерживается.
 
function func_name {
return 0
}
 
# Примечание:
# Обратите внимание, что в этом случае для Bash признаком функции выступает ключевое
# слово function, тогда как в POSIX совместимой записи для этого служили круглые
# скобки.
# Тем не менее, стили не запрещается мешать: в этом случае Bash будет один из признаков
# просто игнорировать.
 
function func_name() {
return 0
}
 
# В непортируемых сценариях вы можете пользоваться ключевым словом function, так как он
# заметно выделяет сигнатуры функций в больших сценариях.
#
</source>
 
Compound-выражение — это сложное выражение, которое либо использует командный список как свое тело, либо имеет особый синтаксис, интерпретируемый единым куском.
 
Признаками такого выражения служат:
* ключевые слова, которые начинают и заканчивают такое выражение;
* возможность перенаправить вывод этого выражения как единого целого.
 
Ниже приведены примеры таких выражений:
* все циклы;
* конструкция ветвления <code>if..fi</code>;
* конструкция ветвления <code>case..esac</code>;
* команда <code>test</code>;
* круглые скобки в контексте выполнить командный список в подоболочке;
* командный список в блоке.
Синтаксис Bash, вообще говоря, требует, чтобы тело функции было compound-выражением. Это означает, что фигурные скобки в Bash не являются частью синтаксиса, потому что блок порождает compound-выражение.
 
<source lang=bash>
# Примеры однострочных функций с одним compound выражением.
 
# Следующая функция просто проверяет значение переданной переменной
# на равенство строке 'yes' без привязки к регистру.
isYes() [[ "${1,,}" == 'yes' ]]
 
read -p "Enter 'yes' or 'no': "
isYes "$REPLY" && echo "Entered 'yes'." || echo "Entered '$REPLY'"
 
# Следующий пример демонстрирует простой принтер. Обратите внимание, что
# интерпретатор после имени функции пытается найти первое compound-выражение
# (в нашем случае это цикл), игнорируя пустые строки. Само выражение может начинаться
# с той же строки или с последующих, потому что у циклов есть ключевые слова,
# показывающие начало и конец конструкции.
say()
for option in "$@"; do
case $option in
-i | --info) printf "[Info]: " ;;
-w | --warn) printf "[Warn]: " ;;
-e | --error) printf "[Error]: " ;;
*) printf "$option\n" ;;
esac
done
 
say -i "Hello" # [Info]: Hello
say -w "Undefined identifier" # [Warn]: Undefined identifier
say --error "Something goes wrong" # [Error]: Something goes wrong
say "Bla-Bla-Bla" # Bla-Bla-Bla
 
# Примечание:
# В предыдущем примере мы используем не совсем одно compund-выражение: здесь ветвление
# case..esac вложено в цикл. Тем не менее, для функции выражение одно, а что в
# нем заключено ее по большей части не интересует.
</source>
 
Следующий пример демонстрирует, как можно создать функцию логирования, которая будет перенаправлять вывод в зависимости от типа выводимого сообщения. Ошибки и предупреждения будут перенаправляться в файл с именем <code>error.log</code>, а все остальное в файл с именем <code>main.log</code>. Мы пользуемся тем, что в compound-выражениях можно работать с файловыми дескрипторами (''redirection'').
 
Следующий пример не потокобезопасен, так как возможна гонка за файл с логом. Для потокобезопасности следует доработать функцию в плане проверки, что в файл никто сейчас не пишет. Для простого сценария без сложных ветвлений подоболочек функция будет работать хорошо.
 
<source lang=bash>
readonly LOG_MAIN='main.log'
readonly LOG_ERRORS='error.log'
 
log() {
for option in "$@"; do
case $option in
-i | --info) printf "[Info]: " ;;
-w | --warn) exec 1>&4; printf "[Warn]: " ;;
-e | --error) exec 1>&4; printf "[Error]: " ;;
*) printf "$option\n" && exec 1>&3 ;;
esac
done
exec 1>&5
} 3>>"${LOG_MAIN}" 4>>"${LOG_ERRORS}" 5>&1 1>&3
# Примечание:
# Блок функции перед выполняет открывает дескрипторы 3 и 4 на дозапись,
# с которыми связывает соответственно файлы main.log и error.log. Также мы
# копируем стандартный поток вывода в пятый дескриптор, чтобы иметь возможность
# его восстановить, как это делается в конце блока функции.
#
# По умолчанию все команды блока будут писать в 3 дескриптор, потому что стандартный
# поток вывода мы направили в него.
#
 
log -i "Process is started."
log -w "Not found configuration. Using default values."
log -e "Error occured."
log "My string"
</source>
 
=== Механизмы ограничения видимости переменных и квалификаторы типов ===
 
В оригинальном Bourne Shell не было возможности ограничить область видимости переменной (т.е. все переменные по сути находились в глобальной области видимости, независимо от того, где они объявляются), да и в этом по большому счету не было необходимости: сценарии были небольшими, поэтому контролировать область видимости переменной было не сложно, используя дисциплинированность самого программиста. Но со временем отсутствие механизмов ограничения видимости давало о себе знать, особенно когда один и тот же сценарий поддерживался многими разработчиками: при потере внимательности программист мог случайно затереть значение переменной, если не знал о ее существовании; исправлял значение не из того места программы по ошибке. Предпринимались попытки исправить эту ситуацию, однако к этому моменту уже существовал большой пласт программистов, привыкших к старой системе, и сценариев, которые было невыгодно переписывать.
 
В Bash есть механизмы поддержки области видимости переменной и есть кое-какие квалификаторы, но все они носят факультативный характер и могут опускаться. Это сделано для того, чтобы старые сценарии могли запускаться по умолчанию без дописывания. В новых сценариях ''настоятельно'' рекомендуется использовать имеющиеся механизмы, дабы предупредить возможные ошибки, связанные с областью видимости переменной.
 
==== Ключевое слово declare ====
 
Declare является встроенной командой Bash. Она идентична команде <code>typeset</code>, введенной в Ksh, и в самом Bash <code>typeset</code> является псевдонимом <code>declare</code> для обратной совместимости с Ksh. Команда <code>declare</code> копирует оригинальный синтаксис <code>typeset</code>, вводя некоторые дополнительные возможности.
 
* Старайтесь не использовать ключевое слово <code>typeset</code> в сценариях для Bash, так как эта возможность помечена, как устаревшая.
* <code>declare</code> не определена в POSIX, поэтому его использование автоматически делает ваши сценарии не переносимыми, но если вам это и не нужно, то переживать особо не стоит.
* <code>declare</code> придумана только для Bash, поэтому ваши сценарии также не будут портироваться в клонах Bourne Shell (Ksh и Zsh).
 
<code>declare</code> служит, чтобы явно обозначить объявление переменной. Команда имеет дополнительные атрибуты, которые наделяют переменную некоторыми свойствами, которые поддерживает сам интерпретатор. Свойство, которое дается переменной по умолчанию, это область видимости: если переменная объявлена вне функции, то она обладает глобальной видимостью; если переменная объявлена в теле функции, то она обладает локальной видимостью для данной функции. Это позволяет вам многократно использовать одни и те же имена для переменных, не беспокоясь об их видимости. Это аналогично концепции видимости в языке Си.
 
<code>declare</code> может использоваться также для получения некоторой технологической информации, например об уже объявленных функциях и переменных.
 
Существует ключевое слово <code>local</code>, которое идентично <code>declare</code> со следующими ограничениями:
* его можно использовать только внутри функций: попытка использовать его вне функции приводит к синтаксической ошибке;
* <code>local</code> с ключом <code>-g</code> работает также как <code>declare</code> с этим ключом внутри функции;
* без аргуметов <code>local</code>, как и <code>declare</code>, выводит список объявленных переменных, но с учетом видимости области, из которой она вызывается.
 
<code>local</code> не определена в POSIX и большинство оболочек — клонов Bourne Shell — её не поддерживает. Кроме Bash, эта команда есть в Dash и BusyBox.
 
<source lang=bash>
# Далее мы приведем практическое использование declare и ее ключей
 
# Общий синтаксис
# declare [-aAfFgilrtux] [-p] [NAME[=VALUE] ...]
 
# Создаем два простых массива
declare -a ARR1=(1 2 3) ARR2=(4 5 6)
echo "${ARR1[@]} ; ${ARR2[@]}"
 
# Создаем ассоциативный массив
declare -A ARR3=([name]=John [age]=24)
echo "${ARR3[@]} ; ${ARR3[name]} ; ${ARR3[age]}"
 
# Объявляем переменную с глобальной областью видимости
declare GLOBAL1="Hello" GLOBAL2="World"
echo "$GLOBAL1 $GLOBAL2"
 
# Объявление числовой переменной
declare -i NUMBER=14 WRONG_NUMBER=abba SEQ="1,3,5,6"
echo "$NUMBER $WRONG_NUMBER $SEQ"
 
# Примечание:
# В предыдущем примере переменной WRONG_NUMBER будет присвоен 0, потому
# что строка не конвертируется в число.
# Переменной SEQ будет присвоено значение 6, вероятно из-за того, что алгоритмы
# просматривают строку справа налево.
# В строке не должно быть пробельных символов.
 
# Выведет все переменные, объявленные на текущий момент, вместе с их атрибутами.
declare -p
 
# Сделать переменную константой
declare -r CONSTANT="const"
# Константу нельзя изменить. Следующий код приведет к аварийному завершению сценария.
# CONSTANT="change"
 
declare -x EXPORTED=hello # Аналогично инструкции export EXPORTED="hello"
 
func() {
declare local_var="local" # Переменная имеет локальную область видимости
local local_var1="local" # Аналогично предыдущей команде
 
declare -g global_var1="global" # Создаем глобальную переменную из функции.
local -g global_var2="global" # Аналогично предыдущей команде
 
declare -p # Выведет только то, что видно из этой функции
}
 
# Объявляем dummy-функцию
func1() { :;}
 
func # Вызываем функцию
# и видим, что локальных переменных функции нам отсюда не видно.
echo "$local_var $local_var1 $global_var1 $global_var2"
 
declare -f # Выведет все функции, вместе с их кодом, объявленные на текущий момент
 
# Выведет только функцию func1, если она объявлена.
declare -f func1
</source>
 
На практике, решая что лучше, использовать только <code>declare</code> или совмещать ее использование с <code>local</code>, следует исходить из удобства этого для вас и вашей команды. Если вы пишите только для Bash, то комбинирование рекомендуется, так как визуально <code>local</code> лучше передает намерения разработчика внутри функций. Кроме того, это защищает вас от необдуманного копирования кода.
 
==== Ключевое слово readonly ====
 
Встроенная команда <code>readonly</code> позволяет пометить переменную или функцию только для чтения. Это также запрещает использование команды <code>unset</code>.
 
Рассмотрим ее использование на конкретных примерах.
 
<source lang=bash>
# Неизменяемый простой массив
readonly -a ARR1=(1 2 3) ARR2=(4 5 6)
echo "${ARR1[@]} ; ${ARR2[@]}"
 
# Неизменяемый ассоциативный массив
readonly -A ARR3=([name]=John [age]=24)
echo "${ARR3[@]} ; ${ARR3[name]} ; ${ARR3[age]}"
 
# Объявление константной переменной
readonly CONST1="hello"
# Объявление константной переменной через declare
declare -r CONST2="hello"
 
func() {
echo "const func"
}
 
# Делаем функцию константной
readonly -f func
func
 
# Следующая команда выполнится с ошибкой, потому что функция защищена
# константностью.
unset -f func
 
# Выведет все константные переменные
readonly -p
 
readonly -pa # вывести все константные простые массивы
readonly -pA # вывести все константные ассоциативные массивы
readonly -pf # вывести все константные функции
</source>
 
<code>readonly</code> определена в POSIX, но только с опцией <code>-p</code>.
 
=== Использование функций в Bash-приложениях ===
 
Практика показывает, что сложность поддержки Bash-сценария напрямую зависит от его размера, и если в некоторый сценарий изначально не была заложена некоторая структура, то по мере его роста все скатывается в совершенно нечитаемый код. В маленьких технологических сценариях вы можете не придерживаться строгих правил, но в больших сценариях следует придерживаться рекомендаций, описанных ниже.
 
* Следует по возможности весь код разбивать по функциям. Это хорошо помогает, когда дело доходит до отладки, тестирования и во время добавления новой функциональности. Неправильно работающие функции быстро вычисляются из общей массы, а неверно работающий код изолируется самой функцией.
* Как дополнение к предыдущей рекомендации, часто повторяющиеся действия всегда должны существовать в форме функций.
* В Bash приложения пишутся также как на Си, но в отличии от Си, вы можете вкладывать исполняемый код между функциями. Код, выполняющийся вне функций, по возможности должен быть технологическим для самого сценария, т.е. он должен проверять достигнуты ли необходимые условия, после которых можно приступить к исполнению основного алгоритма.
* Рекомендуется сразу закладывать функцию <code>main</code>, как в языке Си, и вызывать ее по шаблону описанному ниже. Это поможет обойти проблему отсутствия опережающего определения функций.
* Для удобства поставки, весь код сценария оформляется одним файлом (''standalone bash application''), но это не всегда возможно. Например бывает, что есть сценарии выполняющие разную работу, но использующие один и тот же инструментарий. В этом случае вы вынуждены либо укомплектовать сценарий кучей передаваемых на вход ключей, либо выполнить декомпозицию в разные файлы. В последнем случае приходится иметь дело с дублированием кода в разных файлах. При декомпозиции алгоритмов в разные файлы, общие функции следует оформлять в виде ''разделяемой'' bash-библиотеки, которую следует внедрять посредством инструкции ''точка'' или <code>source</code>.
 
Давайте рассмотрим простой пример декомпозиции по разным файлам. Пусть мы имеем дело с гипотетическим bash-приложением, состоящим из следующих файлов:
* <code>script1.sh</code> — выполняет первый сценарий.
* <code>script2.sh</code> — выполняет второй сценарий. Его алгоритм не пересекается с первым сценарием, но они пользуются одинаковыми функциями.
* <code>common.bash</code> — библиотека с общим набором функций, которые разделяются между сценариями, дабы не дублировать код.
* <code>validators.bash</code> — еще одна библиотека с узконаправленным инструментарием (в данном примере это коллекция валидаторов). Сценарий может пользоваться ей, а может не пользоваться.
 
Мы придерживаемся конвенции, когда bash-библиотеки имеют расширение <code>.bash</code>, а исполняемый сценарий — <code>.sh</code>. Это не общеизвестное правило, но мы настоятельно рекомендуем использовать хоть какие-нибудь расширения, потому что они помогают в поиске файлов. Расширение <code>.bash</code> накладывает на библиотеку ограничение — при попытке вызова ее как сценария ничего не должно происходить, кроме, возможно, самодиагностики.
 
Давайте начнем сначала с файлов библиотек и как их можно построить. В своих проектах вы можете не использовать ненужные вам части, так как в этом руководстве мы преследуем цель показать комплексный пример.
 
<source lang=bash>
# Файл: common.sh
# Примечание: в файлах библиотек не обязательно использовать башенг.
 
# Мы используем эту проверку, чтобы предупредить попытку двойного импорта.
# Для этого мы проверяем две специальные переменные, которые определяют сигнатуру
# библиотеки. Они должны быть уникальны для данной библиотеки.
[[ ! -z "$_LIB_COMM_PREFIX" && ! -z "$_LIB_COMM_VERSION" ]] &&
{ echo "Error: ${BASH_SOURCE##*/}:${BASH_LINENO[1]}: double library importing."; exit 129; }
 
# Эти переменные определяют сигнатуру библиотеки. В этом примере мы используем две констатнты:
# префикс библиотеки и ее версию. Эти переменные могут быть использованы для различных проверок
# импорта, но в такие дебри мы забираться не будем.
#
# В данном случае мы пытаемся эмулировать технику препроцессора языка Си, где похожим
# образом запрещают двойной include.
readonly _LIB_COMM_PREFIX='comm'
readonly _LIB_COMM_VERSION='0.9'
 
# Далее идет коллекция функций данной библиотеки. Формат библиотеки очень строгий, в частности:
# * не должно быть постороннего кода между функциями, кроме глобальных переменных;
# * по возможности все функции должны быть изолированы от внешнего по отношению к библиотеке
# кода. Если это невозможно, то вы должны предусмотреть механизмы контроля порядка включения,
# либо попытаться слить две библиотеки в один файл;
# * для всех функций рекомендуется писать комментарий по тому, как ей пользоваться.
#
 
#
# Функция печати на терминал.
#
# say -[iew] <Сообщение>
#
# Опции:
# -e | --error
# Печать сообщение с плашкой ошибки
#
# -w | --warn
# Печать сообщение с плашкой предупреждения
#
# -i | --info
# Печать сообщение с плашкой информации
#
# Код возврата:
# 0 в случае успеха
#
say() {
# ...
return 0
}
 
#
# Печатает сообщение об ошибке в стандартной форме.
#
# handle_error --number <Проверяемое значение>
# Выводит сообщение об ожидании числа.
#
# handle_error --string <Проверяемое значение> <Ожидаемое значение>
# Выводит сообщение об ожидании конкретного значения.
#
#
handle_error() {
# ...
return 0
}
 
# Следующая секция опциональна и закладывает механизм самодиагностики.
# В частности, вы можете закладывать проверку наличия нужных утилит, используемых
# в библиотеке.
__test() {
[[ ${1:-} == 'test' ]] || return 0
# ...
return 0
}
__test "$@" || { echo "Error: Self test failure in \"$BASH_SOURCE\""; exit 128; }
unset -f __test
</source>
 
Вторая библиотека строится аналогично. Разница состоит в том, что у нее своя коллекция функций, сигнатура и функции самодиагностики.
 
<source lang=bash>
# Файл: validators.bash
[[ ! -z "$_LIB_VAL_PREFIX" && ! -z "$_LIB_VAL_VERSION" ]] &&
{ echo "Error: ${BASH_SOURCE##*/}:${BASH_LINENO[1]}: double library importing."; exit 129; }
 
readonly _LIB_VAL_PREFIX='val'
readonly _LIB_VAL_VERSION='0.9'
 
# В библиотеке common мы так не делали, потому что функции в ней часто используются, но в целом
# следует придерживаться одинакового правила именования функций в библиотеке, а именно начинать их имена
# на один и тот же префикс. Так мы гарантируем их уникальность в пределах сценария, в который эти
# библиотеки включаются.
 
#
# Проверяет, является ли передаваемая строка числом.
#
# $1 Проверяемое число
#
# Возвращаемый код:
# 0 аргумент является числом
# 1 аргумент не число
#
val_is_number() {
# ...
}
 
#
# Проверяет, является ли передаваемая строка пустой.
# Пустой также считается строка, заполненная только пробелами и табуляторами.
#
# $1 Проверяемая строка
#
# Возвращаемый код:
# 0 строка пустая
# 1 строка не пустая
#
val_is_empty() {
# ...
}
 
# Здесь мы реализуем что-то похожее на модульные тесты.
__test() {
[[ ${1:-} == 'test' ]] || return 0
#
! val_is_number || return 1
val_is_number "123" || return 1
val_is_number "123.23" || return 1
! val_is_number "1b3.2a" || return 1
! val_is_number "abc12" || return 1
#
val_is_empty || return 1
val_is_empty "" || return 1
return 0
}
__test "$@" || { echo "Error: Self test failure in \"$BASH_SOURCE\""; exit 128; }
unset -f __test
</source>
 
В Bash нет понятия ''пространства имен'', поэтому уникальность имен функций должен обеспечивать сам программист. Существует несколько подходов к образованию имен функций, способствующих их уникальности:
* Можно делать по старинке, а именно начинать все функции в пределах одного файла на один префикс. Обычно имя префикса по смыслу как то связано с назначением всего файла, если это библиотека. Например, все библиотечные функции библиотеки валидаторов начинаются на <code>val_</code>: <code>val_is_empty</code>, <code>val_is_number</code>, <code>val_is_string</code> и т.п.
* Можно расширить предыдущую идею и использовать синтаксис языка С++, создавая псевдопространства имен: <code>Validators::is_empty</code>, <code>Validators::is_number</code>, <code>Validators::is_string</code> и т.п.
* Если пойти еще дальше, то имена функциям можно давать в верблюжьей нотации: <code>Validators::isEmpty</code>, <code>Validators::isNumber</code>, <code>Validators::isString</code> и т.п.
* Переменные и функции библиотеки, которые не должны использоваться в клиентском коде, настоятельно рекомендуется начинать на нижнее подчеркивание (<code>_INTERNAL_VAR</code>, <code>__internal_function()</code>). Это убережет программиста, который будет пользоваться библиотекой, от конфликтов имен.
 
Важно помнить, что в пределах одного приложения следует стараться пользоваться одним стилем, так как смешение обычно плохо сказывается на читаемости кода.
 
Далее мы рассмотрим как строится типичный сценарий.
 
<source lang=bash>
#!/bin/bash
# Файл: script1.sh
#
# Мы рекомендуем инициализировать следующую переменную, чтобы понять, где хранится
# исходный файл. Этот поможет сделать сценарий независимым от текущей рабочей директории.
readonly _SOURCE_DIR=$(dirname "$(readlink -f ${BASH_SOURCE[0]})")
 
# Включаем зависимые библиотеки. Мы предполагаем, что они лежат в том же каталоге, что сценарий.
source "$_SOURCE_DIR/common.bash"
source "$_SOURCE_DIR/validators.bash"
 
# Мы рекомендуем начинать сценарий с функции usage, которая выводит на терминал правила использования
# сценария. Хорошо когда она находится вверху - ее может видеть разработчик не пролистывая далеко вниз.
usage() {
printf "Usage:
$0 [-h]
 
Пример шаблона для строки использования.
 
Опции:
-h Вывести строку использования и выйти.
 
"
}
 
# Следующая функция разбирает строку с аргументами, переданными сценарию.
# Функция может строиться самым разным образом. Мы покажем типичный код для этой функции.
process_flags() {
while getopts ':h' flag; do
case "${flag}" in
h) usage
die 0
;;
\?) # Секция обработки неизвестного аргумента
;;
:) # Секция обработки пропущенного аргумента
;;
esac
done
return 0
}
 
# Это главная функция сценария. Вы должны вызывать весь последующий код
# исключительно из нее.
main() {
process_flags "$@" || die 1
say "Doing very important things..."
val_is_number 123 && say -i "123 is a number."
exit 0
}
 
# После функции main мы можем объявлять другие вспомогательные функции,
# которые нужны только в этом сценарии. Мы можем объявлять их в любом порядке
# так как мы обходим проблему опережающего определения ниже.
die() {
[[ $# -eq 0 ]] && exit 1
local exit_code=$1
local message=$2
[[ ! -z $message ]] &&
say -e "$message"
exit $exit_code
}
 
# Наконец мы вызываем функцию main. Следующая строка должна быть ВСЕГДА последней.
# Мы используем проверку BASH_SOURCE, чтобы, при желании, этот файл можно было бы
# включить в другой и использовать его функции повторно. Ниже мы рассмотрим как переопределить
# существующую функцию main и сделать код многократно используемым во многих сценариях.
[[ "$0" == "$BASH_SOURCE" ]] && main "$@"
</source>
 
Предположим что <code>script1.sh</code> является подмножеством <code>script2.sh</code>, т.е. <code>script2.sh</code> включает все, что есть в <code>script1.sh</code>, и отличается функцией <code>main</code> и возможно вводит еще функции. Мы могли бы просто скопировать общие части из <code>script1.sh</code>, но это приведет к ненужному дублированию кода, тем более, если сценарии нужно поддерживать одинаковыми в части, в которой они пересекаются.
 
Вместо этого мы можем просто включить <code>script1.sh</code> целиком в <code>script2.sh</code> и переопределить функцию <code>main</code>, избавив нас от двойной работы. Покажем как это сделать.
 
<source lang=bash>
#!/bin/bash
 
# Выполняем включение script1.sh. Проверка на последней строке script1.sh не позволит функции
# main исполниться, так как условие будет ложным.
_p=$(dirname "$(readlink -f ${BASH_SOURCE[0]})")
source "$_p/script1.sh" # Файл должен лежать в том же каталоге, что и данный файл.
 
# Удаляем функцию main.
unset -f main
 
# Определяем новую функцию main.
 
main() {
# ... Все функции, доступные в script1.sh доступны и здесь ...
process_flags "$@" || die 1
echo "The main function has been overriden."
exit 0
}
 
#
# ... Определяем функции, которые нужны нам здесь ...
#
 
[[ "$0" == "$BASH_SOURCE" ]] && main "$@"
</source>
 
У данного подхода есть свои недостатки:
* Данный шаблон не позволяет автоматически контролировать ранее определенные функции без дополнительного программирования. Это может приводить к конфликту имен функций в разных модулях. За этим должен следить разработчик и разрешать конфликты вручную.
* При использовании данного шаблона возникают трудности, когда появляются зависимости между библиотеками. Таких ситуаций нужно избегать как можно дольше. Обычно если такое происходит, то проще продублировать недостающие функции, чтобы не порождать зависимости.
* Данный шаблон требует, чтобы все файлы лежали в одном месте, либо чтобы структура их расположения была заранее известна.
* Данный шаблон требует внимательного проектирования библиотек. Необходимо чтобы код библиотечных функций был максимально самодостаточным и устойчивым к нечаянным изменениям со стороны использующего его сценария. В глобальных переменных, вводимых библиотеками, следует использовать нижнее подчеркивание в начале имени, чтобы избежать конфликт имен в запускающем сценарии.
 
Обычно, если цели автоматизации выходят за рамки манипуляций с файлами, рекомендуется перейти на другой скриптовый язык, вместо дальнейшего усложнения сценария на Bash, например Python, Groovy, Perl и другие. В синтаксис этих языков включены более продвинутые приемы для поддержки многофайлового приложения.
 
=== Вкладывание функций и возможности мета-программирования ===
 
В Bash функции можно вкладывать. В отличие от других языков программирования, вкладывание не создает отдельную область видимости: все функции сценария лежат в одной области и помещаются в нее по мере их обнаружения.
 
Тем не менее, вкладывание функций в Bash имеет полезное свойство, основанное на том, что тело функции не интерпретируется до момента ее реального вызова. Это позволяет нам управлять сценарием на мета-уровне: иметь функции с одинаковым именем, но разной реализацией. Конкретная реализация может привязываться к условиям, т.е. программа может составлять себя на основе мета условий.
 
Рассмотрим такой пример.
<source lang=bash>
outer() {
echo "Called $FUNCNAME function (${1:-parent}). PID: $BASHPID";
inner() {
echo "Called $FUNCNAME function";
}
}
</source>
 
Казалось бы интерпретатор при первом проходе должен увидеть две функции <code>outer()</code> и <code>inner()</code>, однако до реального вызова функции <code>outer()</code>, будет зарегистрирована всего одна функция <code>outer()</code>. Чтобы убедиться в этом, вы можете попробовать сделать такой вызов после объявления
 
<source lang=bash>
inner || true # Мы используем функцию true, чтобы исполнение не прерывалось
# Результат: line 10: inner: command not found
 
# Теперь вызовем outer
outer
# Результат: Called outer function (parent). PID: 931
 
# и снова inner
inner
# Результат: Called inner function
</source>
Таким образом, функции <code>inner()</code> не существует, пока интерпретатор не исполнит тело функции <code>outer()</code>.
 
Приведем несколько примеров как это можно использовать в ваших сценариях. Самый наверное очевидный сценарий применения, это изменение реализации функции на ходу.
<source lang=bash>
#!/bin/bash
# Файл: probe.sh
#
# Возвращает 0, если переданный идентификатор уже занят ранее объявленной функцией.
#
isDefined() [[ -n $(declare -f "$1") ]]
 
set_default_implementation() {
probe() {
echo "$FUNCNAME: default implementation."
}
}
 
if ! isDefined "probe" && [[ $# -eq 0 ]]; then
set_default_implementation
elif ! isDefined "probe" && [[ $# -ne 0 ]]; then
case "$1" in
--first)
probe() {
echo "$FUNCNAME: first implementation."
}
;;
--second)
probe() {
echo "$FUNCNAME: second implementation."
}
;;
*) set_default_implementation ;;
esac
fi
 
probe
</source>
В этом примере реализация функции <code>probe</code> зависит от того, как запущен сценарий:
<source lang=bash>
$ ./probe.sh
probe: default implementation.
 
$ ./probe.sh --first
probe: first implementation.
 
$ ./probe.sh --second
probe: second implementation.
 
$ ./probe.sh unknown
probe: default implementation.
</source>
Интересно то, что если бы функция была объявлена до проверки ее существования, например через процедуру внедрения в этот файл другого, то проверка была бы опущена и произошел бы вызов уже объявленной функции. Добавьте такой код до конструкции <code>if..fi</code>
<source lang=bash>
probe() {
echo "$FUNCNAME: My implementation."
}
</source>
и снова попробуйте вызвать
<source lang=bash>
$ ./probe.sh
probe: My implementation.
 
$ ./probe.sh --first
probe: My implementation.
</source>
В любом случае, данная реализация гарантирует, что функция будет объявлена до её реального вызова. Теперь представьте, что реализация выбирается на основе параметров из конфигурационного файла, либо на основе проверок окружения сценария и т.п.
 
Еще один интересный способ применения, который напрямую с вкладыванием не связан, но тем не менее вполне в духе мета-программирования, — мета-код. Мета-код – это такой код, который интерпретатор получает из передаваемых ему строк, т.е. изначально он даже не записан в исходный файл или не был семантически проанализирован. Это подобие того, что программа пишет сама себя. Ниже приведен простой пример.
<source lang=bash>
#!/bin/bash
# Файл: meta-code.sh
#
# Переопределяет реализацию существующей функции (вариант 1)
#
# $1 Имя функции.
# $2 Код функции.
#
reDefine1() {
[[ $# -eq 2 ]] || return 1
local fname=$1
local fbody=$2
unset -f "$fname"
eval "$fname" "$fbody"
}
 
#
# Переопределяет реализацию существующей функции (вариант 2)
# Тело функции получается из стандартного потока ввода.
#
# $1 Имя функции.
#
#
reDefine2() {
[[ $# -eq 1 ]] || return 1
local fname=$1
local fbody
while read -r; do
fbody="${fbody}\n${REPLY}"
done
fbody=$(echo -e "$fbody")
if [[ -n $fbody ]]; then
unset -f "$fname"
eval "$fname" "$fbody"
else
return 1
fi
return 0
}
 
reDefine1 "meta()" '{
echo "$FUNCNAME: Hello, World! First"
}'
 
meta
 
reDefine2 "meta()" <<<'{
echo "$FUNCNAME: Hello, World! Second"
}'
 
meta
</source>
Вызов сценария даст такой результат:
<source lang=bash>
$ meta-code.sh
meta: Hello, World! First
meta: Hello, World! Second
</source>
 
А теперь представьте, что тело поставляется в функцию через какой-то файл.
 
Конечно подобные фокусы редко пригождаются на практике, тем не менее shell-сценарии могут быть не так просты.
 
== Эмуляция ссылочной адресации ==
 
В оригинальном Bourne Shell не существовало метода косвенной адресации переменных, т.е. когда в некоторой переменной (к которой мы обращаемся) хранится ссылка в виде значения на другую переменную, чье значение нас интересует. Другими словами, все значения переменных в сценариях командной оболочки передаются куда-либо исключительно по значению.
 
Ссылочный метод используется многими языками программирования, потому что передача ссылки на область памяти несет в себе меньше расходов ресурсов при передаче аргументов функциям. Ссылками также пользуются, чтобы возвращать из функций несколько результатов за один раз, так как мы можем сказать функции в какую область памяти следует этот результат записать через ссылку.
 
Хотя, ввиду простоты большинства сценариев, ссылочная адресация для передачи значений в функции может и не нужна, механизм возврата нескольких значений из функции был бы весьма полезен. Без него требуется использовать глобальные переменные, которые еще нужно дополнительно синхронизировать.
 
В этом разделе мы рассмотрим несколько подходов эмуляции ссылочной адресации.
 
=== Основы ===
 
Командная оболочка интерпретирует каждую строку в два этапа:
# Сначала она пытается подставить переменную(ые) в интерпретируемую строку.
# Если есть операция присваивания, то выполняется присваивание, либо строка исполняется как командный список, либо как compound-выражение.
 
По такому принципу, например, работает следующий код
<source lang=bash>
STRING="Hello, World!"
COMMAND="echo $STRING" # Сначала подставит $STRING, потом присвоит
$COMMAND # Сначала подставит $COMMAND, потом исполнит
</source>
 
Но такой код работать не будет
<source lang=bash>
STRING="Hello, World!"
COMMAND="HELLO=$STRING"
"$COMMAND" # интерпретатор будет пытаться искать команду 'HELLO=Hello, World!'
echo "$HELLO"
</source>
Это связано с тем, что присваивание это внутренняя процедура Bash. В этом примере символ равно из подстановки значения не является признаком присваивания, так как он является частью строки после подстановки переменной, и поэтому производится попытка буквально исполнить подставленную команду.
 
Bash можно явно заставить интерпретировать текстовую строку как часть кода сценария с помощью внутренней команды <code>eval</code> (от англ. ''evaluate''). Тогда предыдущий код нужно записать так:
<source lang=bash>
STRING="Hello, World!"
COMMAND="HELLO=\"$STRING\"" # Экранированные кавычки обязательны, потому что
# в строке есть пробелы.
eval "$COMMAND" # Теперь команда интерпретируется как будто она была записана
# в сценарий изначально.
echo "$HELLO"
</source>
 
Обратите внимание, что мы должны сохранить буквальные кавычки во время присваивания значения переменной <code>COMMAND</code>, так как Bash все кавычки также интерпретирует на первом этапе. В этом примере мы их не можем опустить, потому что в противном случае после подстановки переменной <code>$COMMAND</code> на выходе получается строка <code>HELLO=Hello, World!</code>, которая формально состоит из двух инструкций:
* <code>HELLO=Hello,</code>
* <code>World!</code>
Так как команды с именем <code>World!</code> в системе явно не будет, то сценарий завершится с ошибкой.
 
Напомним, что операция присваивания интерпретируется в своем контексте, поэтому она может размещаться с любым командным списком и/или другими операциями присваивания на одной строке. Разделителем будет служить символ пробела.
 
Теперь если левую часть от равно так же сделать заменяемой, то можно получить подобие косвенной адресации в Bash. Главным образом это позволяет передавать в функции переменные, хранящие имена других переменных (левая часть равно), чтобы функция могла знать куда ей можно записать результат.
 
=== Простая косвенная адресация ===
 
Начнем с такого примера
 
<source lang=bash>
#!/bin/bash
 
declare -a FRUITS=()
declare -a VEGETABLES=()
 
upvar() {
unset -v "$1" && eval $1=\"$2\"
}
 
getType() {
upvar "$1" 'unknown'
case $2 in
apple | banana | grapes | pineapple)
upvar "$1" 'fruit'
;;
potato | tomato | beans | carrot)
upvar "$1" 'vegetable'
;;
esac
}
 
sorter() {
local thing=$1
getType 'type' $thing
case $type in
vegetable)
VEGETABLES+=("$thing")
;;
fruit)
FRUITS+=("$thing")
;;
*)
echo "Unknown thing: '$thing'"
;;
esac
}
 
for thing in potato grapes tomato banana rock pineapple beans carrot apple; do
sorter "$thing"
done
 
for thing in "$(echo -e "--------\nFruits\n--------\n")" "${FRUITS[@]}" \
$(echo -e "--------\nVegetables\n--------\n") "${VEGETABLES[@]}"; do
echo "$thing"
done
</source>
 
В этом примере у нас есть куча фруктов и овощей и есть сортировочная машина, которая умеет отличать некоторые фрукты и овощи. Соответственно фрукты она будет складывать в массив с фруктами, а овощи в массив с овощами, попутно отсеивая неизвестные предметы.
 
Обратите внимание как работает сортировщик (функция <code>sorter</code>). Сортировщик обращается к функции <code>getType</code>, чтобы она ему вернула тип передаваемого предмета, при этом функция просит положить результат в переменную с именем <code>type</code>. Эта переменная передается здесь по сути по ссылке, потому что сортировщик в передаваемом параметре отражает имя переменной как данные.
 
Функция <code>getType</code> тоже использует свой первый аргумент как ссылку. Обратите внимание, что <code>getType</code> узнает место, в которое нужно записать значение, лишь в момент получения ссылки (она как бы разыменовывает свой первый аргумент и получает имя переменной). Передай мы ей в ссылке имя другой переменной, функция записала бы значение в нее. Сначала <code>getType</code> инициализирует переменную по ссылке значением <code>unknown</code>, затем в результате своих нехитрых алгоритмов, она уточняет результат.
 
Функция <code>getType</code> тоже передает ссылку транзитом вспомогательной функции, которая как бы разыменовывает ссылку и присваивает переменной в разыменованной ссылке значение, переданное функции во втором аргументе. В итоге, ссылка на переменную <code>type</code> проходит через три функции вперед и назад. С точки зрения программы ее настоящее имя знает только сортировщик, что дает нам преимущество не привязываться к конкретным именам.
 
Результат работы этой программы представлен ниже
<source lang=bash>
Unknown thing: 'rock'
--------
Fruits
--------
grapes
banana
pineapple
apple
--------
Vegetables
--------
potato
tomato
beans
carrot
</source>
 
К сожалению данный метод имеет недостаток в том, что переменная, передаваемая по ссылке, будет хранится в глобальной памяти, и ограничить ее видимость невозможно. Для предотвращения некоторых последствий от возможных гонок, функция <code>upvar</code> делает <code>unset</code> переменной, однако такой подход все равно остается не потокобезопасным.
 
=== Косвенная адресация с помощью printf ===
 
Предыдущий пример можно сделать безопаснее, если использовать внутреннюю команду <code>printf</code> с опцией <code>-v</code>. Напомним, что эта опция записывает строку в переменную, указанную после опции. Преимущество <code>printf</code> состоит в том, что она учитывает видимость переменной, если она была ограничена. Ниже показаны участки кода, которые нужно изменить.
 
<source lang=bash>
# ...Без изменений
 
upvar() {
printf -v "$1" "$2"
}
 
# ...Без изменений
 
sorter() {
local thing=$1
local type # ограничиваем видимость переменной type
#
# ...Без изменений
#
}
 
# ...Без изменений
 
echo "type='$type'" # Для проверки
</source>
 
После запуска получаем такой вывод:
<source lang=bash>
Unknown thing: 'rock'
--------
Fruits
--------
grapes
banana
pineapple
apple
--------
Vegetables
--------
potato
tomato
beans
carrot
type=''
</source>
 
Обратите внимание, что мы поменяли всего две строчки и сохранили функциональность примера:
* В функции <code>upvar</code> мы используем команду <code>printf</code> для записи значения по ссылке.
* В функции <code>sorter</code> мы объявили переменную <code>type</code> и ограничили ее видимость с помощью <code>local</code>.
 
Судя по выводу программы, переменная <code>type</code> действительно ограничена по видимости, т.е. использование <code>printf</code> для косвенной адресации предпочтительно.
 
Хотя команда <code>printf</code> описана в POSIX, ключ <code>-v</code> для нее в нем не описан, что автоматически делает ваши сценарии не портируемыми, если вы используете для косвенной адресации эту команду.
 
=== Косвенная адресация как источник данных ===
 
Можно пойти с другой стороны и говорить через косвенную адресацию откуда данные нужно брать. Следующий пример довольно искусственный, но он позволяет показать идею.
 
<source lang=bash>
#!/bin/bash
# Файл: authorizer.sh
declare -A emploee_card1=(
[id]=2569
[name]=John
)
 
declare -A emploee_card2=(
[id]=1214
[name]=Alice
)
 
declare USER
 
say_hello() {
eval echo "Hello \"\$$USER\"!"
}
 
read -p "Enter your id: "
 
if (( ${emploee_card1[id]} == "$REPLY" )); then
USER='{emploee_card1[name]}'
elif (( ${emploee_card2[id]} == "$REPLY" )); then
USER='{emploee_card2[name]}'
else
echo "Error: This id is unknown to the system."
exit 1
fi
 
say_hello
</source>
 
В этом примере у нас есть две карточки некоторых служащих, в виде ассоциативных массивов, в которых хранится личный идентификатор и имя. Также есть функция, которая приветствует пользователя, если он вводит правильный идентификатор.
 
Функция приветствия спроектирована так, что она не принимает никаких аргументов, но она знает, что к моменту ее вызова в переменной <code>USER</code> будет хранится ссылка на данные, по которым она сможет перейти на имя служащего. В зависимости от того, что ввел пользователь, в <code>USER</code> будет помещена ссылка на поле <code>name</code> одного из массивов.
 
Обратите внимание, что мы используем в функции приветствия двойной доллар для переменной <code>USER</code>. Один доллар нужен, чтобы подставить ссылку из переменной, а второй нужен, чтобы ссылку разыменовать. Второй доллар мы экранируем, потому что он не должен быть интерпретирован во время подстановки ссылки из <code>USER</code>.
 
Ниже показан пример работы сценария.
<source lang=bash>
$ authorizer.sh
Enter your id: 2569
Hello John!
 
$ authorizer.sh
Enter your id: 1214
Hello Alice!
 
$ authorizer.sh
Enter your id: 1
Error: This id is unknown to the system.
</source>
 
== См. также ==
* [[Настольная книга по Linux]]
 
== Примечания ==
{{Примечания}}
 
{{Готовность |75%}}
Строка 1988 ⟶ 36 :
 
[[Категория:Linux]]
[[Категория:Языки программирования]]