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

165

После прочтения справочных страниц 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

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

kstratis
источник
1
Для записи, вторая попытка работает. $($n)работает $nв подрубрике. Он пытается выполнить команду, 1которая не существует.
Мартин Уикман,
1
@MartinWickman Но требование в echo $1конечном итоге запустить , а не 1. Я не думаю, что это можно сделать с помощью подоболочек.
Хари Менон
5
Вы должны знать о последствиях безопасности использованияeval .
Приостановлено до дальнейшего уведомления.
1
@ Raze2dust: Я не думаю, что он предполагал, что он может быть запущен с подоболочками, а скорее объясняю, почему 5-я команда из перечисленного OP не сработала.
jedwards

Ответы:

196

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все еще полезно, когда вам нужно создать более длинную команду, содержащую операторы, зарезервированные слова и т. д.

Жиль "ТАК - перестань быть злым"
источник
Что касается моего комментария выше, сколько «проходов» делает eval?
kstratis
@ Konos5 Какой комментарий? evalполучает строку (которая сама может быть результатом анализа и анализа) и интерпретирует ее как фрагмент кода.
Жиль "ТАК - перестать быть злым"
Под ответом Raze2dust я оставил комментарий. Теперь я склонен полагать, что eval в основном используется для разыменования. Если я наберу eval echo \ $ {$ n}, я получу его. Однако, если я наберу echo \ $ {$ n}, я получу \ $ {1}. Я полагаю, что это происходит из-за "двухпроходного" анализа eval. Теперь мне интересно, что произойдет, если мне понадобится утроить разыменование с помощью дополнительного объявления i = n. В этом случае согласно Raze2dust мне просто нужно поставить дополнительный eval. Однако я считаю, что должен быть лучший способ ... (он может быть легко загроможден)
kstratis
@ Konos5 Я бы не стал использовать eval eval. Я не помню, чтобы когда-либо чувствовал потребность. Если вам действительно нужно два evalпрохода, использовать временную переменную, это будет проще для отладки: eval tmp="\${$i}"; eval x="\${$tmp}".
Жиль "ТАК - перестань быть злым"
1
@ Konos5 "Разбор дважды" немного вводит в заблуждение. Некоторые люди могут поверить в это из-за сложности указания аргумента буквенной строки в Bash, который защищен от различных расширений. evalпросто берет код в строке и оценивает его по обычным правилам. Технически, это даже не правильно, потому что есть несколько угловых случаев, в которых Bash изменяет синтаксический анализ, чтобы даже не выполнять расширения аргументов eval - но это очень туманный лакомый кусочек, о котором, я сомневаюсь, кто-нибудь знает.
ormaaj
39

Просто думайте о 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
Как примечание для первой пули, то ${VAR}синтаксис будет разрешен, и предпочтительно , когда есть какие - либо неоднозначности (делает $VAR == $V, с последующим ARили $VAR == $VAпоследующим R). ${VAR}эквивалентно $VAR. На самом деле, это имя переменной $n, которое не допускается.
jedwards
2
eval eval echo \\\${\${$i}}сделаем тройную разыменование. Я не уверен, есть ли более простой способ сделать это. Кроме того, \${$n}отлично работает (печатает one) на моей машине ..
Хари Менон
2
@ Konos5 echo \\\${\${$i}}печатает \${${n}}. eval echo \\\${\${$i}}эквивалентно echo \${${n}}`` and prints $ {1} . eval eval echo \\\ $ {\ $ {$ i}} `эквивалентно eval echo ${1}и печатает one.
Жиль "ТАК - перестань быть злым"
2
@ Konos5 Думайте в том же духе - первое ` escapes the second one, and the third ` $после этого ускользает . Так делается \${${n}}после одного раунда оценки
Хари Менон
2
@ Konos5 Слева направо - правильный способ анализа цитат и обратной косой черты. Сначала \\ получим одну обратную косую черту. Тогда \$уступая один доллар. И так далее.
Жиль "ТАК ... перестать быть злым"
25

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

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

Без eval вам нужно будет перенаправить стандартный вывод во временный файл, создать временный файл и затем удалить его. С помощью 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!
sootsnoot
источник
есть примеры распространенных инструментов, которые делают это? У самого инструмента есть средство для создания набора команд оболочки, которые можно передать в eval?
Йоаким Эрдфельт
@Joakim Я не знаю ни одного инструмента с открытым исходным кодом, который бы делал это, но он использовался в некоторых частных сценариях в компаниях, где я работал. Я просто снова начал использовать эту технику с xampp. Apache .conf файлы расширяют написанные переменные среды ${varname}. Я считаю удобным использовать идентичные файлы .conf на нескольких разных серверах с несколькими параметрами, параметризованными переменными среды. Я отредактировал / opt / lampp / xampp (который запускает apache), чтобы сделать этот тип eval с помощью скрипта, который копается в системе и выводит exportоператоры bash для определения переменных для файлов .conf.
Sootsnoot
@Joakim Альтернативой будет иметь скрипт для генерации каждого из затронутых файлов .conf из шаблона, основываясь на том же поиске. Одна вещь, которая мне нравится больше в моем пути, это то, что запуск apache без прохождения / opt / lampp / xampp не использует устаревшие сценарии вывода, а скорее не запускается, потому что переменные окружения расширяются до нуля и создают недопустимые директивы.
Sootsnoot
@Anthony Sottile Я вижу, что вы отредактировали ответ, добавив кавычки около $ (скрипт или программа), сказав, что они были важны при выполнении нескольких команд. Можете ли вы привести пример - следующее работает отлично с командами, разделенными точкой с запятой в stdout файла foo.sh: echo '#! / Bin / bash'> foo.sh; echo 'echo' echo -na; echo -nb; echo -n c "'>> foo.sh; chmod 755 foo.sh; eval $ (./ foo.sh). Это производит ABC на стандартный вывод. Запуск ./foo.sh производит: echo -na; эхо-нб; echo -nc
sootsnoot
1
Для примера распространенного инструмента, который использует eval, смотрите pyenv . pyenv позволяет легко переключаться между несколькими версиями Python. Вы помещаете eval "$(pyenv init -)"в свой .bash_profile(или аналогичный) файл конфигурации оболочки. Это создает небольшой сценарий оболочки, а затем оценивает его в текущей оболочке.
Jerry101
10

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

В вашем скрипте, если вы определяете команду в переменную, а затем хотите использовать эту команду, тогда вы должны использовать 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 >
Нихил Гупта
источник
4

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


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

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

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

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

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

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

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


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

Пример 1:

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

$ eval a=b
$ eval echo $a
b

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



Пример 2:

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

$ 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, нулевым, и, если это так, запрашивает у пользователя значение.

ClearLight
источник
3

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

Если у вас есть задания 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 в этом случае небезопасно, потому что мы точно знаем, что будем оценивать заранее.

Люк Паффорд
источник
2

Мне недавно пришлось использовать, 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.

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

flabdablet
источник
1

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

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

Но на самом деле, через «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)

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

Крейг Хикс
источник
Говорить, что «есть больше возможностей» - это, по меньшей мере, тривиально, тривиально и излишне.
dotbit
0

Мне нравится ответ «оцениваю свое выражение за один раз до исполнения», и я хотел бы уточнить на другом примере.

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исправит это.

Адаптировано с https://stackoverflow.com/a/40646371/744133

Йо Йо
источник
0

В вопросе:

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

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

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

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

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

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

who | grep $(echo pts/3)

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

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

Павел
источник