GNU/Linux >> Tutoriels Linux >  >> Linux

Pourquoi eval devrait-il être évité dans Bash et que dois-je utiliser à la place ?

Il y a plus à ce problème qu'il n'y paraît. Commençons par l'évidence :eval a le potentiel d'exécuter des données "sales". Les données sales sont toutes les données qui n'ont pas été réécrites comme étant sûres pour une utilisation en situation XYZ ; dans notre cas, il s'agit de toute chaîne qui n'a pas été formatée de manière à pouvoir être évaluée en toute sécurité.

La désinfection des données semble facile à première vue. En supposant que nous proposions une liste d'options, bash fournit déjà un excellent moyen de nettoyer des éléments individuels, et un autre moyen de nettoyer l'ensemble du tableau en une seule chaîne :

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).

Supposons maintenant que nous voulions ajouter une option pour rediriger la sortie en tant qu'argument vers println. Nous pourrions, bien sûr, simplement rediriger la sortie de println à chaque appel, mais à titre d'exemple, nous n'allons pas le faire. Nous devrons utiliser eval , puisque les variables ne peuvent pas être utilisées pour rediriger la sortie.

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.

Ça a l'air bien, non ? Le problème est que eval analyse deux fois la ligne de commande (dans n'importe quel shell). Lors de la première passe d'analyse, une couche de citations est supprimée. Avec les guillemets supprimés, certains contenus variables sont exécutés.

Nous pouvons résoudre ce problème en laissant l'expansion de la variable se dérouler dans le eval . Tout ce que nous avons à faire est de tout mettre entre guillemets simples, en laissant les guillemets doubles là où ils se trouvent. Une exception :nous devons étendre la redirection avant eval , donc cela doit rester en dehors des guillemets :

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.

Cela devrait fonctionner. C'est aussi sûr tant que $1 en println n'est jamais sale.

Attendez un instant :j'utilise le même sans guillemets syntaxe que nous utilisions à l'origine avec sudo tout le temps! Pourquoi ça marche là-bas et pas ici ? Pourquoi avons-nous dû tout mettre entre guillemets? sudo est un peu plus moderne :il sait mettre entre guillemets chaque argument qu'il reçoit, bien que ce soit une simplification excessive. eval concatène simplement tout.

Malheureusement, il n'y a pas de remplacement direct pour eval qui traite les arguments comme sudo fait, comme eval est un shell intégré ; c'est important, car il prend en compte l'environnement et la portée du code environnant lorsqu'il s'exécute, plutôt que de créer une nouvelle pile et une nouvelle portée comme le fait une fonction.

Alternatives d'évaluation

Des cas d'utilisation spécifiques ont souvent des alternatives viables à eval . Voici une liste pratique. command représente ce que vous enverriez normalement à eval; remplacer par ce que vous voulez.

Pas d'opération

Un simple deux-points est un no-op dans bash :

:

Créer un sous-shell

( command )   # Standard notation

Exécuter la sortie d'une commande

Ne comptez jamais sur une commande externe. Vous devez toujours contrôler la valeur de retour. Mettez-les sur leurs propres lignes :

$(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.

Redirection basée sur la variable

Dans le code d'appel, mappez &3 (ou quoi que ce soit supérieur à &2 ) à votre cible :

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!

S'il s'agissait d'un appel unique, vous n'auriez pas à rediriger tout le shell :

func arg1 arg2 3>&2

Dans la fonction appelée, redirigez vers &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!

Indirection variable

Scénario :

VAR='1 2 3'
REF=VAR

Mauvais :

eval "echo \"\$$REF\""

Pourquoi? Si REF contient un guillemet double, cela cassera et ouvrira le code aux exploits. Il est possible de désinfecter REF, mais c'est une perte de temps quand vous avez ceci :

echo "${!REF}"

C'est vrai, bash a une indirection variable intégrée à partir de la version 2. Cela devient un peu plus compliqué que eval si vous voulez faire quelque chose de plus complexe :

# 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\""

Quoi qu'il en soit, la nouvelle méthode est plus intuitive, bien qu'elle puisse sembler différente pour les programmeurs expérimentés habitués à eval .

Tableaux associatifs

Les tableaux associatifs sont implémentés intrinsèquement dans bash 4. Une mise en garde :ils doivent être créés en utilisant 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

Dans les anciennes versions de bash, vous pouvez utiliser l'indirection variable :

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"`"
# ...

Comment faire eval sûr

eval peut être utilisé en toute sécurité - mais tous ses arguments doivent être cités en premier. Voici comment :

Cette fonction qui le fera pour vous :

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Exemple d'utilisation :

Étant donné une entrée d'utilisateur non fiable :

% input="Trying to hack you; date"

Construisez une commande pour évaluer :

% cmd=(echo "User gave:" "$input")

Évaluez-le, avec apparemment citation correcte :

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018

Notez que vous avez été piraté. date a été exécuté plutôt que d'être imprimé littéralement.

Au lieu de cela, utilisez token_quote() :

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%

eval n'est pas mauvais - c'est juste mal compris :)


