Команда eval в Bash и ее типичное использование

После чтения справочных страниц bash и в отношении этого сообщения.

Мне все еще трудно понять, что именно делает команда eval и которая будет ее типичным использованием. Например, если мы делаем:

bash$ set -- one two three  # sets $1 $2 $3
bash$ echo $1
one
bash$ n=1
bash$ echo ${$n}       ## First attempt to echo $1 using brackets fails
bash: ${$n}: bad substitution
bash$ echo $($n)       ## Second attempt to echo $1 using parentheses fails
bash: 1: command not found
bash$ eval echo \${$n} ## Third attempt to echo $1 using 'eval' succeeds
one

Что именно происходит здесь и как знак доллара и обратная косая черта связаны с проблемой?

Ответ 1

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

${$n} является синтаксической ошибкой в ​​ bash. Внутри фигурных скобок вы можете иметь только имя переменной с некоторыми префиксами и суффиксами, но вы не можете иметь произвольный синтаксис bash и, в частности, вы не можете использовать расширение переменной. Существует способ сказать "значение переменной, имя которой находится в этой переменной", хотя:

echo ${!n}
one

$(…) запускает команду, указанную в круглых скобках в подоболочке (т.е. в отдельном процессе, который наследует все параметры, такие как значения переменных из текущей оболочки) и собирает свой вывод. Итак, echo $($n) запускает $n в качестве команды оболочки и отображает его вывод. Поскольку $n оценивается как 1, $($n) пытается запустить команду 1, которая не существует.

eval echo \${$n} запускает параметры, переданные в eval. После расширения параметры echo и ${1}. Итак, eval echo \${$n} запускает команду echo ${1}.

Обратите внимание, что большую часть времени вы должны использовать двойные кавычки вокруг подстановок переменных и подчиненных команд (т.е. там, где есть $): "$foo", "$(foo)". Всегда ставьте двойные кавычки вокруг переменных и подстановок команд, если только вы не знаете, что вам нужно их оставить. Без двойных кавычек оболочка выполняет разделение поля (т.е. Разделяет значение переменной или вывод из команды на отдельные слова), а затем обрабатывает каждое слово как шаблон подстановочного знака. Например:

$ ls
file1 file2 otherfile
$ set -- 'f* *'
$ echo "$1"
f* *
$ echo $1
file1 file2 file1 file2 otherfile
$ n=1
$ eval echo \${$n}
file1 file2 file1 file2 otherfile
$eval echo \"\${$n}\"
f* *
$ echo "${!n}"
f* *

eval не используется очень часто. В некоторых оболочках наиболее распространенным является получение значения переменной, имя которой неизвестно до времени выполнения. В bash это не обязательно благодаря синтаксису ${!VAR}. eval по-прежнему полезен, когда вам нужно построить более длинную команду, содержащую операторы, зарезервированные слова и т.д.

Ответ 2

Просто подумайте об eval как о "оценке вашего выражения за одно дополнительное время до выполнения"

eval echo \${$n} становится echo $1 после первого раунда оценки. Три изменения:

  • \$ стал $ (нужна обратная косая черта, в противном случае она пытается оценить ${$n}, что означает переменную с именем {$n}, которая недопустима)
  • $n оценивалась как 1
  • eval исчез

Во втором раунде это в основном echo $1, которое может быть выполнено непосредственно.

Итак, eval <some command> сначала оценит <some command> (путем оценки здесь я имею в виду заменяющие переменные, заменяю экранированные символы правильными и т.д.), а затем снова запустите результирующее выражение.

eval используется, когда вы хотите динамически создавать переменные или читать выходы из программ, специально предназначенных для чтения следующим образом. См. http://mywiki.wooledge.org/BashFAQ/048. Ссылка также содержит некоторые типичные способы использования eval и связанные с ней риски.

Ответ 3

По моему опыту, "типичное" использование eval - это запуск команд, которые генерируют команды оболочки для установки переменных среды.

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

Без eval вам нужно будет перенаправить stdout в временный файл, указать временный файл и затем удалить его. С помощью eval вы можете просто:

eval "$(script-or-program)"

Обратите внимание, что цитаты важны. Возьмите этот (надуманный) пример:

# activate.sh
echo 'I got activated!'

# test.py
print("export foo=bar/baz/womp")
print(". activate.sh")

$ eval $(python test.py)
bash: export: `.': not a valid identifier
bash: export: `activate.sh': not a valid identifier
$ eval "$(python test.py)"
I got activated!

Ответ 4

Оператор eval сообщает оболочке принимать аргументы evals как команду и запускать их через командную строку. Это полезно в ситуации, как показано ниже:

В вашем script, если вы определяете команду в переменной, а позже вы хотите использовать эту команду, вы должны использовать eval:

/home/user1 > a="ls | more"
/home/user1 > $a
bash: command not found: ls | more
/home/user1 > # Above command didn't work as ls tried to list file with name pipe (|) and more. But these files are not there
/home/user1 > eval $a
file.txt
mailids
remote_cmd.sh
sample.txt
tmp
/home/user1 >

