Почему в Bash следует избегать eval и что использовать вместо этого?

107

Снова и снова я вижу ответы Bash об использовании переполнения стека, evalи ответы получаются резкими, каламбурными, за использование такой «злой» конструкции. Почему evalтакое зло?

Если evalиспользовать безопасно, что я должен использовать вместо этого?

Зенексер
источник

Ответы:

148

В этой проблеме больше, чем кажется на первый взгляд. Начнем с очевидного: evalпотенциально может выполнять «грязные» данные. Грязные данные - это любые данные, которые не были переписаны как безопасные для использования в ситуации-XYZ; в нашем случае это любая строка, которая не была отформатирована для обеспечения безопасности при оценке.

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

function println
{
    # Send each element as a separate argument, starting with the second element.
    # Arguments to printf:
    #   1 -> "$1\n"
    #   2 -> "$2"
    #   3 -> "$3"
    #   4 -> "$4"
    #   etc.

    printf "$1\n" "${@:2}"
}

function error
{
    # Send the first element as one argument, and the rest of the elements as a combined argument.
    # Arguments to println:
    #   1 -> '\e[31mError (%d): %s\e[m'
    #   2 -> "$1"
    #   3 -> "${*:2}"

    println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit "$1"
}

# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).

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

function println
{
    eval printf "$2\n" "${@:3}" $1
}

