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

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

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

Ответ 1

Там больше этой проблемы, чем кажется на первый взгляд. Начнем с очевидного: 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. Вот удобный список. command представляет то, что вы обычно отправляете на eval; замените все, что угодно.

Нет-оп

Простая двоеточие в no-op в 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}"

Правильно, bash имеет переменную косвенность, встроенную с версии 2. Это немного сложнее, чем 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"`"
# ...

Ответ 2

Как сделать безопасным 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 не злой - он просто неправильно понял :)

Ответ 3

Что насчет

ls -la /path/to/foo | grep bar | bash

или

(ls -la /path/to/foo | grep bar) | bash

?