Ответ 5

Обновление: некоторые люди говорят, что нужно - никогда не использовать eval.Я не согласен.Я думаю, что риск возникает, когда коррумпированный ввод может быть передан eval.Однако существует много распространенных ситуаций, когда это не риск, и поэтому стоит знать, как использовать eval в любом случае.Этот fooobar.com/questions/5886/... объясняет риски eval и альтернативы eval.В конечном счете, пользователь должен определить, безопасно ли и когда eval безопасен и эффективен.


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

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

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

eval концептуально прост. Однако строгий синтаксис языка bash и порядок разбора интерпретатора bash могут быть нюансированы и сделать eval кажутся загадочными и трудными в использовании или понимании. Вот основные положения:

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

  2. Синтаксис и порядок синтаксического анализа строгие. Если результат не является исполняемой строкой кода bash, в рамках вашего сценария программа выйдет из eval выражении eval когда пытается выполнить мусор.

  3. При тестировании вы можете заменить выражение eval echo и посмотреть, что отображается. Если это законный код в текущем контексте, выполнение его через eval будет работать.


Следующие примеры могут помочь выяснить, как работает eval...

Пример 1:

eval перед "нормальным" кодом является NOP

$ eval a=b
$ eval echo $a
b

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



Пример 2:

Выполните назначение var с использованием имен переменных, переданных как строковые значения.

$ key="mykey"
$ val="myval"
$ eval $key=$val
$ echo $mykey
myval

Если бы вы echo $key=$val, выход был бы следующим:

mykey=myval

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



Пример 3:

Добавление большего количества ссылок на пример 2

$ keyA="keyB"
$ valA="valB"
$ keyB="that"
$ valB="amazing"
$ eval eval \$$keyA=\$$valA
$ echo $that
amazing

Вышеприведенное несколько сложнее, чем предыдущий пример, в большей степени полагаясь на порядок синтаксического анализа и особенности bash. Строка eval будет грубо обрабатываться внутренне в следующем порядке (обратите внимание, что следующие утверждения представляют собой псевдокод, а не настоящий код, чтобы попытаться показать, как инструкция будет разбита на внутренние шаги, чтобы получить конечный результат).

 eval eval \$$keyA=\$$valA  # substitution of $keyA and $valA by interpreter
 eval eval \$keyB=\$valB    # convert '$' + name-strings to real vars by eval
 eval $keyB=$valB           # substitution of $keyB and $valB by interpreter
 eval that=amazing          # execute string literal 'that=amazing' by eval

Если принятый порядок синтаксического анализа не объясняет, что делает eval, третий пример может более подробно описать синтаксический анализ, чтобы разъяснить, что происходит.



Пример 4:

Узнайте, содержат ли строки, чьи имена содержатся в строках, строковые значения.

a="User-provided"
b="Another user-provided optional value"
c=""

myvarname_a="a"
myvarname_b="b"
myvarname_c="c"

for varname in "myvarname_a" "myvarname_b" "myvarname_c"; do
    eval varval=\$$varname
    if [ -z "$varval" ]; then
        read -p "$varname? " $varname
    fi
done

На первой итерации:

varname="myvarname_a"

Bash анализирует аргумент eval, и eval видит это буквально во время выполнения:

eval varval=\$$myvarname_a

Следующий псевдокод пытается проиллюстрировать, как bash интерпретирует вышеприведенную строку реального кода, чтобы получить окончательное значение, выполняемое eval. (следующие строковые описания, а не точный код bash):

1. eval varval="\$" + "$varname"      # This substitution resolved in eval statement
2. .................. "$myvarname_a"  # $myvarname_a previously resolved by for-loop
3. .................. "a"             # ... to this value
4. eval "varval=$a"                   # This requires one more parsing step
5. eval varval="User-provided"        # Final result of parsing (eval executes this)

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

varval="User-provided"

Оставшийся код в приведенном выше примере просто проверяет, имеет ли значение, присвоенное $ varval значение null, и, если это так, предлагает пользователю указать значение.

Ответ 6

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

Если у вас есть задания cron, которые вы хотите запустить интерактивно для тестирования, вы можете просмотреть содержимое файла с помощью cat, а также скопировать и вставить задание cron для его запуска. К сожалению, это касается касания мыши, что является грехом в моей книге.

Допустим, у вас есть работа cron в /etc/cron.d/repeatme с содержимым:

*/10 * * * * root program arg1 arg2

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

eval $( cut -d ' ' -f 6-/etc/cron.d/repeatme)

Команда cut выдает только 6-е поле файла, ограниченное пробелами. Затем Eval выполняет эту команду.

В качестве примера я использовал работу cron, но концепция заключается в форматировании текста из stdout, а затем для оценки этого текста.

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

Ответ 7

Мне нравится "оценка вашего выражения за одно дополнительное время перед исполнением", и я хотел бы пояснить в другом примере.

var="\"par1 par2\""
echo $var # prints nicely "par1 par2"

