Практическое написание сценариев командной оболочки Bash/Эмуляция ссылочной адресации
← Функции | Глава | Bash подстановки → |
Эмуляция ссылочной адресации | ||
В оригинальном Bourne Shell не существовало метода косвенной адресации переменных (indirect addressing), т.е. когда в некоторой переменной (к которой мы обращаемся) хранится в виде значения ссылка на другую переменную, чье значение нас интересует. Другими словами, все значения переменных в сценариях командной оболочки передаются куда-либо исключительно по значению.
Ссылочный метод используется многими языками программирования, потому что передача ссылки на область памяти несет в себе меньше расходов ресурсов при передаче аргументов функциям по значению. Ссылками также пользуются, чтобы возвращать из функций несколько результатов за один раз, так как мы можем сказать функции в какую область памяти следует этот результат записать через ссылку.
Хотя, ввиду простоты большинства сценариев, ссылочная адресация для передачи значений в функции может и не нужна, механизм возврата нескольких значений из функции был бы весьма полезен. Без него требуется использовать глобальные переменные, которые еще нужно дополнительно синхронизировать.
В этом разделе мы рассмотрим несколько подходов эмуляции ссылочной адресации.
Основы[править]
Командная оболочка интерпретирует каждую строку в два этапа:
- Сначала она пытается подставить переменную(ые) в интерпретируемую строку.
- Если есть операция присваивания, то выполняется присваивание, либо строка исполняется как командный список, либо как compound-выражение.
По такому принципу, например, работает следующий код
STRING="Hello, World!"
COMMAND="echo $STRING" # Сначала подставит $STRING, потом присвоит
$COMMAND # Сначала подставит $COMMAND, потом исполнит
Но такой код работать не будет
STRING="Hello, World!"
COMMAND="HELLO=$STRING"
"$COMMAND" # интерпретатор будет пытаться искать команду 'HELLO=Hello, World!'
echo "$HELLO"
Это связано с тем, что присваивание это внутренняя процедура Bash. В этом примере символ равно из подстановки значения не является признаком присваивания, так как он является частью строки после подстановки переменной, и поэтому производится попытка буквально исполнить подставленную команду.
Bash можно явно заставить интерпретировать текстовую строку как часть кода сценария с помощью внутренней команды eval
(от англ. evaluate). Тогда предыдущий код нужно записать так:
STRING="Hello, World!"
COMMAND="HELLO=\"$STRING\"" # Экранированные кавычки обязательны, потому что
# в строке есть пробелы.
eval "$COMMAND" # Теперь команда интерпретируется как будто она была записана
# в сценарий изначально.
echo "$HELLO"
Обратите внимание, что мы должны сохранить буквальные кавычки во время присваивания значения переменной COMMAND
, так как Bash все кавычки также интерпретирует на первом этапе. В этом примере мы их не можем опустить, потому что в противном случае после подстановки переменной $COMMAND
на выходе получается строка HELLO=Hello, World!
, которая формально состоит из двух инструкций:
HELLO=Hello,
(инициализация переменной окружения команды)World!
(команда)
Так как команды с именем World!
в системе явно не будет, то сценарий завершится с ошибкой.
Теперь если левую часть от равно так же сделать заменяемой, то можно получить подобие косвенной адресации в Bash. Главным образом это позволяет передавать в функции переменные, хранящие имена других переменных (левая часть равно), чтобы функция могла знать куда ей можно записать результат.
Простая косвенная адресация[править]
Начнем с такого примера
#!/bin/bash
declare -a FRUITS=()
declare -a VEGETABLES=()
upvar() {
unset -v "$1" && eval "${1}=\$2" # или можно так: 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
В этом примере у нас есть куча фруктов и овощей и есть сортировочная машина, которая умеет отличать некоторые фрукты и овощи. Соответственно фрукты она будет складывать в массив с фруктами, а овощи в массив с овощами, попутно отсеивая неизвестные предметы.
Обратите внимание как работает сортировщик (функция sorter
). Сортировщик обращается к функции getType
, чтобы она ему вернула тип передаваемого предмета, при этом функция просит положить результат в переменную с именем type
. Эта переменная передается здесь по сути по ссылке, потому что сортировщик в передаваемом параметре отражает имя переменной как данные.
Функция getType
тоже использует свой первый аргумент как ссылку. Обратите внимание, что getType
узнает место, в которое нужно записать значение, лишь в момент получения ссылки (она как бы разыменовывает свой первый аргумент и получает имя переменной). Передай мы ей в ссылке имя другой переменной, функция записала бы значение в нее. Сначала getType
инициализирует переменную по ссылке значением unknown
, затем в результате своих нехитрых алгоритмов, она уточняет результат.
Функция getType
тоже передает ссылку транзитом вспомогательной функции, которая как бы разыменовывает ссылку и присваивает переменной в разыменованной ссылке значение, переданное функции во втором аргументе. В итоге, ссылка на переменную type
проходит через три функции вперед и назад. С точки зрения программы ее настоящее имя знает только сортировщик, что дает нам преимущество не привязываться к конкретным именам.
Результат работы этой программы представлен ниже
Unknown thing: 'rock'
--------
Fruits
--------
grapes
banana
pineapple
apple
--------
Vegetables
--------
potato
tomato
beans
carrot
К сожалению данный метод имеет недостаток в том, что переменная, передаваемая по ссылке, будет хранится в глобальной памяти, и ограничить ее видимость невозможно. Для предотвращения некоторых последствий от возможных гонок, функция upvar
делает unset
переменной, однако такой подход все равно остается не потокобезопасным.
Кроме того, данный трюк (как и все трюки с eval
) имеет потенциальные проблемы с безопасностью, если правая часть от равно никак не проверяется на содержание. Если это значение приходит в сценарий с клиентской стороны, то в нее потенциально может быть передан вредоносный код, который будет исполнен до присваивания значения. Поэтому такая косвенная адресация должна использоваться только тогда, когда вы, как разработчик, понимаете что делаете.
Косвенная адресация с помощью printf[править]
Предыдущий пример можно сделать безопаснее, если использовать внутреннюю команду printf
с опцией -v
. Напомним, что эта опция записывает строку в переменную, указанную после опции. Преимущество printf
состоит в том, что она учитывает видимость переменной, если она была ограничена. Ниже показаны участки кода, которые нужно изменить.
# ...Без изменений
upvar() {
printf -v "$1" "$2"
}
# ...Без изменений
sorter() {
local thing=$1
local type # ограничиваем видимость переменной type
#
# ...Без изменений
#
}
# ...Без изменений
echo "type='$type'" # Для проверки
После запуска получаем такой вывод:
Unknown thing: 'rock'
--------
Fruits
--------
grapes
banana
pineapple
apple
--------
Vegetables
--------
potato
tomato
beans
carrot
type=''
Обратите внимание, что мы поменяли всего две строчки и сохранили функциональность примера:
- В функции
upvar
мы используем командуprintf
для записи значения по ссылке. - В функции
sorter
мы объявили переменнуюtype
и ограничили ее видимость с помощьюlocal
.
Судя по выводу программы, переменная type
действительно ограничена по видимости, т.е. использование printf
для косвенной адресации предпочтительно.
Хотя команда printf
описана в POSIX, ключ -v
для нее в нем не описан (в Bash опция появилась с версии 3.1), что автоматически делает ваши сценарии не портируемыми, если вы используете для косвенной адресации эту команду. Однако и здесь можно выкрутиться, если использовать eval
, например так:
# $ref - имя переменной по ссылке
# $value - присваиваемое значение
eval "$(printf %s=%q "$ref" "$value")"
Косвенная адресация как источник данных[править]
Можно пойти с другой стороны и говорить через косвенную адресацию откуда данные нужно брать. Следующий пример довольно искусственный, но он позволяет показать идею.
#!/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
В этом примере у нас есть две карточки некоторых служащих, в виде ассоциативных массивов, в которых хранится личный идентификатор и имя. Также есть функция, которая приветствует пользователя, если он вводит правильный идентификатор.
Функция приветствия спроектирована так, что она не принимает никаких аргументов, но она знает, что к моменту ее вызова в переменной USER
будет хранится ссылка на данные, по которым она сможет перейти на имя служащего. В зависимости от того, что ввел пользователь, в USER
будет помещена ссылка на поле name
одного из массивов.
Обратите внимание, что мы используем в функции приветствия двойной доллар для переменной USER
. Один доллар нужен, чтобы подставить ссылку из переменной, а второй нужен, чтобы ссылку разыменовать. Второй доллар мы экранируем, потому что он не должен быть интерпретирован во время подстановки ссылки из USER
.
Ниже показан пример работы сценария.
$ 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.
Косвенная адресация средствами Bash[править]
Начиная с версии Bash 4.3 стабильно можно косвенно ссылаться на переменные средствами самой командной оболочки. Следующие методы вообще не переносятся между разными командными оболочками и ими стоит пользоваться только, когда вы пишите только для Bash.
Для начала рассмотрим самый простой случай.
ORIGIN_VARIABLE='Some value' # Это простая переменная с некоторым значением, на которое мы сошлемся косвенно через ссылку.
REF='ORIGIN_VARIABLE' # Переменная REF (ссылка) хранит имя переменной, на которую ссылается.
# Примечание: в данном случае не обязательно ставить одинарные кавычки при присваивании имени переменной ссылке.
# Мы так делаем, потому что строковые литералы подсвечиваются другим цветом, если текстовый редактор умеет подсвечивать
# код на Bash.
# Чтобы подставить значение по ссылке, вы должны воспользоваться следующей специальной подстановкой:
echo "${!REF}" # Ссылка будет разрешена интерпретатором. Результат подстановки: "Some value"
Можно обозначить ссылку явно через команду declare
с опцией -n
. Эта опция аналогична команде nameref
в Ksh. Явное объявление ссылок улучшает читаемость кода, так как передает ваши намерения явно.
declare -n REF='ORIGIN_VARIABLE'
На практике чаще всего ссылки передаются в универсальные функции, которые могут пользоваться ими, чтобы записать результат своей работы. Следующий пример демонстрирует функцию, которая ищет максимальное и минимальное значения в любом простом массиве (конечно опуская различные проверки).
#!/bin/bash
declare -a ARRAY=(5 6 -1 7 5 10 8 7 12 -8 4)
min_max() {
local refarr=${1}[@] # Это ссылка на передаваемый фунции массив. Ссылки на массивы мы обсудим позже.
local -n min=$2 # Это ссылка на выходную переменную, в которую мы запишем минимальное значение.
local -n max=$3 # Это ссылка на выходную переменную, в которую мы запишем максимальное значение.
min=${1}[0]
max=${1}[0]
for element in "${!refarr}"; do
((min = element < min ? element : min))
((max = element > max ? element : max))
done
}
min_max ARRAY minval maxval # Вызов функции.
# Обратите внимание, что мы передаем функции имена объектов, с которыми функция внутри работает через ссылки,
# что позволяет не привязываться к конкретным именам в реализации функции.
echo "Min value: $minval"
echo "Max value: $maxval"
Результат работы:
Min value: -8
Max value: 12
Как мы уже показали в предыдущем примере, можно делать ссылки на массивы и на их отдельные элементы. К сожалению по таким ссылкам можно только читать данные, что очень ограничивает их применение.
declare -a ARRAY_STRS=('John Doe' 'Bill Watson') # Простой массив
declare -A PERSON=([name]="John" [age]=23) # Ассоциативный массив
# Ссылка на простой массив выглядит так, при этом вариации 'declare -n' для массивов нет
REF_1=ARRAY_STRS[@] # Ссылка на массив
declare REF_1=ARRAY_STRS[@] # Допустимо
# Следующий вариант формально является ссылкой на массив, но он НЕ РЕКОМЕНДУЕТСЯ из-за нежелательного побочного эффекта,
# связанного с механизмом word splitting
WRONG_REF_1=ARRAY_STRS[*]
# На отдельный элемент простого массива ссылка создается похожим образом, но нужно указать индекс
REF_EL_1=ARRAY_STRS[1] # Ссылка на элемент с индексом 1
declare REF_EL_1=ARRAY_STRS[1] # Допустимо
# В ассоциативных массивах обычно ссылаются на конкретные элементы
REF_NAME=PERSON[name]
REF_AGE=PERSON[age]
# Разрешаются ссылки на массивы и их элементы все тем же образом
echo "${!REF_1}" # John Doe Bill Watson
echo "${!WRONG_REF_1}" # John Doe Bill Watson
echo "${!REF_EL_1}" # Bill Watson
echo "${!REF_NAME}" # John
echo "${!REF_AGE}" # 23
Ссылка на простой массив на практике часто используется в связке с функцией printf
из-за очень интересного побочного эффекта: можно распечатать все элементы массива, используя единый формат для каждого элемента, не прибегая к циклу. Например
printf "<%s> " "${!REF_1}"; echo # Вот так можно распечатать все элементы массива, применяя один формат к каждому элементу
Результат
<John Doe> <Bill Watson>
Ссылки на отдельные элементы простых массивов можно использовать для организации произвольных переборов, если вместо конкретного индекса использовать произвольный счетчик. Это показано в следующем примере.
#!/bin/bash
declare -a ARRAY_STRS=('John Doe' 'Bill Watson' 'Bart Simpson' 'Bugs Bunny' 'Homer Simpson') # Простой массив
declare -a NUMS=({-10..10}) # Массив, сгенерированный через скобочную подстановку (см. в след. главе)
# Следующая функция печатает элементы простого массива в определенном формате, начиная с определенного элемента (если указано во-втором аргументе)
# или с начала
print_from() {
local ref_element=${1}[index]
local index=${2:-0}
while printf "<%s> " "${!ref_element}"; ((index++)) ; [[ -n ${!ref_element+_} ]]; do true; done
echo
}
print_from ARRAY_STRS
print_from ARRAY_STRS 2
print_from NUMS
print_from NUMS 6
Результат
<John Doe> <Bill Watson> <Bart Simpson> <Bugs Bunny> <Homer Simpson> # с начала
<Bart Simpson> <Bugs Bunny> <Homer Simpson> # с третьего элемента
<-10> <-9> <-8> <-7> <-6> <-5> <-4> <-3> <-2> <-1> <0> <1> <2> <3> <4> <5> <6> <7> <8> <9> <10> # с начала
<-4> <-3> <-2> <-1> <0> <1> <2> <3> <4> <5> <6> <7> <8> <9> <10> # с седьмого элемента
Обратите внимание как функция объявляет универсальную ссылку на элемент массива. Для этого мы используем счетчик index
, который увеличивается в цикле и двигает ссылку как итератор по массиву. Изменяя начальное значение для index
, мы определяем с какого элемента начать. Чтобы цикл остановился, мы используем хитрое условие [[ -n ${!ref_element+_} ]]
, которое возвращает ИСТИНУ всякий раз, когда ссылку удается раскрыть.
← Функции | Bash подстановки → |