команда eval в Bash та її типові види використання


165

Після прочитання сторінок bash man та щодо цієї публікації .

У мене все ще виникають проблеми з розумінням того, що саме 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
Для запису працює друга спроба. $($n)працює $nв передпласті. Він намагається запустити команду, 1якої не існує.
Мартін Вікман

1
@MartinWickman Але вимога полягає в запуску echo $1зрештою, а не 1. Я не думаю, що це можна зробити за допомогою допоміжних оболонок.
Харі Менон


1
@ Raze2dust: Я не вірю, що він припускав, що це може бути запущено з підзаголовками, але, швидше, пояснює, чому 5-та команда, перелічена в ОП не працює.
jedwards

Відповіді:


196

evalприймає рядок як свій аргумент і оцінює його так, як ніби ви ввели цей рядок у командному рядку. (Якщо ви передаєте кілька аргументів, їх спочатку з'єднують з пробілами між ними.)

${$n}- синтаксична помилка в базі. Всередині брекетів ви можете мати лише ім’я змінної з можливими префіксами та суфіксами, але ви не можете мати синтаксис довільного 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}. Я вважаю, що це відбувається завдяки синтаксичному розбору "двох проходів". Мені зараз цікаво, що трапиться, якщо мені потрібно потрійне повалення, використовуючи додаткове i = n оголошення. У цьому випадку відповідно до Raze2dust мені просто потрібно поставити додаткову оцінку. Однак я вважаю, що має бути кращий спосіб ... (він може легко захараститися)
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після першого раунду оцінювання. Три зміни, які слід помітити:

  • The \$став $(зворотний косий рядок потрібен, інакше він намагається оцінити ${$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
@ Конос5 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, вам потрібно буде перенаправити 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!

є якісь приклади поширених інструментів, які це роблять? Сам інструмент має засоби для створення набору команд оболонки, які можна передавати в 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 на stdout. Запуск ./foo.sh виробляє: echo -na; відлуння -nb; echo -nc
sootsnoot

1
Приклад поширеного інструменту, що використовує eval, див. Pyenv . pyenv дозволяє легко перемикатися між декількома версіями Python. Ви поміщаєте eval "$(pyenv init -)"у свій .bash_profile(або подібний) конфігураційний файл оболонки. Це будує невеликий сценарій оболонки, а потім оцінює його в поточній оболонці.
Джеррі101

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 у будь-якому випадку. Ця відповідь stackoverflow пояснює ризики eval та альтернативи eval. Зрештою, користувач повинен визначити, чи / коли eval безпечний та ефективний у використанні.


Оператор bash evalдозволяє виконувати рядки коду, обчислені чи придбані, за допомогою вашого bash script.

Мабуть, найпростішим прикладом може бути програма bash, яка відкриває інший bash-скрипт у вигляді текстового файлу, читає кожен рядок тексту та використовує evalдля їх виконання по порядку. Це, по суті, така ж поведінка, як і bash- sourceоператор, для чого можна було б скористатися, якщо тільки не було необхідності здійснити якусь трансформацію (наприклад, фільтрування чи заміну) вмісту імпортованого сценарію.

Мені це рідко потрібно 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:

Виконуйте призначення var, використовуючи імена var, передані як значення рядка.

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

Якби ви були echo $key=$val, вихід був би:

mykey=myval

Це , будучи кінцевим результатом розбору рядків, - це те, що буде виконано eval, отже, результат констатації ехо в кінці ...



Приклад 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. (наступні рядки описові, не точний баш-код):

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


3

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

Якщо у вас є завдання cron, які потрібно запустити в інтерактивному режимі для тестування, ви можете переглянути вміст файлу разом з cat та скопіювати та вставити завдання cron для його запуску. На жаль, це стосується дотику до миші, що є гріхом у моїй книзі.

Скажімо, у вас є робота з Cron у /etc/cron.d/repeatme із вмістом:

*/10 * * * * root program arg1 arg2

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

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

Команда cut виводить лише 6-е поле файлу, розділене пробілами. Потім Евал виконує цю команду.

Я використав тут завдання 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.

Можливо, існує якась хитра схема, що включає вкладені розширення дужок, що дозволяє це статися за один крок, але якщо є, я занадто стара і дурна, щоб це бачити.


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)

У цьому вихідному тексті майже необмежена ширина смуги! І є більше можливостей, якщо використовуються декілька вихідних рядків: наприклад, перший рядок може використовуватися для призначення змінних, другий для безперервного "потоку думок", але це виходить за межі цієї публікації.


сказати, що є "більше можливостей", це дрібниця, дрібниця, а найменше - зайве.
дотбіт

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 є те, що ми пропустили б два параметри наступним чином:

  • Перший параметр: "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 - це не нова хвильова програма, написана рекурсивно. Натомість, bash - це стара програма 1,2,3, до якої додаються функції. Для забезпечення зворотної сумісності початковий спосіб тлумачення ніколи не змінювався. Якщо bash був переписаний на круглі дужки першого збігу, скільки помилок було б введено в скільки програм bash? Багато програмістів люблять бути дурними.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.