GNU/Linux >> Linux Esercitazione >  >> Linux

Perché eval dovrebbe essere evitato in Bash e cosa dovrei usare invece?

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 :

  1. Esecuzione di comandi costruiti in modo strano
  2. 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:

  1. Aggiungi commenti su un argomento specifico:
cmd=(
    # Important because blah blah:
    -v
)
  1. Raggruppa gli argomenti per la leggibilità lasciando righe vuote all'interno della definizione dell'array.
  2. Commenta argomenti specifici per scopi di debug.
  3. 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[@]}")
  1. 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
  1. 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.
  1. 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

Linux
  1. Howto:cos'è Git e Github? Come lo uso e perché dovrebbe interessarmene?

  2. Perché *non* analizzare `ls` (e cosa fare invece)?

  3. Perché non posso usare Cd in uno script Bash??

  4. Perché Deis e che cos'è?

  5. Qual è l'uso di $# in Bash

7 motivi per cui uso Manjaro Linux e dovresti farlo anche tu

Che cos'è una macchina virtuale e perché usarla?

Che cosa sono i contenitori multi-account di Firefox? Perché e come usarlo?

Che cos'è un Homelab e perché dovresti averne uno?

Cos'è Zsh? Dovresti usarlo?

Cos'è la funzionalità della community di ONLYOFFICE e perché dovresti usarla?