Bash'de değerlendirme yapmaktan neden kaçınılmalı ve bunun yerine ne kullanmalıyım?


107

Defalarca Stack Overflow'da Bash yanıtlarını görüyorum evalve yanıtlar böyle bir "şeytani" yapının kullanımı için dayak atılıyor, amaçlanıyor. Neden bu evalkadar kötü?

Eğer evalgüvenle kullanılamaz, bunun yerine neyi kullanmalıyım?

Yanıtlar:


148

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 $1de printlnasla 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? sudobiraz daha modern: Aldığı her argümanı tırnak içine almayı biliyor, ancak bu aşırı bir basitleştirme. evalher şeyi basitçe birleştirir.

Ne yazık ki, evalargümanları sudo, evalyerleş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.

eval Alternatifler

Özel kullanım durumlarının genellikle uygun alternatifleri vardır eval. İşte kullanışlı bir liste. commandnormalde neye göndereceğinizi temsil eder eval; ne istersen yerine koy.

İşlem yok

Basit bir kolon, bash'ta işlemsizdir:

:

Bir alt kabuk oluşturun

( command )   # Standard notation

Bir komutun çıktısını çalıştır

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.

Değişkene dayalı yeniden yönlendirme

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!

Değişken dolaylı

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. evalDaha 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

İ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"`"
# ...

4
Ben bir söz özlüyorum eval "export $var='$val'"... (?)
Zrin

1
@Zrin Şansları beklediğinizi yapmıyor. 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".
Zenexer

1
@anishsane: Varsayımınız için , x="echo hello world";O zaman içerdiği her şeyi yürütmek xiçin kullanabiliriz eval $xAncak, $($x)yanlış, değil mi? Evet: $($x)yanlıştır çünkü çalışır echo hello worldve sonra yakalanan çıktıyı çalıştırmaya çalışır (en azından kullandığınızı düşündüğüm bağlamlarda), bu, helloetrafta tekmeleme denen bir programınız yoksa başarısız olur .
Jonathan Leffler

1
@tmow Ah, yani gerçekten eval işlevselliği istiyorsunuz. İstediğin buysa, eval; sadece birçok güvenlik uyarısı olduğunu unutmayın. Ayrıca, uygulamanızda bir tasarım hatası olduğuna dair bir işarettir.
Zenexer

1
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 refdeğişken gerçekten önce tanımlanmamış, ikame sonucu olacağını ref="VAR_2" echo ""ve yürütülecek hangi yıllardan bu.
Yoory N.

17

Nasıl evalgüvenli hale getirilir

eval 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. datetam 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 :)


"Token_quote" işlevi bağımsız değişkenlerini nasıl kullanır? Bu özellikle ilgili herhangi bir belge bulamıyorum ...
Akito


Sanırım çok anlaşılmaz bir şekilde söyledim. Fonksiyon argümanlarını kastetmiştim. Neden hiç yoktur arg="$1"? For döngüsü işleve hangi argümanların aktarıldığını nasıl bilir?
Akito

Basitçe "yanlış anlaşılmaktan" daha öteye gidecektim, aynı zamanda sıklıkla yanlış kullanılır ve gerçekten gerekli değildir. Zenexer'in cevabı bu tür pek çok vakayı kapsar, ancak herhangi bir kullanım evalkı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.
dimo414
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.