Hinter diesem Problem steckt mehr als man denkt. Wir beginnen mit dem Offensichtlichen:eval
hat das Potenzial, "schmutzige" Daten auszuführen. Schmutzige Daten sind alle Daten, die nicht als sicher für die Verwendung in Situation XYZ neu geschrieben wurden; in unserem Fall ist es jede Zeichenfolge, die nicht so formatiert wurde, dass sie für die Auswertung sicher ist.
Das Bereinigen von Daten erscheint auf den ersten Blick einfach. Angenommen, wir werfen eine Liste von Optionen herum, bietet Bash bereits eine großartige Möglichkeit, einzelne Elemente zu bereinigen, und eine weitere Möglichkeit, das gesamte Array als einzelnen String zu bereinigen:
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).
Angenommen, wir möchten eine Option zum Umleiten der Ausgabe als Argument für println hinzufügen. Wir könnten natürlich die Ausgabe von println bei jedem Aufruf einfach umleiten, aber zum Beispiel werden wir das nicht tun. Wir müssen eval
verwenden , da Variablen nicht zum Umleiten der Ausgabe verwendet werden können.
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.
Sieht gut aus, oder? Das Problem ist, dass eval die Befehlszeile zweimal analysiert (in jeder Shell). Beim ersten Parsing-Durchgang wird eine Zitatebene entfernt. Wenn die Anführungszeichen entfernt sind, werden einige variable Inhalte ausgeführt.
Wir können dies beheben, indem wir die Variablenerweiterung innerhalb von eval
stattfinden lassen . Alles, was wir tun müssen, ist, alles in einfache Anführungszeichen zu setzen und die doppelten Anführungszeichen dort zu belassen, wo sie sind. Eine Ausnahme:Wir müssen die Umleitung vor eval
erweitern , das muss also außerhalb der Anführungszeichen bleiben:
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.
Das sollte funktionieren. Es ist auch sicher, solange $1
in println
ist nie schmutzig.
Jetzt warten Sie einen Moment:Ich benutze dasselbe nicht zitiert Syntax, die wir ursprünglich mit sudo
verwendet haben die ganze Zeit! Warum funktioniert es dort und nicht hier? Warum mussten wir alles in einfache Anführungszeichen setzen? sudo
ist ein bisschen moderner:Es weiß, dass es jedes Argument, das es erhält, in Anführungszeichen setzt, obwohl das eine zu starke Vereinfachung ist. eval
verkettet einfach alles.
Leider gibt es keinen direkten Ersatz für eval
die Argumente wie sudo
behandelt wie eval
ist eine eingebaute Shell; Dies ist wichtig, da es bei der Ausführung die Umgebung und den Bereich des umgebenden Codes übernimmt, anstatt wie eine Funktion einen neuen Stack und Bereich zu erstellen.
Alternativen evaluieren
Bestimmte Anwendungsfälle haben oft praktikable Alternativen zu eval
. Hier ist eine praktische Liste. command
stellt dar, was Sie normalerweise an eval
senden würden; Ersetzen Sie durch was auch immer Sie wollen.
No-op
Ein einfacher Doppelpunkt ist ein No-Op in Bash:
:
Erstellen Sie eine Sub-Shell
( command ) # Standard notation
Ausgabe eines Befehls ausführen
Verlassen Sie sich niemals auf einen externen Befehl. Sie sollten immer die Kontrolle über den Rückgabewert haben. Setzen Sie diese in eigene Zeilen:
$(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.
Umleitung basierend auf Variable
Ordnen Sie im Aufrufcode &3
zu (oder alles höher als &2
) zu Ihrem Ziel:
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!
Wenn es sich um einen einmaligen Aufruf handeln würde, müssten Sie nicht die gesamte Shell umleiten:
func arg1 arg2 3>&2
Leiten Sie innerhalb der aufgerufenen Funktion zu &3
weiter :
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!
Variable Indirektion
Szenario:
VAR='1 2 3'
REF=VAR
Schlecht:
eval "echo \"\$$REF\""
Wieso den? Wenn REF ein doppeltes Anführungszeichen enthält, bricht dies und öffnet den Code für Exploits. Es ist möglich, REF zu bereinigen, aber es ist Zeitverschwendung, wenn Sie Folgendes haben:
echo "${!REF}"
Das ist richtig, bash hat ab Version 2 eine Variablenindirektion eingebaut. Es wird ein bisschen kniffliger als eval
wenn Sie etwas Komplexeres machen wollen:
# 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\""
Ungeachtet dessen ist die neue Methode intuitiver, obwohl es erfahrenen Programmierern, die an eval
gewöhnt sind, vielleicht nicht so erscheint .
Assoziative Arrays
Assoziative Arrays sind in Bash 4 implementiert. Eine Einschränkung:Sie müssen mit declare
erstellt werden .
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
In älteren Bash-Versionen können Sie die Variablenindirektion verwenden:
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"`"
# ...
Wie man eval
macht sicher
eval
kann sicher verwendet werden - aber alle seine Argumente müssen zuerst zitiert werden. So geht's:
Diese Funktion erledigt das für Sie:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
Beispielverwendung:
Angesichts einiger nicht vertrauenswürdiger Benutzereingaben:
% input="Trying to hack you; date"
Erstellen Sie einen Befehl zum Auswerten:
% cmd=(echo "User gave:" "$input")
Bewerten Sie es mit scheinbar richtiges Zitieren:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Beachten Sie, dass Sie gehackt wurden. date
wurde ausgeführt, anstatt wörtlich gedruckt zu werden.
Stattdessen mit token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
ist nicht böse - es wird nur missverstanden :)
Ich werde diese Antwort in zwei Teile aufteilen , die meiner Meinung nach einen großen Teil der Fälle abdecken, in denen Menschen dazu neigen, von eval
in Versuchung geführt zu werden :
- Ausführen seltsam aufgebauter Befehle
- Mit dynamisch benannten Variablen herumspielen
Ausführen seltsam aufgebauter Befehle
Viele, viele Male einfache indizierte Arrays reichen aus, vorausgesetzt, Sie nehmen sich gute Gewohnheiten bezüglich doppelter Anführungszeichen an, um Erweiterungen zu schützen, während Sie das Array definieren.
# 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[@]}"
Dadurch wird foo bar
erstellt und plop yo
(zwei Dateien, nicht vier).
Beachten Sie, dass es manchmal besser lesbare Skripte erzeugen kann, wenn Sie nur die Argumente (oder eine Reihe von Optionen) in das Array einfügen (zumindest wissen Sie auf den ersten Blick, was Sie ausführen):
touch "${args[@]}"
touch "${opts[@]}" file1 file2
Als Bonus können Sie mit Arrays ganz einfach:
- Kommentare zu einem bestimmten Argument hinzufügen:
cmd=(
# Important because blah blah:
-v
)
- Gruppieren Sie Argumente zur besseren Lesbarkeit, indem Sie innerhalb der Array-Definition Leerzeilen lassen.
- Kommentieren Sie bestimmte Argumente für Debugging-Zwecke aus.
- Hängen Sie Argumente an Ihren Befehl an, manchmal dynamisch gemäß bestimmten Bedingungen oder in Schleifen:
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[@]}")
- Definieren Sie Befehle in Konfigurationsdateien, während Sie konfigurationsdefinierte Leerzeichen enthaltende Argumente zulassen:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
- Protokollieren Sie einen robust ausführbaren Befehl, der perfekt darstellt, was ausgeführt wird, indem Sie printfs
%q
verwenden :
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.
- Genießen Sie eine bessere Syntaxhervorhebung als mit
eval
Zeichenfolgen, da Sie keine Anführungszeichen verschachteln oder$
verwenden müssen -s, dass „nicht sofort, aber irgendwann ausgewertet wird“.
Für mich ist der Hauptvorteil dieses Ansatzes (und umgekehrt der Nachteil von eval
) besteht darin, dass Sie der gleichen Logik wie üblich in Bezug auf Zitate, Erweiterungen usw. folgen können Sie müssen sich nicht den Kopf zerbrechen, wenn Sie versuchen, „im Voraus“ Anführungszeichen in Anführungszeichen zu setzen, während Sie versuchen, herauszufinden, welcher Befehl welches Paar von Anführungszeichen in welchem Moment interpretiert. Und natürlich sind viele der oben genannten Dinge mit eval
schwerer oder gar nicht zu erreichen .
Damit musste ich mich nie auf eval
verlassen in den letzten sechs Jahren oder so, und Lesbarkeit und Robustheit (insbesondere in Bezug auf Argumente, die Leerzeichen enthalten) wurden wohl verbessert. Sie müssen nicht einmal wissen, ob IFS
wurde mit gemildert! Natürlich gibt es immer noch Grenzfälle, in denen eval
könnte tatsächlich benötigt werden (ich nehme an, zum Beispiel, wenn der Benutzer in der Lage sein muss, ein vollwertiges Stück Skript über eine interaktive Eingabeaufforderung oder was auch immer bereitzustellen), aber hoffentlich wird Ihnen das nicht täglich begegnen.
Mit dynamisch benannten Variablen herumspielen
declare -n
(oder seine Within-Funktionen local -n
Gegenstück) sowie ${!foo}
, mache meistens den Trick.
$ help declare | grep -- -n
-n make NAME a reference to the variable named by its value
Nun, ohne ein Beispiel ist es nicht besonders klar:
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
(Ich liebe diesen Trick ↑, weil er mir das Gefühl gibt, Objekte an meine Funktionen zu übergeben, wie in einer objektorientierten Sprache. Die Möglichkeiten sind umwerfend.)
Wie bei ${!…}
(was den Wert der Variablen erhält, die von einer anderen Variablen benannt wurde):
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