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

Содержимое удалено Содержимое добавлено
Строка 480:
 
== Циклы ==
 
В 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[@]}</nowiki></code> и <code><nowiki>${fruits[*]}</nowiki></code>. Без кавычек разницы нет никакой, но вариант <code><nowiki>"${fruits[@]}"</nowiki></code> позволяет учесть пробельные символы внутри самих элементов массива. Так, в нашем примере в массиве с фруктами без кавычек, элемент <code>'Peruvian cherry'</code> разделился бы на два, что неправильно. Вариант <code><nowiki>"${fruits[*]}"</nowiki></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><nowiki>"${!mesg[@]}"</nowiki></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><nowiki>(( <EXPR1> ; <EXPR2> ; <EXPR3> ))</nowiki></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><nowiki>: $(( RETRY_TIMES -= 1 ));</nowiki></code> — уменьшает счетчик повторений в начале новой итерации.
* <code><nowiki>[[ $RETRY_TIMES -gt 0 ]] && echo -n "Retry $RETRY_TIMES: " || echo "While has finished its work.";</nowiki></code> — делает печать в начале каждой итерации.
* <code><nowiki>[[ $RETRY_TIMES -gt 0 ]]</nowiki></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>
 
== Функции ==