C'è di più in questo problema di quanto 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 sicuri per l'uso in situazioni XYZ; 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 che stiamo lanciando 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).
Supponiamo ora di voler aggiungere un'opzione per reindirizzare l'output come argomento a println. Potremmo, ovviamente, semplicemente reindirizzare l'output di println a ogni chiamata, ma per esempio, non lo faremo. Dovremo 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 dell'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 del eval
. Tutto quello che dobbiamo fare è mettere tutto tra virgolette singole, lasciando le virgolette dove sono. Un'eccezione:dobbiamo espandere il reindirizzamento prima di eval
, quindi deve stare 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 lo stesso non quotato sintassi che abbiamo usato originariamente con sudo
tutto il tempo! Perché funziona lì e non qui? Perché abbiamo dovuto mettere tutto tra virgolette singole? sudo
è un po' più moderno:sa racchiudere tra virgolette ogni argomento che riceve, anche se questa è una semplificazione eccessiva. eval
semplicemente concatena tutto.
Sfortunatamente, non esiste un sostituto immediato per eval
che tratta argomenti come sudo
fa, come eval
è una shell integrata; questo è importante, poiché assume l'ambiente e l'ambito del codice circostante quando viene eseguito, piuttosto che creare un nuovo stack e ambito come fa una funzione.
valuta le alternative
Casi d'uso specifici hanno spesso valide alternative a eval
. Ecco un pratico elenco. command
rappresenta ciò che normalmente invieresti a eval
; sostituiscilo con quello che preferisci.
Nessuna operazione
Un semplice due punti è un no-op in bash:
:
Crea una sotto-shell
( command ) # Standard notation
Esegue l'output di un comando
Non fare mai affidamento su un comando esterno. Dovresti sempre avere il controllo del valore restituito. Mettili sulle proprie 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.
Reindirizzamento basato su variabile
Nel codice di chiamata, mappa &3
(o qualsiasi valore superiore a &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!
Indirizzamento variabile
Scenario:
VAR='1 2 3'
REF=VAR
Cattivo:
eval "echo \"\$$REF\""
Come mai? Se REF contiene una doppia virgoletta, 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 variabile integrato a partire dalla versione 2. Diventa un po' più complicato di 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 a eval
.
Array associativi
Gli array associativi sono implementati intrinsecamente in bash 4. Un avvertimento:devono essere creati usando 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 usare l'indirizzamento variabile:
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"`"
# ...
Come creare eval
sicuro
eval
può essere usato con sicurezza, ma tutti i suoi argomenti devono essere citati prima. 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[*]}"
}
Esempio di utilizzo:
Dati alcuni input di utenti non attendibili:
% input="Trying to hack you; date"
Costruisci un comando per eval:
% cmd=(echo "User gave:" "$input")
Valutalo, con apparentemente citazione corretta:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Nota che sei stato hackerato. 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 :)
Dividerò questa risposta in due parti , che, credo, coprano gran parte dei casi in cui le persone tendono ad essere tentate da eval
:
- Esecuzione di comandi costruiti in modo strano
- Giocherellare con variabili denominate dinamicamente
Esecuzione di comandi costruiti in modo strano
Molte, molte volte, semplici array indicizzati sono sufficienti, a patto di prendere buone abitudini riguardo alle virgolette doppie per proteggere le espansioni durante la definizione dell'array.
# 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[@]}"
Questo creerà foo bar
e plop yo
(due file, non quattro).
Nota che a volte può produrre script più leggibili per inserire solo gli argomenti (o una serie di opzioni) nell'array (almeno sai a prima vista cosa stai eseguendo):
touch "${args[@]}"
touch "${opts[@]}" file1 file2
Come bonus, gli array ti consentono, facilmente:
- Aggiungi commenti su un argomento specifico:
cmd=(
# Important because blah blah:
-v
)
- Raggruppa gli argomenti per la leggibilità lasciando righe vuote all'interno della definizione dell'array.
- Commenta argomenti specifici per scopi di debug.
- Aggiungi argomenti al tuo comando, a volte dinamicamente in base a condizioni specifiche o in loop:
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[@]}")
- Definisci i comandi nei file di configurazione consentendo al contempo argomenti contenenti spazi bianchi definiti dalla configurazione:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
- Registra un comando eseguibile in modo affidabile, che rappresenta perfettamente ciò che viene eseguito, utilizzando
%q
di 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.
- Goditi una migliore evidenziazione della sintassi rispetto a
eval
stringhe, poiché non è necessario nidificare le virgolette o utilizzare$
-s che "non verrà valutato subito ma lo sarà a un certo punto".
Per me, il vantaggio principale di questo approccio (e viceversa lo svantaggio di eval
) è che puoi seguire la stessa logica del solito per quanto riguarda quotazione, espansione, ecc. Non c'è bisogno di scervellarsi cercando di mettere virgolette tra virgolette "in anticipo" mentre si cerca di capire quale comando interpreterà quale coppia di virgolette in quel momento. E ovviamente molte delle cose sopra menzionate sono più difficili o addirittura impossibili da ottenere con eval
.
Con questi, non ho mai dovuto fare affidamento su eval
negli ultimi sei anni circa, e la leggibilità e la robustezza (in particolare per quanto riguarda gli argomenti che contengono spazi bianchi) sono state probabilmente aumentate. Non hai nemmeno bisogno di sapere se IFS
è stato temperato con! Naturalmente, ci sono ancora casi limite in cui eval
potrebbe effettivamente essere necessario (suppongo, ad esempio, se l'utente deve essere in grado di fornire un pezzo completo di script tramite un prompt interattivo o altro), ma si spera che non sia qualcosa che ti imbatterai quotidianamente. /P>
Giocherellare con variabili denominate dinamicamente
declare -n
(o le sue funzioni interne local -n
controparte), così come ${!foo}
, fai il trucco per la maggior parte del tempo.
$ help declare | grep -- -n
-n make NAME a reference to the variable named by its value
Bene, non è eccezionalmente chiaro senza un esempio:
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
(Adoro questo trucco ↑ perché mi fa sentire come se stessi passando oggetti alle mie funzioni, come in un linguaggio orientato agli oggetti. Le possibilità sono sbalorditive.)
Per quanto riguarda ${!…}
(che ottiene il valore della variabile nominata da un'altra variabile):
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