GNU/Linux >> Belajar Linux >  >> Linux

Mengapa eval harus dihindari di Bash, dan apa yang harus saya gunakan?

Ada lebih banyak masalah ini daripada yang terlihat. Kita akan mulai dengan yang sudah jelas:eval memiliki potensi untuk mengeksekusi data "kotor". Data kotor adalah data apa pun yang belum ditulis ulang sebagai aman-untuk-digunakan-dalam-situasi-XYZ; dalam kasus kita, ini adalah string apa pun yang belum diformat agar aman untuk evaluasi.

Sanitasi data tampak mudah pada pandangan pertama. Dengan asumsi kita memberikan daftar opsi, bash sudah menyediakan cara yang bagus untuk membersihkan elemen individual, dan cara lain untuk membersihkan seluruh array sebagai string tunggal:

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

Sekarang katakanlah kita ingin menambahkan opsi untuk mengarahkan keluaran sebagai argumen ke println. Kita bisa, tentu saja, mengarahkan output dari println pada setiap panggilan, tetapi sebagai contoh, kita tidak akan melakukan itu. Kita harus menggunakan eval , karena variabel tidak dapat digunakan untuk mengalihkan keluaran.

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.

Terlihat bagus, bukan? Masalahnya adalah, eval mem-parsing dua kali baris perintah (di shell apa pun). Pada parsing pertama, satu lapisan kutipan dihapus. Dengan tanda kutip dihapus, beberapa konten variabel dieksekusi.

Kita dapat memperbaikinya dengan membiarkan ekspansi variabel berlangsung di dalam eval . Yang harus kita lakukan adalah mengutip tunggal semuanya, meninggalkan tanda kutip ganda di tempatnya. Satu pengecualian:kita harus memperluas pengalihan sebelum eval , sehingga harus tetap berada di luar tanda kutip:

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.

Ini seharusnya berhasil. Aman juga selama $1 di println tidak pernah kotor.

Sekarang tunggu sebentar:Saya menggunakan tanpa tanda kutip yang sama sintaks yang kami gunakan awalnya dengan sudo setiap waktu! Mengapa bekerja di sana, dan tidak di sini? Mengapa kita harus mengutip semuanya? sudo sedikit lebih modern:ia tahu untuk menyertakan tanda kutip setiap argumen yang diterimanya, meskipun itu adalah penyederhanaan yang berlebihan. eval cukup gabungkan semuanya.

Sayangnya, tidak ada pengganti drop-in untuk eval yang memperlakukan argumen seperti sudo tidak, seperti eval adalah shell built-in; ini penting, karena mengambil lingkungan dan cakupan kode di sekitarnya saat dijalankan, daripada membuat tumpukan dan cakupan baru seperti yang dilakukan fungsi.

eval Alternatif

Kasus penggunaan khusus seringkali memiliki alternatif yang layak untuk eval . Berikut daftar praktisnya. command mewakili apa yang biasanya Anda kirim ke eval; gantikan apa pun yang Anda suka.

Tanpa operasi

Titik dua sederhana adalah no-op di bash:

:

Buat subkulit

( command )   # Standard notation

Jalankan keluaran perintah

Jangan pernah mengandalkan perintah eksternal. Anda harus selalu mengendalikan nilai pengembalian. Letakkan ini di baris mereka sendiri:

