Відповіді:
Цієї проблеми є більше, ніж очі. Почнемо з очевидного: чи 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
do, як eval
і вбудована оболонка; це важливо, оскільки воно виконує середовище та обсяг навколишнього коду під час його виконання, а не створює новий стек та область, як це робить функція.
У конкретних випадках використання часто є життєздатні альтернативи eval
. Ось зручний список. command
представляє те, до чого ви зазвичай надсилаєте eval
; замінюйте чим завгодно.
Проста двокрапка є неоперативною в баші:
:
( 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"`"
# ...
export "$var"="$val"
це, мабуть, те, що ти хочеш. Єдиний раз, коли ви можете скористатись формою, це якщо var='$var2'
, і ви хочете подвоїти її відновлення - але вам не слід намагатися робити щось подібне в баш. Якщо ви дійсно повинні, можете використовувати 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'"
... (?)