function cntpars() {
  echo "  > Count: $#"
  echo "  > Pars : $*"
  echo "  > par1 : $1"
  echo "  > par2 : $2"

  if [[ $# = 1 && $1 = "par1 par2" ]]; then
    echo "  > PASS"
  else
    echo "  > FAIL"
    return 1
  fi
}

# Option 1: Will Pass
echo "eval \"cntpars \$var\""
eval "cntpars $var"

# Option 2: Will Fail, with curious results
echo "cntpars \$var"
cntpars $var

Любопытные результаты в Варианте 2 состоят в том, что мы бы передали 2 параметра следующим образом:

  • Первый параметр: "value
  • Второй параметр: content"

Как это для счетчика интуитивно понятным? Дополнительный eval будет исправлять это.

Адаптировано из fooobar.com/questions/52041/...

Ответ 8

В вопросе:

who | grep $(tty | sed s:/dev/::)

выводит ошибки, утверждая, что файлы a и tty не существуют. Я понял, что это означает, что tty не интерпретируется перед выполнением grep, но вместо этого bash передал tty в качестве параметра grep, который интерпретировал его как имя файла.

Существует также ситуация вложенного перенаправления, которая должна обрабатываться согласованными скобками, которые должны указывать дочерний процесс, но bash является примитивным разделителем слов, создавая параметры для отправки в программу, поэтому круглые скобки не сопоставляются сначала, но интерпретируется как увиденный.

Я определился с grep и задал файл как параметр вместо использования канала. Я также упростил базовую команду, передав вывод из команды в виде файла, так что i/o piping не будет вложен:

grep $(tty | sed s:/dev/::) <(who)

работает хорошо.

who | grep $(echo pts/3)

на самом деле не требуется, но устраняет вложенную трубу и также хорошо работает.

В заключение, bash, похоже, не похож на вложенное pipping. Важно понимать, что bash не является новой волновой программой, написанной рекурсивным образом. Вместо этого bash представляет собой старую программу 1,2,3, которая добавлена ​​с функциями. В целях обеспечения обратной совместимости первоначальный способ интерпретации никогда не был изменен. Если bash было переписано в скобки первого совпадения, сколько ошибок было бы введено во сколько программ bash? Многие программисты любят быть загадочными.

Ответ 9

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

xargs -I_ cat _/{11..15}/{8..5}.jpg

расширяется до

xargs -I_ cat _/11/8.jpg _/11/7.jpg _/11/6.jpg _/11/5.jpg _/12/8.jpg _/12/7.jpg _/12/6.jpg _/12/5.jpg _/13/8.jpg _/13/7.jpg _/13/6.jpg _/13/5.jpg _/14/8.jpg _/14/7.jpg _/14/6.jpg _/14/5.jpg _/15/8.jpg _/15/7.jpg _/15/6.jpg _/15/5.jpg

но мне понадобилось второе расширение скобки, сделанное первым, уступая

xargs -I_ cat _/11/8.jpg _/12/8.jpg _/13/8.jpg _/14/8.jpg _/15/8.jpg _/11/7.jpg _/12/7.jpg _/13/7.jpg _/14/7.jpg _/15/7.jpg _/11/6.jpg _/12/6.jpg _/13/6.jpg _/14/6.jpg _/15/6.jpg _/11/5.jpg _/12/5.jpg _/13/5.jpg _/14/5.jpg _/15/5.jpg

Лучшее, что я мог придумать, чтобы это сделать

xargs -I_ cat $(eval echo _/'{11..15}'/{8..5}.jpg)

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

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

Ответ 10

Вы спрашивали о типичных целях.

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

Но на самом деле, через "eval", вы можете пройти по ссылке. Вызывающий может передать список назначений переменных, которые будут оцениваться вызывающим. Он передается по ссылке, потому что вызывающий может разрешать указывать имя (имена) результирующей переменной (ей) - см. Пример ниже. Результатам ошибок могут быть возвращены стандартные имена, такие как errno и errstr.

Вот пример прохождения по ссылке в bash:

#!/bin/bash
isint()
{
    re='^[-]?[0-9]+$'
    [[ $1 =~ $re ]]
}

#args 1: name of result variable, 2: first addend, 3: second addend 
iadd()
{
    if isint ${2} && isint ${3} ; then
        echo "$1=$((${2}+${3}));errno=0"
        return 0
    else
        echo "errstr=\"Error: non-integer argument to iadd $*\" ; errno=329"
        return 1
    fi
}

var=1
echo "[1] var=$var"

eval $(iadd var A B)
if [[ $errno -ne 0 ]]; then
    echo "errstr=$errstr"
    echo "errno=$errno"
fi
echo "[2] var=$var (unchanged after error)"

eval $(iadd var $var 1)
if [[ $errno -ne 0 ]]; then
    echo "errstr=$errstr"
    echo "errno=$errno"
fi  
echo "[3] var=$var (successfully changed)"

Результат выглядит следующим образом:

[1] var=1
errstr=Error: non-integer argument to iadd var A B
errno=329
[2] var=1 (unchanged after error)
[3] var=2 (successfully changed)

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