$(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.

Pengalihan berdasarkan variabel

Dalam kode panggilan, petakan &3 (atau yang lebih tinggi dari &2 ) ke target Anda:

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!

Jika ini adalah panggilan satu kali, Anda tidak perlu mengarahkan ulang seluruh shell:

func arg1 arg2 3>&2

Di dalam fungsi yang dipanggil, alihkan ke &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!

Tiruan variabel

Skenario:

VAR='1 2 3'
REF=VAR

Buruk:

eval "echo \"\$$REF\""

Mengapa? Jika REF berisi kutipan ganda, ini akan merusak dan membuka kode untuk dieksploitasi. Dimungkinkan untuk membersihkan REF, tetapi membuang-buang waktu jika Anda memiliki ini:

echo "${!REF}"

Benar, bash memiliki variabel tipuan bawaan pada versi 2. Ini menjadi sedikit lebih rumit daripada eval jika Anda ingin melakukan sesuatu yang lebih rumit:

# 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\""

Terlepas dari itu, metode baru ini lebih intuitif, meskipun mungkin tidak terlihat seperti itu bagi pemrogram berpengalaman yang terbiasa dengan eval .

Array asosiatif

Array asosiatif diimplementasikan secara intrinsik di bash 4. Satu peringatan:mereka harus dibuat menggunakan 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

Di versi bash yang lebih lama, Anda dapat menggunakan tipuan variabel:

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

Cara membuat eval aman

eval bisa digunakan dengan aman - tetapi semua argumennya harus dikutip terlebih dahulu. Begini caranya:

Fungsi ini yang akan melakukannya untuk Anda:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Contoh penggunaan:

Diberikan beberapa masukan pengguna yang tidak dipercaya:

% input="Trying to hack you; date"

Bangun perintah untuk eval:

% cmd=(echo "User gave:" "$input")

Evaluasilah, dengan tampaknya kutipan yang benar:

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018

Perhatikan bahwa Anda diretas. date dieksekusi daripada dicetak secara harfiah.

Alih-alih dengan token_quote() :

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%

eval tidak jahat - itu hanya disalahpahami :)


Saya akan membagi jawaban ini menjadi dua bagian , yang menurut saya mencakup sebagian besar kasus di mana orang cenderung tergoda oleh eval :

  1. Menjalankan perintah yang dibuat dengan aneh
  2. Mengotak-atik variabel yang dinamai secara dinamis

Menjalankan perintah yang dibuat dengan aneh

Sering kali, array terindeks sederhana sudah cukup, asalkan Anda membiasakan diri dengan tanda kutip ganda untuk melindungi perluasan saat menentukan larik.

# One nasty argument which must remain a single argument and not be split:
f='foo bar'
# The command in an indexed array (use `declare -a` if you really want to be explicit):
cmd=(
    touch
    "$f"
    # Yet another nasty argument, this time hardcoded:
    'plop yo'
)
# Let Bash expand the array and run it as a command:
"${cmd[@]}"

Ini akan membuat foo bar dan plop yo (dua file, bukan empat).

Perhatikan bahwa kadang-kadang dapat menghasilkan skrip yang lebih mudah dibaca untuk menempatkan hanya argumen (atau sekumpulan opsi) dalam larik (setidaknya Anda tahu sekilas apa yang Anda jalankan):

touch "${args[@]}"
touch "${opts[@]}" file1 file2

Sebagai bonus, array memungkinkan Anda dengan mudah:

  1. Tambahkan komentar tentang argumen tertentu:
cmd=(
    # Important because blah blah:
    -v
)
  1. Kelompokkan argumen agar mudah dibaca dengan mengosongkan baris dalam definisi larik.
  2. Komentari argumen tertentu untuk tujuan debugging.
  3. Tambahkan argumen ke perintah Anda, terkadang secara dinamis sesuai dengan kondisi tertentu atau dalam pengulangan:
cmd=(myprog)
for f in foo bar
do
    cmd+=(-i "$f")
done
if [[ $1 = yo ]]
then
    cmd+=(plop)
fi
to_be_added=(one two 't h r e e')
cmd+=("${to_be_added[@]}")
  1. Tetapkan perintah dalam file konfigurasi sambil mengizinkan argumen yang berisi ruang kosong yang ditentukan konfigurasi:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
  1. Catat perintah yang dapat dijalankan dengan kuat, yang secara sempurna mewakili apa yang sedang dijalankan, menggunakan %q printf :
function please_log_that {
    printf 'Running:'
    # From `help printf`:
    # “The format is re-used as necessary to consume all of the arguments.”
    # From `man printf` for %q:
    # “printed in a format that can be reused as shell input,
    # escaping  non-printable  characters with the proposed POSIX $'' syntax.”
    printf ' %q' "[email protected]"
    echo
}

