Dieses Problem hat mehr zu bieten 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 der Situation XYZ umgeschrieben wurden. In unserem Fall handelt es sich um eine Zeichenfolge, die nicht so formatiert wurde, dass sie für die Auswertung sicher ist.
Das Desinfizieren von Daten erscheint auf den ersten Blick einfach. Angenommen, wir werfen eine Liste von Optionen durch, bietet bash bereits eine großartige Möglichkeit, einzelne Elemente zu bereinigen, und eine andere Möglichkeit, das gesamte Array als einzelne Zeichenfolge 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 nur die Ausgabe von println bei jedem Aufruf umleiten, aber zum Beispiel werden wir das nicht tun. Wir müssen verwenden eval
, 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 zweimal die Befehlszeile analysiert (in jeder Shell). Beim ersten Durchlauf des Parsens wird eine Zitierschicht entfernt. Wenn Anführungszeichen entfernt werden, werden einige variable Inhalte ausgeführt.
Wir können dies beheben, indem wir die variable Erweiterung innerhalb der eval
. 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 vorher erweitern eval
, damit sie außerhalb der Anführungszeichen bleibt:
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
nie verschmutzt ist.
Warten Sie einen Moment: Ich verwende dieselbe nicht zitierte Syntax, die wir ursprünglich sudo
immer verwendet haben! Warum funktioniert es dort und nicht hier? Warum mussten wir alles in Anführungszeichen setzen? sudo
ist etwas moderner: Es kann jedes Argument, das es erhält, in Anführungszeichen setzen, obwohl dies eine übermäßige Vereinfachung darstellt. eval
verkettet einfach alles.
Leider gibt es keinen Drop-In-Ersatz dafür eval
, der Argumente so behandelt wie dies der sudo
Fall ist, ebenso eval
wie eine integrierte Shell. Dies ist wichtig, da es bei der Ausführung die Umgebung und den Umfang des umgebenden Codes übernimmt, anstatt wie eine Funktion einen neuen Stapel und Bereich zu erstellen.
eval Alternativen
Bestimmte Anwendungsfälle bieten häufig praktikable Alternativen zu eval
. Hier ist eine praktische Liste. command
stellt dar, an was Sie normalerweise senden würden eval
; Ersatz in was auch immer Sie bitte.
No-op
Ein einfacher Doppelpunkt ist ein No-Op in Bash:
:
Erstellen Sie eine Sub-Shell
( command ) # Standard notation
Führen Sie die Ausgabe eines Befehls aus
Verlassen Sie sich niemals auf einen externen Befehl. Sie sollten immer die Kontrolle über den Rückgabewert haben. Setzen Sie diese auf ihre eigenen 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
Wenn Sie Code aufrufen, ordnen Sie Ihrem Ziel Folgendes zu &3
(oder etwas höheres als &2
):
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 Anruf handeln würde, müssten Sie nicht die gesamte Shell umleiten:
func arg1 arg2 3>&2
Leiten Sie innerhalb der aufgerufenen Funktion zu &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!
Variable Indirektion
Szenario:
VAR='1 2 3'
REF=VAR
Schlecht:
eval "echo \"\$$REF\""
Warum? Wenn REF ein doppeltes Anführungszeichen enthält, wird der Code unterbrochen und für Exploits geöffnet. Es ist möglich, REF zu bereinigen, aber es ist Zeitverschwendung, wenn Sie Folgendes haben:
echo "${!REF}"
Richtig, in Bash ist ab Version 2 eine variable Indirektion integriert. Es wird etwas schwieriger, als eval
wenn Sie etwas Komplexeres tun möchten:
# 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\""
Unabhängig davon ist die neue Methode intuitiver, obwohl sie erfahrenen Programmierern, die es gewohnt sind, möglicherweise nicht so erscheint eval
.
Assoziative Arrays
Assoziative Arrays sind in Bash 4 intrinsisch implementiert. Eine Einschränkung: Sie müssen mit erstellt werden 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
In älteren Versionen von bash können Sie die variable Indirektion 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"`"
# ...
eval "export $var='$val'"
... (?)