function error
{
    println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

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

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

function println
{
    eval 'printf "$2\n" "${@:3}"' $1
}

function error
{
    println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Это должно работать. Это также безопасно, если $1в нем printlnникогда не бывает грязи.

Теперь подождите минутку: я использую тот же синтаксис без кавычек, который мы использовали изначально sudoвсе время! Почему работает там, а не здесь? Почему нам пришлось все заключать в одинарные кавычки? sudoнемного более современен: он знает, что нужно заключать в кавычки каждый полученный аргумент, хотя это чрезмерное упрощение. evalпросто объединяет все.

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

Альтернативы eval

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

Нет операции

В bash нельзя использовать простое двоеточие:

:

Создать подоболочку

( command )   # Standard notation

Выполнить вывод команды

Никогда не полагайтесь на внешнюю команду. Вы всегда должны контролировать возвращаемое значение. Поместите их в отдельные строки:

$(command)   # Preferred
`command`    # Old: should be avoided, and often considered deprecated

# Nesting:
$(command1 "$(command2)")
`command "\`command\`"`  # Careful: \ only escapes $ and \ with old style, and
                         # special case \` results in nesting.

Перенаправление на основе переменной

В вызывающем коде сопоставьте &3(или что-нибудь выше &2) с вашей целью:

exec 3<&0         # Redirect from stdin
exec 3>&1         # Redirect to stdout
exec 3>&2         # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt  # Redirect to file
exec 3> "$var"    # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1    # Input and output!

Если бы это был разовый вызов, вам бы не пришлось перенаправлять всю оболочку:

func arg1 arg2 3>&2

Внутри вызываемой функции выполните перенаправление на &3:

command <&3       # Redirect stdin
command >&3       # Redirect stdout
command 2>&3      # Redirect stderr
command &>&3      # Redirect stdout and stderr
command 2>&1 >&3  # idem, but for older bash versions
command >&3 2>&1  # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4   # Input and output!

Переменная косвенность

Сценарий:

VAR='1 2 3'
REF=VAR

Плохой:

eval "echo \"\$$REF\""

Зачем? Если REF содержит двойные кавычки, это сломает и откроет код для эксплойтов. Очистить REF можно, но это пустая трата времени, когда у вас есть это:

echo "${!REF}"

Правильно, начиная с версии 2 в bash встроено косвенное обращение к переменным. Это становится немного сложнее, чем evalесли бы вы хотели сделать что-то более сложное:

# Add to scenario:
VAR_2='4 5 6'

# We could use:
local ref="${REF}_2"
echo "${!ref}"

# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""

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

Ассоциативные массивы

Ассоциативные массивы встроены в bash 4. Одно предостережение: они должны быть созданы с использованием declare.

declare -A VAR   # Local
declare -gA VAR  # Global

# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )

VAR+=( ['alpha']='beta' [2]=3 )  # Combine arrays

VAR['cow']='moo'  # Set a single element
unset VAR['cow']  # Unset a single element

unset VAR     # Unset an entire array
unset VAR[@]  # Unset an entire array
unset VAR[*]  # Unset each element with a key corresponding to a file in the
              # current directory; if * doesn't expand, unset the entire array

local KEYS=( "${!VAR[@]}" )  # Get all of the keys in VAR

В более старых версиях bash вы можете использовать косвенное обращение к переменной:

VAR=( )  # This will store our keys.

# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )

# Recover a simple value.
local var_key="VAR_$key"       # The name of the variable that holds the value
local var_value="${!var_key}"  # The actual value--requires bash 2
# For < bash 2, eval is required for this method.  Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""

# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value"                         # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`"   # Retrieve

# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
    local key="`mkpasswd -5R0 "$1" 00000000`"
    echo -n "${key##*$}"
}

local var_key="VAR_`mkkey "$key"`"
# ...
Zenexer
источник
4
Мне не хватает упоминания о eval "export $var='$val'"... (?)
Зрин
1
@Zrin Скорее всего, это не то, что вы ожидаете. export "$var"="$val"это, вероятно, то, что вы хотите. Единственный раз, когда вы можете использовать свою форму, - это если var='$var2', и вы хотите дважды разыменовать ее, но вы не должны пытаться делать что-либо подобное в bash. Если вам действительно нужно, вы можете использовать export "${!var}"="$val".
Zenexer
1
@anishsane: Предположим, x="echo hello world";тогда для выполнения всего, что содержится в нем x, мы можем использовать. eval $xОднако $($x)это неправильно, не так ли? Да: $($x)неправильно, потому что он запускается, echo hello worldа затем пытается запустить захваченный вывод (по крайней мере, в тех контекстах, где, я думаю, вы его используете), что приведет к сбою, если у вас нет программы, называемой " helloпинать".
Джонатан Леффлер
1
@tmow А, значит, вам действительно нужна функция eval. Если вы этого хотите, вы можете использовать eval; просто имейте в виду, что у него есть много предостережений в отношении безопасности. Это также признак того, что в вашем приложении есть недостаток дизайна.
Zenexer 07
1
ref="${REF}_2" echo "${!ref}"пример неверен, он не будет работать должным образом, поскольку bash заменяет переменные перед выполнением команды. Если до этого refпеременная действительно не была определена, результатом подстановки будет ref="VAR_2" echo ""то, что будет выполнено.
Yoory N.
17

Как evalобезопасить

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

Эта функция сделает это за вас:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Пример использования:

Учитывая некоторый ненадежный ввод пользователя:

% input="Trying to hack you; date"

Создайте команду для eval:

% cmd=(echo "User gave:" "$input")

Оцените это с, казалось бы, правильной цитатой:

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018

Обратите внимание, что вас взломали. dateбыл выполнен, а не напечатан буквально.

Вместо этого token_quote():

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%

eval не зло - это просто неправильно понимают :)

Том Хейл
источник
Как функция token_quote использует свои аргументы? Я не могу найти никакой документации по этой функции ...
Акито,
Думаю, я сформулировал это слишком нечетко. Я имел в виду аргументы функции. Почему нет arg="$1"? Как цикл for узнает, какие аргументы были переданы функции?
Акито
Я бы пошел дальше, чем просто «неправильно понял», он также часто используется неправильно и в действительности не нужен. Ответ Zenexer охватывает множество таких случаев, но любое использование evalдолжно быть красным флагом и тщательно изучено, чтобы подтвердить, что на самом деле нет лучшего варианта, уже предоставленного языком.
dimo414