Risposte:
C'è di più in questo problema di quanto non sembri. Inizieremo con l'ovvio: eval
ha il potenziale per eseguire dati "sporchi". I dati sporchi sono tutti i dati che non sono stati riscritti come XYZ sicuri per l'uso in situazioni; nel nostro caso, è qualsiasi stringa che non è stata formattata in modo da essere sicura per la valutazione.
La sanificazione dei dati sembra facile a prima vista. Supponendo di lanciare un elenco di opzioni, bash fornisce già un ottimo modo per disinfettare i singoli elementi e un altro modo per disinfettare l'intero array come una singola stringa:
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).
Ora diciamo di voler aggiungere un'opzione per reindirizzare l'output come argomento a println. Potremmo, ovviamente, reindirizzare l'output di println su ogni chiamata, ma per amore di esempio, non lo faremo. Avremo bisogno di usare eval
, poiché le variabili non possono essere utilizzate per reindirizzare l'output.
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.
Sembra buono, vero? Il problema è che eval analizza due volte la riga di comando (in qualsiasi shell). Al primo passaggio di analisi viene rimosso uno strato di citazioni. Con le virgolette rimosse, alcuni contenuti variabili vengono eseguiti.
Possiamo risolvere questo problema lasciando che l'espansione della variabile avvenga all'interno di eval
. Tutto quello che dobbiamo fare è virgolette singole tutto, lasciando le virgolette doppie dove sono. Un'eccezione: dobbiamo espandere il reindirizzamento prima di eval
, in modo che rimanga fuori dalle virgolette:
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.
Questo dovrebbe funzionare. È anche sicuro finché$1
in println
non è mai sporco.
Ora aspetta solo un momento: uso sempre la stessa sintassi non quotata che abbiamo usato originariamente sudo
! Perché funziona lì e non qui? Perché abbiamo dovuto citare tutto in una sola volta? sudo
è un po 'più moderno: sa racchiudere tra virgolette ogni argomento che riceve, anche se si tratta di una semplificazione eccessiva. eval
concatena semplicemente tutto.
Sfortunatamente, non esiste un sostituto immediato per eval
trattare gli argomenti come sudo
fa, comeeval
è una shell incorporata; questo è importante, poiché assume l'ambiente e l'ambito del codice circostante quando viene eseguito, piuttosto che creare un nuovo stack e un nuovo ambito come fa una funzione.
Casi d'uso specifici spesso hanno valide alternative a eval
. Ecco un pratico elenco. command
rappresenta ciò a cui spediresti normalmenteeval
; sostituisci quello che vuoi.
Un semplice due punti è un no-op in bash:
:
( command ) # Standard notation
Non fare mai affidamento su un comando esterno. Dovresti sempre avere il controllo del valore restituito. Mettili sulle loro linee:
$(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.
Nel chiamare il codice, mappare &3
(o qualcosa di più alto di &2
) al tuo obiettivo:
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!
Se fosse una chiamata una tantum, non dovresti reindirizzare l'intera shell:
func arg1 arg2 3>&2
All'interno della funzione chiamata, reindirizza a &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!
Scenario:
VAR='1 2 3'
REF=VAR
Male:
eval "echo \"\$$REF\""
Perché? Se REF contiene una virgoletta doppia, questo interromperà e aprirà il codice agli exploit. È possibile disinfettare REF, ma è una perdita di tempo quando hai questo:
echo "${!REF}"
Esatto, bash ha l'indirizzamento indiretto variabile integrato a partire dalla versione 2. Diventa un po 'più complicato che eval
se vuoi fare qualcosa di più complesso:
# 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\""
Indipendentemente da ciò, il nuovo metodo è più intuitivo, anche se potrebbe non sembrare così ai programmati esperti che sono abituati eval
.
Gli array associativi sono implementati intrinsecamente in bash 4. Un avvertimento: devono essere creati utilizzando 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
Nelle versioni precedenti di bash, puoi utilizzare l'indirizzamento indiretto delle variabili:
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"
è probabilmente quello che vuoi. L'unica volta in cui potresti usare il tuo modulo è se var='$var2'
, e vuoi dereferenziarlo due volte, ma non dovresti provare a fare nulla del genere in bash. Se proprio devi, puoi usare export "${!var}"="$val"
.
x="echo hello world";
quindi per eseguire tutto ciò che è contenuto x
, possiamo usare eval $x
Tuttavia, $($x)
è sbagliato, non è vero? Sì: $($x)
è sbagliato perché viene eseguito echo hello world
e quindi tenta di eseguire l'output catturato (almeno nei contesti in cui penso che lo stai usando), che fallirà a meno che tu non abbia un programma chiamato hello
kicking around.
ref="${REF}_2" echo "${!ref}"
esempio è sbagliato, non funzionerà come previsto poiché bash sostituisce le variabili prima che venga eseguito un comando. Se la ref
variabile è davvero indefinita prima, il risultato della sostituzione sarà ref="VAR_2" echo ""
, ed è quello che verrà eseguito.
eval
sicurezzaeval
può essere utilizzato in modo sicuro, ma tutti i suoi argomenti devono essere prima citati. Ecco come:
Questa funzione che lo farà per te:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
Utilizzo di esempio:
Dato un input dell'utente non attendibile:
% input="Trying to hack you; date"
Costruisci un comando per valutare:
% cmd=(echo "User gave:" "$input")
Valutalo, con citazioni apparentemente corrette:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Nota che sei stato violato. date
è stato eseguito anziché essere stampato letteralmente.
Invece con token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
non è malvagio - è solo frainteso :)
arg="$1"
? Come fa il ciclo for a sapere quali argomenti sono stati passati alla funzione?
eval
dovrebbe essere una bandiera rossa ed esaminata attentamente per confermare che non esiste davvero un'opzione migliore già fornita dalla lingua.
eval "export $var='$val'"
... (?)