it-roy-ru.com

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

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

У меня все еще есть проблемы с пониманием того, что именно делает команда 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

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

137
kstratis

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

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

echo ${!n}
one

$(…) запускает команду, указанную в круглых скобках в подоболочке (т.е. в отдельном процессе, который наследует все параметры, такие как значения переменных из текущей оболочки), и собирает ее выходные данные. Таким образом, echo $($n) запускает $n как команду Shell и отображает ее вывод. Поскольку $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 по-прежнему полезен, когда вам нужно создать более длинную команду, содержащую операторы, зарезервированные слова и т. д.

175
Gilles

Просто думайте о 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 и риски, связанные с этим.

32
Hari Menon

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

Возможно, у вас есть система, которая использует набор переменных среды, и у вас есть сценарий или программа, которая определяет, какие из них следует установить, и их значения. Каждый раз, когда вы запускаете скрипт или программу, он запускается в разветвленном процессе, поэтому все, что он делает непосредственно с переменными среды, теряется при выходе. Но этот скрипт или программа может отправлять команды экспорта в 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!
21
sootsnoot

Оператор 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 >
7
Nikhil Gupta

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


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

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

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

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



Пример 2: 

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

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

Если бы вы были echo $key=$val, результат будет:

mykey=myval

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

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

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

2
clearlight

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

1
Luke Pafford

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

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

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

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

0
Craig Hicks

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

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

0
flabdablet

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

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
YoYo

В вопросе:

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

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

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

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

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

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

who | grep $(echo pts/3)

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

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

0
Carmin