Ce problème est plus complexe 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 en tant que données sûres à utiliser dans la situation XYZ ; dans notre cas, il s'agit de toute chaîne de caractères qui n'a pas été formatée de manière à être sûre pour l'évaluation.
L'assainissement des données semble facile à première vue. En supposant qu'il s'agisse d'une liste d'options, bash fournit déjà un excellent moyen d'assainir les éléments individuels, et un autre moyen d'assainir le tableau entier comme 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).
Disons maintenant que nous voulons ajouter une option pour rediriger la sortie comme argument de println. Nous pourrions, bien sûr, rediriger la sortie de println à chaque appel, mais pour les besoins de l'exemple, nous ne le ferons pas. Nous aurons besoin d'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). Au premier passage de l'analyse, une couche de citations est supprimée. Une fois les guillemets supprimés, le contenu de certaines variables est exécuté.
Nous pouvons résoudre ce problème en laissant l'expansion de la variable s'effectuer dans le fichier eval
. Il suffit de tout mettre entre guillemets, en laissant les guillemets doubles où ils sont. Une exception : nous devons étendre la redirection avant de 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 également sûr tant que $1
sur println
n'est jamais sale.
Maintenant, attendez juste un moment : J'utilise ce même non coté 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 qu'il doit mettre entre guillemets chaque argument qu'il reçoit, bien que ce soit une simplification excessive. eval
concatène simplement tout.
Malheureusement, il n'existe pas de solution de remplacement pour eval
qui traite les arguments comme sudo
fait, comme eval
est un shell intégré ; ceci est important, car il prend 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 à l'évaluation
Les cas d'utilisation spécifiques présentent souvent des alternatives viables à eval
. Voici une liste pratique. command
représente ce que vous enverriez normalement à eval
; remplacez-la par ce que vous voulez.
No-op
Un simple deux-points n'est pas une option dans bash :
:
Créer un sous-shell
( command ) # Standard notation
Exécuter la sortie d'une commande
Ne vous fiez jamais à une commande extérieure. Vous devez toujours avoir le contrôle de 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 une variable
Dans le code d'appel, la carte &3
(ou tout ce qui est 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!
Si c'était un appel unique, vous n'auriez pas à rediriger l'ensemble du shell :
func arg1 arg2 3>&2
Dans la fonction appelée, rediriger 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!
L'indirection des variables
Scénario :
VAR='1 2 3'
REF=VAR
Mauvais :
eval "echo \"\$$REF\""
Pourquoi ? Si REF contient un guillemet double, le code sera cassé et ouvert aux exploits. Il est possible d'assainir REF, mais c'est une perte de temps lorsque vous avez cela :
echo "${!REF}"
C'est vrai, bash a intégré l'indirection des variables à partir de la version 2. C'est un peu plus délicat 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, même si elle ne semble pas l'être pour les programmateurs expérimentés qui ont l'habitude de eval
.
Tableaux associatifs
Les tableaux associatifs sont implémentés intrinsèquement dans bash 4. Un seul bémol : 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 des variables :
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"`"
# ...