Косвенное присвоение переменных в bash

Кажется, что рекомендуемый способ установки косвенных переменных в bash - это использовать eval:

var=x; val=foo
eval $var=$val
echo $x  # --> foo

Проблема обычная с eval:

var=x; val=1$'\n'pwd
eval $var=$val  # bad output here

(и поскольку это рекомендуется во многих местах, мне интересно, сколько сценариев уязвимы из-за этого...)

В любом случае очевидное решение использования (экранированных) кавычек на самом деле не работает:

var=x; val=1\"$'\n'pwd\"
eval $var=\"$val\"  # fail with the above

Дело в том, что bash имеет косвенную ссылку на переменную, запеченную в (с ${!foo}), но я не вижу такого способа сделать косвенное назначение - есть ли какой-нибудь вменяемый способ сделать это?

Для протокола, я действительно нашел решение, но это не то, что я бы посчитал "вменяемым"...:

eval "$var='"${val//\'/\'\"\'\"\'}"'"

7 ответов

Решение

Суть в том, что рекомендуемый способ сделать это:

eval "$var=\$val"

с RHS сделано косвенно тоже. поскольку eval используется в той же среде, он будет иметь $val связанный, так что отсрочка работает, и с тех пор это просто переменная. Так как $val переменная имеет известное имя, проблем с кавычками нет, и ее даже можно было бы записать так:

eval $var=\$val

Но так как лучше всегда добавлять кавычки, первое лучше, или даже это:

eval "$var=\"\$val\""

Лучшая альтернатива в bash, которая упоминалась для всего, что позволяет избежать eval полностью (и не так тонко, как declare так далее):

printf -v "$var" "%s" "$val"

Хотя это не прямой ответ на то, что я первоначально спросил...

Bash имеет расширение для printf который сохраняет свой результат в переменную:

printf -v "${VARNAME}" '%s' "${VALUE}"

Это предотвращает все возможные проблемы с выходом.

Если вы используете неверный идентификатор для $VARNAME, команда не выполнится и вернет код состояния 2:

$ printf -v ';;;' foobar; echo $?
bash: printf: `;;;': not a valid identifier
2

Немного лучший способ избежать возможных последствий для безопасности использования eval, является

declare "$var=$val"

Обратите внимание, что declare это синоним typeset в bash, typeset команда более широко поддерживается (ksh а также zsh и пользуйся)

typeset "$var=$val"
eval "$var=\$val"

Аргумент к eval всегда должна быть единственной строкой, заключенной в одинарные или двойные кавычки. Весь код, который отклоняется от этого шаблона, имеет непреднамеренное поведение в крайних случаях, таких как имена файлов со специальными символами.

Когда аргумент eval расширяется оболочкой, $var заменяется именем переменной, а \$ заменяется простым долларом. Строка, которая оценивается, становится:

varname=$value

Это именно то, что вы хотите.

Обычно все выражения вида $varname должны быть заключены в двойные кавычки. Есть только два места, где кавычки могут быть опущены: назначение переменных и case, Поскольку это присваивание переменной, кавычки здесь не нужны. Тем не менее, они не причиняют вреда, поэтому вы также можете написать оригинальный код как:

eval "$var=\"the value is $val\""

Более новые версии bash поддерживают так называемое "преобразование параметров", описанное в разделе с тем же именем в bash(1).

"${value@Q}" расширяется до версии в кавычках "${value}" что вы можете повторно использовать в качестве входных данных.

Это означает, что следующее является безопасным решением:

eval="${varname}=${value@Q}"

Просто для полноты картины я также хочу предложить возможное использование bash, встроенного в read.

Но при использовании чтения необходимо соблюдать особую осторожность, чтобы убедиться, что ввод очищен (-d\0 читает до завершения с нулевым значением, а printf "...\0" завершает значение с нулевым значением), и что само чтение выполняется в основная оболочка, где требуется переменная, а не подоболочка (отсюда и синтаксис <<(...)).

var=x; val=foo
read -d\0 -r $var < <(printf "$val\0")
echo $x  # --> foo
echo ${!var} # -->  foo

Я проверил это с пробелами \ n \ t \ r и т. Д., Как и ожидалось, в моей версии bash.

-R позволит избежать экранирования \, поэтому, если в вашем значении есть символы "\" и "n", а не символ новой строки, x также будет содержать два символа "\" и "n".

Этот метод не может быть эстетически приятным, как решение e val или printf, и будет более полезным, если значение поступает из файла или другого дескриптора входного файла

read -d\0 -r $var < <( cat $file )

Еще один способ сделать это без eval - использовать "read":

      INDIRECT=foo
read -d '' -r "${INDIRECT}" <<<"$(( 2 * 2 ))"
echo "${foo}"  # outputs "4"
Другие вопросы по тегам