Vấn đề này còn nhiều thứ hơn là gặp mắt. Chúng ta sẽ bắt đầu với điều hiển nhiên: eval
có khả năng thực thi dữ liệu "bẩn". Dữ liệu bẩn là bất kỳ dữ liệu nào chưa được viết lại dưới dạng an toàn cho việc sử dụng trong tình huống-XYZ; trong trường hợp của chúng tôi, đó là bất kỳ chuỗi nào chưa được định dạng để an toàn cho việc đánh giá.
Việc vệ sinh dữ liệu thoạt nhìn có vẻ dễ dàng. Giả sử chúng ta đang xoay quanh một danh sách các tùy chọn, bash đã cung cấp một cách tuyệt vời để khử trùng các phần tử riêng lẻ và một cách khác để khử trùng toàn bộ mảng dưới dạng một chuỗi đơn:
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).
Bây giờ giả sử chúng ta muốn thêm một tùy chọn để chuyển hướng đầu ra làm đối số cho println. Tất nhiên, chúng tôi có thể chuyển hướng đầu ra của println trên mỗi cuộc gọi, nhưng vì lợi ích của ví dụ, chúng tôi sẽ không làm điều đó. Chúng tôi sẽ cần sử dụng eval
, vì các biến không thể được sử dụng để chuyển hướng đầu ra.
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.
Có vẻ tốt, phải không? Vấn đề là, eval phân tích cú pháp hai lần dòng lệnh (trong bất kỳ trình bao nào). Trong lần phân tích cú pháp đầu tiên, một lớp trích dẫn bị xóa. Khi loại bỏ dấu ngoặc kép, một số nội dung biến đổi sẽ được thực thi.
Chúng tôi có thể khắc phục điều này bằng cách cho phép mở rộng biến diễn ra trong eval
. Tất cả những gì chúng ta phải làm là trích dẫn đơn mọi thứ, để nguyên dấu ngoặc kép. Một ngoại lệ: chúng tôi phải mở rộng chuyển hướng trước eval
, vì vậy điều đó phải nằm ngoài dấu ngoặc kép:
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.
Điều này sẽ hoạt động. Nó cũng an toàn miễn là $1
trong println
không bao giờ bẩn.
Bây giờ, hãy chờ một chút: Tôi sử dụng cùng một cú pháp không được trích dẫn mà chúng ta đã sử dụng ban đầu sudo
! Tại sao nó hoạt động ở đó mà không phải ở đây? Tại sao chúng ta phải trích dẫn duy nhất mọi thứ? sudo
hiện đại hơn một chút: nó biết đặt trong dấu ngoặc kép mỗi đối số mà nó nhận được, mặc dù đó là sự đơn giản hóa quá mức. eval
chỉ cần nối mọi thứ.
Thật không may, không có trình thay thế thả vào eval
nào xử lý các đối số như sudo
hiện tại, cũng như eval
một trình bao được tích hợp sẵn; điều này rất quan trọng, vì nó sử dụng môi trường và phạm vi của mã xung quanh khi nó thực thi, thay vì tạo một ngăn xếp và phạm vi mới như một hàm.
đánh giá các lựa chọn thay thế
Các trường hợp sử dụng cụ thể thường có các lựa chọn thay thế khả thi eval
. Đây là một danh sách hữu ích. command
đại diện cho những gì bạn thường gửi đến eval
; thay thế trong bất cứ điều gì bạn muốn.
Không ra đâu
Dấu hai chấm đơn giản là không chọn trong bash:
:
Tạo một vỏ con
( command ) # Standard notation
Thực thi đầu ra của một lệnh
Không bao giờ dựa vào lệnh bên ngoài. Bạn phải luôn kiểm soát được giá trị trả về. Đặt những điều này trên dòng riêng của họ:
$(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.
Chuyển hướng dựa trên biến
Trong mã gọi điện, ánh xạ &3
(hoặc bất kỳ thứ gì cao hơn &2
) tới mục tiêu của bạn:
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!
Nếu đó là cuộc gọi một lần, bạn sẽ không phải chuyển hướng toàn bộ shell:
func arg1 arg2 3>&2
Trong hàm đang được gọi, hãy chuyển hướng đến &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!
Biến hướng
Tình huống:
VAR='1 2 3'
REF=VAR
Xấu:
eval "echo \"\$$REF\""
Tại sao? Nếu REF chứa một dấu ngoặc kép, điều này sẽ phá vỡ và mở mã để khai thác. Có thể khử trùng REF, nhưng thật lãng phí thời gian khi bạn có điều này:
echo "${!REF}"
Đúng vậy, bash đã tích hợp sẵn tính năng định hướng thay đổi kể từ phiên bản 2. Nó sẽ phức tạp hơn một chút so với việc eval
bạn muốn làm điều gì đó phức tạp hơn:
# 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\""
Bất kể, phương pháp mới trực quan hơn, mặc dù nó có vẻ không giống như vậy đối với những người đã từng lập trình có kinh nghiệm eval
.
Mảng liên kết
Các mảng liên kết được triển khai thực chất trong bash 4. Một lưu ý: chúng phải được tạo bằng cách sử dụng 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
Trong các phiên bản cũ hơn của bash, bạn có thể sử dụng hướng biến:
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"`"
# ...
eval "export $var='$val'"
... (?)