Ответы:
В этой проблеме больше, чем кажется на первый взгляд. Начнем с очевидного: 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
; замените что угодно.
В 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"`"
# ...
export "$var"="$val"
это, вероятно, то, что вы хотите. Единственный раз, когда вы можете использовать свою форму, - это если var='$var2'
, и вы хотите дважды разыменовать ее, но вы не должны пытаться делать что-либо подобное в bash. Если вам действительно нужно, вы можете использовать export "${!var}"="$val"
.
x="echo hello world";
тогда для выполнения всего, что содержится в нем x
, мы можем использовать. eval $x
Однако $($x)
это неправильно, не так ли? Да: $($x)
неправильно, потому что он запускается, echo hello world
а затем пытается запустить захваченный вывод (по крайней мере, в тех контекстах, где, я думаю, вы его используете), что приведет к сбою, если у вас нет программы, называемой " hello
пинать".
ref="${REF}_2" echo "${!ref}"
пример неверен, он не будет работать должным образом, поскольку bash заменяет переменные перед выполнением команды. Если до этого ref
переменная действительно не была определена, результатом подстановки будет ref="VAR_2" echo ""
то, что будет выполнено.
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
не зло - это просто неправильно понимают :)
arg="$1"
? Как цикл for узнает, какие аргументы были переданы функции?
eval
должно быть красным флагом и тщательно изучено, чтобы подтвердить, что на самом деле нет лучшего варианта, уже предоставленного языком.
eval "export $var='$val'"
... (?)