GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Warum sollte eval in Bash vermieden werden und was sollte ich stattdessen verwenden?

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 :

  1. Ausführen seltsam aufgebauter Befehle
  2. 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:

  1. Kommentare zu einem bestimmten Argument hinzufügen:
cmd=(
    # Important because blah blah:
    -v
)
  1. Gruppieren Sie Argumente zur besseren Lesbarkeit, indem Sie innerhalb der Array-Definition Leerzeilen lassen.
  2. Kommentieren Sie bestimmte Argumente für Debugging-Zwecke aus.
  3. 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[@]}")
  1. 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
  1. 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.
  1. 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

Linux
  1. Howto:Was ist Git und Github? Wie verwende ich es und warum sollte es mich interessieren?

  2. Warum *nicht* `ls` parsen (und was stattdessen tun)?

  3. Verwenden Sie $[Ausdr] anstelle von $((Ausdr))?

  4. Wann und warum sollte ich Apt-get Update verwenden?

  5. Was sollte ich anstelle von windows.h unter Linux verwenden?

7 Gründe, warum ich Manjaro Linux verwende und Sie es auch sollten

Was ist eine virtuelle Maschine und warum sollte man sie verwenden?

Was sind Firefox Multi-Account-Container? Warum und wie wird es verwendet?

Was ist ein Homelab und warum sollten Sie eines haben?

Was ist Zsch? Sollten Sie es verwenden?

Was ist die ONLYOFFICE Community-Funktion und warum sollten Sie sie verwenden?