Поиск по сайту:

Как использовать eval в скриптах Linux Bash


Из всех команд Bash бедная старая eval, вероятно, имеет худшую репутацию. Оправдано или просто плохая пресса? Мы обсудим использование и опасности этой наименее любимой команды Linux.

Нам нужно поговорить об eval

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

Нечто подобное можно сказать и об автомобилях. В неумелых руках они — смертельное оружие. Люди используют их в таранных рейдах и в качестве транспортных средств для побега. Должны ли мы все перестать пользоваться автомобилями? Нет, конечно нет. Но они должны использоваться правильно и людьми, которые умеют ими управлять.

Обычное прилагательное, применяемое к eval, — «зло». Но все упирается в то, как он используется. Команда eval сопоставляет значения из одной или нескольких переменных. Он создает командную строку. Затем он выполняет эту команду. Это делает его полезным, когда вам нужно справляться с ситуациями, когда содержимое команды извлекается динамически во время выполнения вашего скрипта.

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

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

Первые шаги с eval

Команда eval — это встроенная команда оболочки Bash. Если присутствует Bash, будет присутствовать eval.

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

Давайте создадим переменную с именем wordcount.

wordcount="wc -w raw-notes.md"

Строковая переменная содержит команду для подсчета слов в файле с именем «raw-notes.md».

Мы можем использовать eval для выполнения этой команды, передав ей значение переменной.

eval "$wordcount"

Команда выполняется в текущей оболочке, а не в подоболочке. Мы можем легко показать это. У нас есть короткий текстовый файл с именем «variables.txt». Он содержит эти две строки.

first=How-To
second=Geek

Мы будем использовать cat для отправки этих строк в окно терминала. Затем мы будем использовать eval для оценки команды cat, чтобы выполнить инструкции внутри текстового файла. Это установит переменные для нас.

cat variables.txt
eval "$(cat variables.txt)"
echo $first $second

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

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

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

Использование переменных в командной строке

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

num1=10 
num2=7

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

add="`expr $num1 + $num2`"

Мы создадим еще одну команду, чтобы показать нам результат оператора expr.

show="echo"

Обратите внимание, что нам не нужно включать пробел ни в конец строки echo, ни в начало строки expr. eval позаботится об этом.

И для выполнения всей команды мы используем:

eval $show $add

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

Доступ к переменным внутри переменных

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

Скопируйте этот сценарий в редактор и сохраните его как файл с именем «assign.sh».

#!/bin/bash

title="How-To Geek"
webpage=title
command="echo"
eval $command \${$webpage}

Нам нужно сделать его исполняемым с помощью команды chmod.

chmod +x assign.sh

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

Когда мы запускаем наш скрипт, мы видим текст из переменной title, хотя команда eval использует переменную webpage.

./assign.sh

Экранированный знак доллара «$» и фигурные скобки «{}» заставляют eval просматривать значение, хранящееся внутри переменной, имя которой хранится на веб-странице переменная.

Использование динамически создаваемых переменных

Мы можем использовать eval для динамического создания переменных. Этот скрипт называется «loop.sh».

#!/bin/bash

total=0
label="Looping complete. Total:"

for n in {1..10}
do
  eval x$n=$n
  echo "Loop" $x$n
  ((total+=$x$n))
done

echo $x1 $x2 $x3 $x4 $x5 $x6 $x7 $x8 $x9 $x10

echo $label $total

Он создает переменную с именем total, которая содержит сумму значений созданных нами переменных. Затем он создает строковую переменную с именем label. Это простая строка текста.

Мы собираемся выполнить цикл 10 раз и создать 10 переменных с именами от x1 до x10. Оператор eval в теле цикла предоставляет «x» и принимает значение счетчика цикла $n для создания имени переменной. В то же время он устанавливает новую переменную в значение счетчика цикла $n.

Он выводит новую переменную в окно терминала, а затем увеличивает переменную total на значение новой переменной.

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

Наконец, мы печатаем значение переменной total.

./loop.sh

Использование eval с массивами

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

Вы не хотите, чтобы он просто rm *.log, вам нужно только удалить созданные файлы журнала. Этот скрипт имитирует эту функциональность. Это «clear-logs.sh».

#!/bin/bash

declare -a logfiles

filecount=0 
rm_string="echo"

function create_logfile() {
  ((++filecount))
  filename=$(date +"%Y-%m-%d_%H-%M-%S").log
  logfiles[$filecount]=$filename
  echo $filecount "Created" ${logfiles[$filecount]}
}

# body of the script. Some processing is done here that
# periodically generates a log file. We'll simulate that
create_logfile
sleep 3
create_logfile
sleep 3
create_logfile
sleep 3
create_logfile

# are there any files to remove?
for ((file=1; file<=$filecount; file++))
do
  # remove the logfile
  eval $rm_string ${logfiles[$file]} "deleted..."
  logfiles[$file]=""
done

Сценарий объявляет массив с именем logfiles . Здесь будут храниться имена файлов журналов, созданных сценарием. Он объявляет переменную с именем filecount . Здесь будет храниться количество созданных файлов журнала.

Он также объявляет строку с именем rm_string. В реальном сценарии это будет содержать команду rm , но мы используем echo, поэтому мы можем продемонстрировать принцип неразрушающим образом.

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

Функция увеличивает переменную filecount. Его начальное значение равно нулю, поэтому первое имя файла, которое мы создаем, сохраняется в первой позиции в массиве. Это сделано специально, как об этом далее.

Имя файла создается с помощью команды date и расширения «.log». Имя сохраняется в массиве в позиции, указанной filecount. Имя печатается в окне терминала. В реальном сценарии вы также должны создать реальный файл.

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

Наконец, есть цикл, который удаляет файлы журнала. Файл счетчика циклов установлен в единицу. Он считает до значения filecount включительно, которое содержит количество созданных файлов.

Если filecount по-прежнему равен нулю (поскольку файлы журналов не создавались), тело цикла никогда не будет выполнено, поскольку единица не меньше или равна нулю. Вот почему переменная filecount была установлена равной нулю при ее объявлении и почему она была увеличена перед созданием первого файла.

Внутри цикла мы используем eval с нашим неразрушающим rm_string и именем файла, который извлекается из массива. Затем мы устанавливаем элемент массива в пустую строку.

Это то, что мы видим, когда запускаем скрипт.

./clear-logs.sh

Не все так плохо

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

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