Je vais diviser cette réponse en deux parties , qui, je pense, couvrent une grande partie des cas où les gens ont tendance à être tentés par eval :

  1. Exécuter des commandes bizarrement construites
  2. Travailler avec des variables nommées dynamiquement

Exécuter des commandes bizarrement construites

Très souvent, de simples tableaux indexés suffisent, à condition de prendre de bonnes habitudes concernant les guillemets doubles pour protéger les expansions lors de la définition du tableau.

# 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[@]}"

Cela créera foo bar et plop yo (deux fichiers, pas quatre).

Notez que parfois, il peut produire des scripts plus lisibles pour ne mettre que les arguments (ou un tas d'options) dans le tableau (au moins, vous savez à première vue ce que vous exécutez) :

touch "${args[@]}"
touch "${opts[@]}" file1 file2

En prime, les tableaux vous permettent, facilement :

  1. Ajouter des commentaires sur un argument spécifique :
cmd=(
    # Important because blah blah:
    -v
)
  1. Regroupez les arguments pour la lisibilité en laissant des lignes vides dans la définition du tableau.
  2. Commentez des arguments spécifiques à des fins de débogage.
  3. Ajoutez des arguments à votre commande, parfois dynamiquement selon des conditions spécifiques ou en boucles :
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. Définissez les commandes dans les fichiers de configuration tout en autorisant les arguments contenant des espaces blancs définis par la configuration :
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
  1. Enregistrer une commande exécutable de manière robuste, qui représente parfaitement ce qui est exécuté, en utilisant le %q de 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. Profitez d'une meilleure coloration syntaxique qu'avec eval chaînes, puisque vous n'avez pas besoin d'imbriquer les guillemets ou d'utiliser $ -s qui "ne seront pas évalués tout de suite mais le seront à un moment donné".

Pour moi, le principal avantage de cette approche (et inversement l'inconvénient de eval ) est que vous pouvez suivre la même logique que d'habitude concernant la cotation, l'expansion, etc. Inutile de vous casser la tête en essayant de mettre des guillemets entre guillemets "à l'avance" tout en essayant de déterminer quelle commande interprétera quelle paire de guillemets à quel moment. Et bien sûr, beaucoup des choses mentionnées ci-dessus sont plus difficiles ou carrément impossibles à réaliser avec eval .

Avec ceux-ci, je n'ai jamais eu à compter sur eval au cours des six dernières années environ, et la lisibilité et la robustesse (en particulier en ce qui concerne les arguments contenant des espaces) ont sans doute augmenté. Vous n'avez même pas besoin de savoir si IFS a été tempéré avec! Bien sûr, il existe encore des cas extrêmes où eval pourrait en fait être nécessaire (je suppose, par exemple, si l'utilisateur doit être en mesure de fournir un morceau de script complet via une invite interactive ou autre), mais j'espère que ce n'est pas quelque chose que vous rencontrerez quotidiennement. /P>

Travailler avec des variables nommées dynamiquement

declare -n (ou ses fonctions internes local -n contrepartie), ainsi que ${!foo} , faites le tour la plupart du temps.

$ help declare | grep -- -n
      -n    make NAME a reference to the variable named by its value

Eh bien, ce n'est pas exceptionnellement clair sans exemple :

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

(J'adore cette astuce ↑ car elle me donne l'impression de passer des objets à mes fonctions, comme dans un langage orienté objet. Les possibilités sont ahurissantes.)

Comme pour ${!…} (qui récupère la valeur de la variable nommée par une autre variable) :

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. Tutoriel :qu'est-ce que Git et Github ? Comment puis-je l'utiliser et pourquoi devrais-je m'en soucier?

  2. Pourquoi *pas* analyser `ls` (et que faire à la place) ?

  3. Quand et pourquoi devrais-je utiliser Apt-get Update ?

  4. Base de données NoSQL distribuée Elasticsearch - Qu'est-ce que c'est et devriez-vous l'utiliser ?

  5. Pourquoi Deis et qu'est-ce que c'est ?

7 raisons pour lesquelles j'utilise Manjaro Linux et vous devriez aussi

Qu'est-ce qu'une machine virtuelle et pourquoi l'utiliser ?

Qu'est-ce que les conteneurs multi-comptes Firefox ? Pourquoi et comment l'utiliser ?

Qu'est-ce qu'un Homelab et pourquoi devriez-vous en avoir un ?

Qu'est-ce que Zsh ? Devriez-vous l'utiliser ?

Qu'est-ce que la fonctionnalité de la communauté ONLYOFFICE et pourquoi devriez-vous l'utiliser ?