Как присвоить пространственные значения переменным в bash, используя eval

19

Я хочу динамически присваивать значения переменным, используя eval. Следующий фиктивный пример работает:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

Однако когда значение переменной содержит пробелы, evalвозвращается ошибка, даже если $var_valueона заключена в двойные кавычки:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

Есть ли способ обойти это?

Себастьян Климент
источник

Ответы:

13

Не используйте eval, используйте declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
Гленн Джекман
источник
11

Не использовать eval для этого; использовать declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Обратите внимание, что разделение слов не является проблемой, потому что все, что следует за =ним, рассматривается как значение declare, а не только по первому слову.

В bash 4.3 именованные ссылки делают это немного проще.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

Вы можете сделать evalработу, но вы все равно не должны :) Использовать evalэто вредная привычка.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"
chepner
источник
2
Использовать eval этот путь неправильно. Вы расширяете, $var_valueпрежде чем передать его, evalчто означает, что он будет интерпретироваться как шелл-код! (попробуйте, например, с var_value="';:(){ :|:&};:'")
Стефан Шазелас
1
Хорошая точка зрения; Есть некоторые строки, которые вы не можете назначить с помощью eval(это одна из причин, по которой я сказал, что вы не должны использовать eval).
Чепнер
@chepner - я не верю, что это правда. может быть, но не этот, по крайней мере. Подстановка параметров допускает условное расширение, поэтому в большинстве случаев, я думаю, вы можете расширять только безопасные значения. Тем не менее, ваша главная проблема для $var_value- это инверсия кавычек - если принять безопасное значение для $var_name (что на самом деле может быть столь же опасным, как и предположение) , тогда вы должны заключать двойные кавычки правой части в одинарные кавычки - не наоборот.
mikeserv
Я думаю, что я исправил eval, используя printfи его bashспецифичный %qформат. Это все еще не рекомендация для использования eval, но я думаю, что это безопаснее, чем было раньше. Тот факт, что вам нужно приложить столько усилий, чтобы заставить его работать, является доказательством того, что вы должны использовать declareили использовать именованные ссылки.
Чепнер
Ну, на самом деле, на мой взгляд, именные ссылки являются проблемой. Лучший способ использовать его - по моему опыту - это как ... set -- a bunch of args; eval "process2 $(process1 "$@")"где process1просто печатать цитируемые числа, как "${1}" "${8}" "${138}". Это безумно просто - и так же просто, как и '"${'$((i=$i+1))'}" 'в большинстве случаев. Индексированные ссылки делают его безопасным, надежным и быстрым . Тем не менее - я проголосовал.
mikeserv
4

