Yanıtlar:
Bu sorunun göründüğünden daha fazlası var. Bariz olanla başlayacağız: eval
"kirli" verileri yürütme potansiyeline sahiptir. Kirli veriler, durumda kullanım için güvenli XYZ olarak yeniden yazılmamış verilerdir; bizim durumumuzda, değerlendirme için güvenli olacak şekilde biçimlendirilmemiş herhangi bir dizedir.
Verileri temizlemek ilk bakışta kolay görünür. Bir seçenek listesi oluşturduğumuzu varsayarsak, bash zaten tek tek öğeleri sterilize etmek için harika bir yol ve tüm diziyi tek bir dize olarak sterilize etmenin başka bir yolunu sunuyor:
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).
Şimdi, çıktıyı println'ye bir argüman olarak yeniden yönlendirmek için bir seçenek eklemek istediğimizi varsayalım. Elbette, her çağrıda println çıktısını yeniden yönlendirebiliriz, ancak örnek olarak bunu yapmayacağız. Kullanmamız gerekecek eval
çünkü değişkenler çıktıyı yönlendirmek için kullanılamaz.
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.
İyi görünüyor, değil mi? Sorun şu ki, değerlendirme komut satırının iki katı (herhangi bir kabukta) ayrıştırır. İlk çözümlemede bir alıntı katmanı kaldırılır. Alıntılar kaldırıldığında, bazı değişken içerikler çalıştırılır.
Bunu, değişken genişlemesinin içinde yer almasına izin vererek düzeltebiliriz eval
. Tek yapmamız gereken her şeyi tek alıntı yapmak ve çift tırnakları oldukları yerde bırakmak. Bir istisna: yönlendirmeyi önceden genişletmemiz gerekiyor eval
, böylece tekliflerin dışında kalmalıyız:
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.
Bu çalışmalı. Bu sürece de güvenli $1
de println
asla kirli.
Şimdi bir dakika bekleyin: Her zaman orijinal olarak kullandığımız aynı alıntı yapılmamış sözdizimini kullanıyorum sudo
! Neden orada çalışıyor ve burada değil? Neden her şeyi tek bir alıntı yapmak zorunda kaldık? sudo
biraz daha modern: Aldığı her argümanı tırnak içine almayı biliyor, ancak bu aşırı bir basitleştirme. eval
her şeyi basitçe birleştirir.
Ne yazık ki, eval
argümanları sudo
, eval
yerleşik bir kabuk gibi ele alan bir drop-in ikamesi yoktur ; Bu, bir işlev gibi yeni bir yığın ve kapsam oluşturmak yerine, çalıştırıldığında çevreleyen kodun ortamını ve kapsamını aldığından önemlidir.
Özel kullanım durumlarının genellikle uygun alternatifleri vardır eval
. İşte kullanışlı bir liste. command
normalde neye göndereceğinizi temsil eder eval
; ne istersen yerine koy.
Basit bir kolon, bash'ta işlemsizdir:
:
( command ) # Standard notation
Asla harici bir komuta güvenmeyin. Her zaman dönüş değerinin kontrolü sizde olmalıdır. Bunları kendi satırlarına koyun:
$(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.
Kodu çağırırken , hedefinize eşleyin &3
(veya daha yüksek bir şey &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!
Tek seferlik bir arama olsaydı, tüm kabuğu yeniden yönlendirmeniz gerekmezdi:
func arg1 arg2 3>&2
Çağrılan işlev içinde şu adrese yönlendirin &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!
Senaryo:
VAR='1 2 3'
REF=VAR
Kötü:
eval "echo \"\$$REF\""
Neden? REF bir çift tırnak içeriyorsa, bu kırılır ve istismarlara yönelik kodu açar. REF'i sterilize etmek mümkündür, ancak buna sahip olduğunuzda zaman kaybı olur:
echo "${!REF}"
Doğru, bash sürüm 2'den itibaren yerleşik değişken yönlendirme özelliğine sahiptir. eval
Daha karmaşık bir şey yapmak istemenizden biraz daha zorlaşır :
# 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\""
Her şeye rağmen, yeni yöntem daha sezgiseldir, ancak alışkın olan deneyimli programcılar için bu şekilde görünmeyebilir eval
.
İlişkili diziler, bash 4'te özünde gerçeklenir. Bir uyarı: kullanılarak oluşturulmalıdır 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'ın eski sürümlerinde, değişken yönlendirmeyi kullanabilirsiniz:
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"
muhtemelen istediğin şeydir. Formunuzu kullanabileceğiniz tek zaman if var='$var2'
ve ondan iki kez referans almak istemenizdir - ancak bash'ta böyle bir şey yapmaya çalışmamalısınız. Eğer gerçekten mecbursan, kullanabilirsin export "${!var}"="$val"
.
x="echo hello world";
O zaman içerdiği her şeyi yürütmek x
için kullanabiliriz eval $x
Ancak, $($x)
yanlış, değil mi? Evet: $($x)
yanlıştır çünkü çalışır echo hello world
ve sonra yakalanan çıktıyı çalıştırmaya çalışır (en azından kullandığınızı düşündüğüm bağlamlarda), bu, hello
etrafta tekmeleme denen bir programınız yoksa başarısız olur .
ref="${REF}_2" echo "${!ref}"
örnek yanlıştır, bash bir komut çalıştırılmadan önce değişkenleri değiştirdiği için amaçlandığı gibi çalışmayacaktır . Eğer ref
değişken gerçekten önce tanımlanmamış, ikame sonucu olacağını ref="VAR_2" echo ""
ve yürütülecek hangi yıllardan bu.
eval
güvenli hale getirilireval
can güvenle kullanılabilir - ama Tüm bağımsız değişkenleri ilk alıntı gerekmektedir. Bunu nasıl yapacağınız aşağıda açıklanmıştır:
Bunu sizin için yapacak olan bu fonksiyon:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
Örnek kullanım:
Güvenilmeyen bazı kullanıcı girdileri göz önüne alındığında:
% input="Trying to hack you; date"
Değerlendirmek için bir komut oluşturun:
% cmd=(echo "User gave:" "$input")
İle Eval o görünüşte doğru alıntı:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Saldırıya uğradığınızı unutmayın. date
tam anlamıyla basılmak yerine idam edildi.
Bunun yerine token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
kötü değil - sadece yanlış anlaşılıyor :)
arg="$1"
? For döngüsü işleve hangi argümanların aktarıldığını nasıl bilir?
eval
kırmızı bayrak olmalı ve dil tarafından zaten sağlanan daha iyi bir seçenek olmadığını doğrulamak için yakından incelenmelidir.
eval "export $var='$val'"
... (?)