Respostas:
Esse problema é mais do que aparenta. Começaremos com o óbvio: eval
tem potencial para executar dados "sujos". Dados sujos são quaisquer dados que não foram reescritos como seguros para uso na situação-XYZ; no nosso caso, é qualquer string que não tenha sido formatada de forma a ser segura para avaliação.
A higienização de dados parece fácil à primeira vista. Supondo que estamos lançando uma lista de opções, o bash já fornece uma ótima maneira de higienizar elementos individuais e outra maneira de limpar todo o array como uma única string:
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).
Agora, digamos que queremos adicionar uma opção para redirecionar a saída como um argumento para println. Podemos, é claro, apenas redirecionar a saída de println em cada chamada, mas, por exemplo, não faremos isso. Precisamos usar eval
, já que as variáveis não podem ser usadas para redirecionar a saída.
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.
Parece bom, certo? O problema é que eval analisa duas vezes a linha de comando (em qualquer shell). Na primeira passagem de análise, uma camada de citação é removida. Com as cotações removidas, algum conteúdo variável é executado.
Podemos corrigir isso permitindo que a expansão da variável ocorra dentro do eval
. Tudo o que precisamos fazer é colocar tudo em aspas simples, deixando as aspas duplas onde estão. Uma exceção: temos que expandir o redirecionamento antes de eval
, para que fique fora das aspas:
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.
Isso deve funcionar. Também é seguro, desde que $1
em println
nunca é sujo.
Agora espere um momento: eu uso a mesma sintaxe não citada que usamos originalmente com sudo
o tempo todo! Por que funciona lá e não aqui? Por que tivemos que fazer aspas simples em tudo? sudo
é um pouco mais moderno: sabe colocar entre aspas cada argumento que recebe, embora isso seja uma simplificação excessiva. eval
simplesmente concatena tudo.
Infelizmente, não há um substituto imediato para eval
que trate argumentos como o sudo
faz, pois eval
é um shell embutido; isso é importante, pois assume o ambiente e o escopo do código circundante ao ser executado, em vez de criar uma nova pilha e escopo como uma função faz.
Casos de uso específicos geralmente têm alternativas viáveis para eval
. Aqui está uma lista útil. command
representa para onde você normalmente enviaria eval
; substitua o que quiser.
Dois pontos simples são autônomos no bash:
:
( command ) # Standard notation
Nunca confie em um comando externo. Você deve estar sempre no controle do valor de retorno. Coloque em suas próprias linhas:
$(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.
No código de chamada, mapeie &3
(ou qualquer coisa maior que &2
) para seu destino:
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 uma chamada única, você não teria que redirecionar todo o shell:
func arg1 arg2 3>&2
Dentro da função que está sendo chamada, redirecione para &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!
Cenário:
VAR='1 2 3'
REF=VAR
Ruim:
eval "echo \"\$$REF\""
Por quê? Se REF contiver aspas duplas, isso interromperá e abrirá o código para exploits. É possível higienizar REF, mas é uma perda de tempo quando você tem isso:
echo "${!REF}"
Isso mesmo, o bash tem indireção variável incorporada a partir da versão 2. É um pouco mais complicado do que eval
se você quiser fazer algo mais complexo:
# 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\""
Independentemente disso, o novo método é mais intuitivo, embora possa não parecer assim para os programados experientes que estão acostumados eval
.
Arrays associativos são implementados intrinsecamente no bash 4. Uma advertência: eles devem ser criados 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
Em versões mais antigas do bash, você pode usar indireção variável:
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"`"
# ...
export "$var"="$val"
é provavelmente o que você deseja. A única vez em que você pode usar seu formulário é se var='$var2'
e quiser desreferenciá-lo duas vezes - mas não deve tentar fazer nada parecido no bash. Se você realmente precisa, você pode usar export "${!var}"="$val"
.
x="echo hello world";
para executar o que quer que esteja contido x
, podemos usar eval $x
No entanto, $($x)
está errado, não é? Sim: $($x)
está errado porque ele roda echo hello world
e tenta rodar a saída capturada (pelo menos nos contextos onde eu acho que você está usando), que irá falhar a menos que você tenha um programa chamado hello
kicking around.
ref="${REF}_2" echo "${!ref}"
exemplo está errado, ele não funcionará como planejado, pois o bash substitui variáveis antes de um comando ser executado. Se a ref
variável for realmente indefinida antes, o resultado da substituição será ref="VAR_2" echo ""
, e é isso que será executado.
eval
seguroeval
pode ser usado com segurança - mas todos os seus argumentos precisam ser citados primeiro. Veja como:
Esta função que fará isso por você:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
Exemplo de uso:
Dado alguma entrada de usuário não confiável:
% input="Trying to hack you; date"
Construa um comando para avaliar:
% cmd=(echo "User gave:" "$input")
Avalie, com citações aparentemente corretas:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Observe que você foi hackeado. date
foi executado em vez de ser impresso literalmente.
Em vez de token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
não é mau - é apenas mal compreendido :)
arg="$1"
? Como o loop for sabe quais argumentos foram passados para a função?
eval
deve ser um sinalizador vermelho e examinado de perto para confirmar que realmente não há uma opção melhor já fornecida pela linguagem.
eval "export $var='$val'"
... (?)