Ответ 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"`"
# ...