Odpowiedzi:
W tym problemie jest coś więcej niż na pierwszy rzut oka. Zaczniemy od tego, co oczywiste: eval
ma potencjał do wykonywania „brudnych” danych. Brudne dane to wszelkie dane, które nie zostały przepisane jako bezpieczne do użycia w sytuacji-XYZ; w naszym przypadku jest to dowolny ciąg, który nie został sformatowany tak, aby był bezpieczny do oceny.
Odkażanie danych na pierwszy rzut oka wydaje się łatwe. Zakładając, że rzucamy listę opcji, bash już zapewnia świetny sposób na oczyszczenie poszczególnych elementów i inny sposób na wyczyszczenie całej tablicy jako pojedynczego ciągu:
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).
Teraz powiedzmy, że chcemy dodać opcję przekierowania wyjścia jako argument do println. Moglibyśmy oczywiście po prostu przekierować wyjście println przy każdym wywołaniu, ale dla przykładu, nie zamierzamy tego robić. Będziemy musieli użyć eval
, ponieważ zmiennych nie można używać do przekierowywania danych wyjściowych.
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.
Wygląda dobrze, prawda? Problem w tym, że eval analizuje dwukrotnie wiersz poleceń (w dowolnej powłoce). Podczas pierwszego przebiegu analizy usuwana jest jedna warstwa cytatów. Po usunięciu cudzysłowów wykonywana jest zawartość zmiennej.
Możemy to naprawić, pozwalając na rozwijanie zmiennych w ramach eval
. Wszystko, co musimy zrobić, to zacytować wszystko w jednym cudzysłowie, pozostawiając podwójne cudzysłowy tam, gdzie są. Jeden wyjątek: musimy rozszerzyć przekierowanie przed eval
, aby pozostać poza cudzysłowami:
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.
To powinno działać. Jest to także bezpieczne tak długo, jak $1
na println
to nigdy nie brudne.
A teraz chwileczkę: używam tej samej niecytowanej składni, której pierwotnie używaliśmy przez sudo
cały czas! Dlaczego to działa tam, a nie tutaj? Dlaczego musieliśmy wszystko cytować pojedynczo? sudo
jest nieco nowocześniejszy: wie, że każdy otrzymany argument należy ująć w cudzysłów, chociaż jest to nadmierne uproszczenie. eval
po prostu łączy wszystko.
Niestety, nie ma zamiennika typu drop-in, eval
który traktuje argumenty tak jak sudo
robi, podobnie jak eval
wbudowana powłoka; jest to ważne, ponieważ podczas wykonywania przyjmuje środowisko i zakres otaczającego kodu, a nie tworzy nowy stos i zakres, jak robi to funkcja.
Konkretne przypadki użycia często mają realną alternatywę dla eval
. Oto przydatna lista. command
reprezentuje to, do czego normalnie wysyłasz eval
; zastępuj cokolwiek chcesz.
Prosty dwukropek to brak działania w bash:
:
( command ) # Standard notation
Nigdy nie polegaj na zewnętrznym poleceniu. Zawsze powinieneś mieć kontrolę nad zwracaną wartością. Umieść je w osobnych wierszach:
$(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.
W wywołaniu kodu zmapuj &3
(lub cokolwiek wyższego niż &2
) na swój cel:
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!
Gdyby to było jednorazowe połączenie, nie musiałbyś przekierowywać całej powłoki:
func arg1 arg2 3>&2
W ramach wywoływanej funkcji przekieruj do &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!
Scenariusz:
VAR='1 2 3'
REF=VAR
Zły:
eval "echo \"\$$REF\""
Czemu? Jeśli REF zawiera podwójny cudzysłów, spowoduje to uszkodzenie i otwarcie kodu na exploity. Odkażanie REF jest możliwe, ale szkoda czasu, gdy masz to:
echo "${!REF}"
Zgadza się, bash ma wbudowaną zmienną pośrednią od wersji 2. Jest to nieco trudniejsze niż w eval
przypadku, gdy chcesz zrobić coś bardziej złożonego:
# 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\""
Mimo wszystko nowa metoda jest bardziej intuicyjna, chociaż może się tak nie wydawać doświadczonym programistom, którzy są do tego przyzwyczajeni eval
.
Tablice asocjacyjne są implementowane wewnętrznie w bash 4. Jedno zastrzeżenie: muszą być tworzone przy użyciu 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
W starszych wersjach basha możesz używać zmiennej pośredniej:
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"
jest prawdopodobnie tym, czego chcesz. Jedyny przypadek, w którym możesz użyć swojego formularza, to jeśli var='$var2'
i chcesz go podwójnie wyodrębnić - ale nie powinieneś próbować robić czegoś takiego w bashu. Jeśli naprawdę musisz, możesz użyć export "${!var}"="$val"
.
x="echo hello world";
następnie do wykonania tego, co jest zawarte w x
, możemy użyć eval $x
Jednak $($x)
jest źle, prawda? Tak: $($x)
jest źle, ponieważ działa, echo hello world
a następnie próbuje uruchomić przechwycone dane wyjściowe (przynajmniej w kontekstach, w których myślę, że ich używasz), co nie powiedzie się, chyba że masz program o nazwie hello
kopanie.
ref="${REF}_2" echo "${!ref}"
przykład jest zły, nie będzie działać zgodnie z przeznaczeniem, ponieważ bash podstawia zmienne przed wykonaniem polecenia. Jeśli ref
zmienna jest wcześniej naprawdę niezdefiniowana, wynikiem podstawienia będzie ref="VAR_2" echo ""
i to właśnie zostanie wykonane.
eval
zabezpieczyćeval
można bezpiecznie używać - ale wszystkie jego argumenty należy najpierw zacytować. Oto jak:
Ta funkcja, która zrobi to za Ciebie:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
Przykładowe użycie:
Biorąc pod uwagę niezaufane dane wejściowe użytkownika:
% input="Trying to hack you; date"
Skonstruuj polecenie ewaluacji:
% cmd=(echo "User gave:" "$input")
Oceń to, pozornie poprawnym cytatem:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Zauważ, że zostałeś zhakowany. date
został wykonany, a nie wydrukowany dosłownie.
Zamiast tego token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
nie jest zły - jest po prostu źle zrozumiany :)
arg="$1"
? Skąd pętla for wie, które argumenty zostały przekazane do funkcji?
eval
powinno być czerwoną flagą i dokładnie zbadane, aby potwierdzić, że naprawdę nie ma lepszej opcji już dostarczonej przez język.
eval "export $var='$val'"
... (?)