arg='foo bar'
cmd=(prog "$arg" 'plop yo' $'arg\nnewline\tand tab')
please_log_that "${cmd[@]}"
# ⇒ “Running: prog foo\ bar plop\ yo $'arg\nnewline\tand tab'”
# You can literally copy and paste that ↑ to a terminal and get the same execution.
  1. Nikmati penyorotan sintaks yang lebih baik dibandingkan dengan eval string, karena Anda tidak perlu menyarangkan tanda kutip atau menggunakan $ -s bahwa “tidak akan langsung dievaluasi tetapi akan dievaluasi pada titik tertentu”.

Bagi saya, keuntungan utama dari pendekatan ini (dan sebaliknya kerugian dari eval ) adalah bahwa Anda dapat mengikuti logika yang sama seperti biasanya mengenai kutipan, perluasan, dll. Tidak perlu memeras otak untuk mencoba memberi tanda kutip dalam tanda kutip "di muka" sambil mencoba mencari tahu perintah mana yang akan menafsirkan pasangan tanda kutip mana pada saat itu. Dan tentu saja banyak hal yang disebutkan di atas lebih sulit atau sama sekali tidak mungkin dicapai dengan eval .

Dengan ini, saya tidak pernah bergantung pada eval dalam enam tahun terakhir atau lebih, dan keterbacaan dan ketangguhan (khususnya mengenai argumen yang mengandung spasi) bisa dibilang meningkat. Anda bahkan tidak perlu tahu apakah IFS telah marah dengan! Tentu saja, masih ada kasus ekstrim di mana eval mungkin benar-benar diperlukan (saya kira, misalnya, jika pengguna harus dapat menyediakan skrip lengkap melalui prompt interaktif atau apa pun), tapi mudah-mudahan itu bukan sesuatu yang akan Anda temui setiap hari.

Mengotak-atik variabel yang dinamai secara dinamis

declare -n (atau local -n dalam fungsinya mitra), serta ${!foo} , sering-seringlah melakukan trik.

$ help declare | grep -- -n
      -n    make NAME a reference to the variable named by its value

Yah, itu tidak terlalu jelas tanpa contoh:

declare -A global_associative_array=(
    [foo]=bar
    [plop]=yo
)

# $1    Name of global array to fiddle with.
fiddle_with_array() {
    # Check this if you want to make sure you’ll avoid
    # circular references, but it’s only if you really
    # want this to be robust.
    # You can also give an ugly name like “__ref” to your
    # local variable as a cheaper way to make collisions less likely.
    if [[ $1 != ref ]]
    then
        local -n ref=$1
    fi
    
    printf 'foo → %s\nplop → %s\n' "${ref[foo]}" "${ref[plop]}"
}

# Call the function with the array NAME as argument,
# not trying to get its content right away here or anything.
fiddle_with_array global_associative_array

# This will print:
# foo → bar
# plop → yo

(Saya suka trik ini ↑ karena membuat saya merasa seperti meneruskan objek ke fungsi saya, seperti dalam bahasa berorientasi objek. Kemungkinannya membingungkan.)

Adapun ${!…} (yang mendapatkan nilai dari variabel yang dinamai oleh variabel lain):

foo=bar
plop=yo

for var_name in foo plop
do
    printf '%s = %q\n' "$var_name" "${!var_name}"
done

# This will print:
# foo = bar
# plop = yo

Linux
  1. Mengapa *tidak* Mengurai `ls` (dan Apa yang Harus Dilakukan)?

  2. Linux – Mengapa Kami Menggunakan Su – Dan Bukan Hanya Su?

  3. Apa Env X=() { :;}; Command' Bash Do Dan Mengapa Tidak Aman?

  1. Gunakan $[ Ekspr ] Alih-alih $(( Ekspr ))?

  2. Kapan Dan Mengapa Saya Harus Menggunakan Pembaruan Apt-get?

  3. Database NoSQL Terdistribusi Elasticsearch – Apa Itu dan Haruskah Anda Menggunakannya?

  1. Mengapa Deis dan apa itu?

  2. Apa yang harus saya gunakan selain windows.h di Linux?

  3. Apa gunanya $# di Bash