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

Содержимое удалено Содержимое добавлено
Строка 1732:
 
== Эмуляция ссылочной адресации ==
 
В оригинальном 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><nowiki>HELLO=Hello, World!</nowiki></code>, которая формально состоит из двух инструкций:
* <code><nowiki>HELLO=Hello,</nowiki></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>
 
== См. также ==