Хороший способ работы с ним eval- заменить его echoдля тестирования. echoи evalработать так же (если мы отложим \xрасширение, сделанное некоторымиecho реализациями, подобными bash's, при некоторых условиях).

Обе команды объединяют свои аргументы с одним пробелом между ними. Разница в том, что echo отображает результат, в то время как eval оценивает / интерпретирует результат как оболочку.

Итак, чтобы увидеть, что код оболочки

eval $(echo $var_name=$var_value)

оцените, вы можете запустить:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

Это не то, что вы хотите, а то, что вы хотите:

fruit=$var_value

Кроме того, использование $(echo ...)здесь не имеет смысла.

Чтобы вывести выше, вы должны запустить:

$ echo "$var_name=\$var_value"
fruit=$var_value

Итак, чтобы интерпретировать это, это просто:

eval "$var_name=\$var_value"

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

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

Как уже говорили другие, если вам не нужен bashконкретный код , вы можете использовать его declareкак:

declare "$var_name=$var_value"

Однако обратите внимание, что у него есть некоторые побочные эффекты.

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

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Потому что это объявило бы fooпеременную, локальную для setvarso, и было бы бесполезно.

bash-4.2добавлена -gопция для declareобъявить глобальную переменную, но это не то , что мы хотим , или как наши setvarбы установить глобальный вар в противоположность тому , что вызывающего абонента , если вызывающий абонент является функцией, как в:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

который бы вывел:

1:
2: some value

Также обратите внимание, что, хотя declareвызывается declare(фактически bashзаимствовано понятие из typesetвстроенной оболочки Korn ), если переменная уже установлена,declare не объявляет новую переменную, и способ ее назначения зависит от типа переменной.

Например:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

приведет к другому результату (и потенциально может иметь неприятные побочные эффекты), если он varnameбыл ранее объявлен как скаляр , массив или ассоциативный массив .

Стефан Шазелас
источник
2
Что плохого в том, чтобы быть специфичным для Bash? ОП поставил на вопрос тег bash, поэтому он использует bash. Предоставлять альтернативы - это хорошо, но я думаю, что говорить кому-то не использовать функцию оболочки, потому что она не переносима, глупо.
Патрик
@ Патрик, видел смайлик? Сказав это, использование переносимого синтаксиса означает меньше усилий, когда вам нужно перенести свой код в другую систему, bashкоторая недоступна (или когда вы понимаете, что вам нужна лучшая / более быстрая оболочка). В evalсинтаксических работает во всех Bourne-подобных оболочек и является POSIX , поэтому все системы будут иметь , shгде это работает. (это также означает, что мой ответ применим ко всем оболочкам, и рано или поздно, как это часто случается здесь, вы увидите, что вопрос, не относящийся к bash, закрыт как дубликат этого вопроса.
Стефан Шазелас
но что если $var_nameсодержит токены? ... как ;?
mikeserv
@mikeserv, тогда это не имя переменной. Если вы не можете доверять его содержание, то вам необходимо дезинфицировать его с обоими evalи declare(думаю PATH, TMOUT, PS4, SECONDS...).
Стефан Шазелас
но на первом проходе это всегда расширение переменной и никогда не имя переменной до второго. в своем ответе я санирую его расширением параметров, но если вы подразумеваете выполнение санации в подоболочке на первом проходе, это также можно сделать переносимо с / export. Я не следую за скобками в конце, хотя.
mikeserv
1

Если вы делаете:

eval "$name=\$val"

... и $nameсодержит; - или любой из нескольких других токенов, которые оболочка может интерпретировать как разграничение простой команды - с предшествующим надлежащим синтаксисом оболочки, который будет выполнен.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

ВЫХОД

hi
be careful with eval

Однако иногда можно разделить оценку и выполнение таких утверждений. Например, aliasможет использоваться для предварительной оценки команды. В следующем примере определение переменной сохраняется в aliasтом, что может быть успешно объявлено, только если $nmоцениваемая переменная не содержит байтов, которые не соответствуют буквенно-цифровым или ASCII-символам _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

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

Однако определение в aliasfor _$nmпредназначено для предотвращения перезаписи значимых значений среды. Я не знаю каких-либо заслуживающих внимания значений окружения, начинающихся с a, _и обычно это безопасная ставка для полу-частного объявления.

В любом случае, если aliasопределение будет успешным, оно объявит aliasимя для $nmзначения. И evalбудет вызывать только это, aliasесли также не начинается с числа - иначе evalполучает только нулевой аргумент. Таким образом, если оба условия выполняются, evalвызывается псевдоним и создается определение переменной, сохраненное в псевдониме, после чего новое aliasбыстро удаляется из хеш-таблицы.

mikeserv
источник
;не допускается в именах переменных. Если у вас нет контроля над содержимым $name, то вам необходимо очистить его и для export/ declare. Хотя exportкод не выполняется, установка некоторых переменных, таких как PATH, PS4и многих из них info -f bash -n 'Bash Variables'имеет одинаково опасные побочные эффекты.
Стефан Шазелас
@ StéphaneChazelas - конечно, это не разрешено, но, как и прежде, это не имя переменной на evalпервом проходе - это расширение переменной. Как вы сказали в другом месте, в этом контексте это очень разрешено. Тем не менее, аргумент $ PATH очень хороший - я сделал небольшое редактирование и добавлю немного позже.
mikeserv
@ StéphaneChazelas - лучше поздно, чем никогда ...?
mikeserv
На практике zsh, pdksh, mksh, yashне жалуются на unset 'a;b'.
Стефан Шазелас
Тебе тоже захочется unset -v -- ....
Стефан Шазелас