comando eval no Bash e seus usos típicos

165

Depois de ler as páginas do manual do bash e com relação a este post .

Ainda estou tendo problemas para entender o que exatamente o evalcomando faz e quais seriam seus usos típicos. Por exemplo, se o fizermos:

bash$ set -- one two three  # sets $1 $2 $3
bash$ echo $1
one
bash$ n=1
bash$ echo ${$n}       ## First attempt to echo $1 using brackets fails
bash: ${$n}: bad substitution
bash$ echo $($n)       ## Second attempt to echo $1 using parentheses fails
bash: 1: command not found
bash$ eval echo \${$n} ## Third attempt to echo $1 using 'eval' succeeds
one

O que exatamente está acontecendo aqui e como o cifrão e a barra invertida se ligam ao problema?

kstratis
fonte
1
Para o registro, a segunda tentativa funciona. $($n)é executado $nem um subshell. Ele tenta executar o comando 1que não existe.
Martin Wickman
1
@MartinWickman Mas o requisito é executar echo $1eventualmente, não 1. Eu não acho que isso possa ser feito usando sub-conchas.
Hari Menon
5
Você deve estar ciente das implicações de segurança do usoeval .
Pausado até novo aviso.
1
@ Raze2dust: Não acredito que ele estivesse sugerindo que pudesse ser executado com subcascas, mas explicando por que o quinto comando do OP listado não funcionou.
Jedwards

Respostas:

196

evalusa uma string como argumento e a avalia como se você tivesse digitado essa string em uma linha de comando. (Se você passar vários argumentos, eles primeiro serão unidos aos espaços entre eles.)

${$n}é um erro de sintaxe no bash. Dentro dos chavetas, você pode ter apenas um nome de variável, com alguns prefixos e sufixos possíveis, mas não pode ter sintaxe arbitrária de bash e, em particular, não pode usar expansão de variável. Existe uma maneira de dizer "o valor da variável cujo nome está nessa variável", no entanto:

echo ${!n}
one

$(…)executa o comando especificado entre parênteses em um subshell (ou seja, em um processo separado que herda todas as configurações, como valores de variáveis ​​do shell atual), e reúne sua saída. Então, echo $($n)é executado $ncomo um comando shell e exibe sua saída. Desde $navalia para 1, $($n)tenta executar o comando 1, que não existe.

eval echo \${$n}executa os parâmetros passados ​​para eval. Após a expansão, os parâmetros são echoe ${1}. Então eval echo \${$n}executa o comando echo ${1}.

Note-se que a maior parte do tempo, você deve usar aspas em torno substituições variáveis e substituições de comando (ou seja, sempre que houver uma $): "$foo", "$(foo)". Sempre coloque aspas duplas em torno de substituições de variáveis ​​e comandos , a menos que você saiba que precisa deixá-las desativadas. Sem as aspas duplas, o shell realiza a divisão do campo (ou seja, divide o valor da variável ou a saída do comando em palavras separadas) e, em seguida, trata cada palavra como um padrão curinga. Por exemplo:

$ ls
file1 file2 otherfile
$ set -- 'f* *'
$ echo "$1"
f* *
$ echo $1
file1 file2 file1 file2 otherfile
$ n=1
$ eval echo \${$n}
file1 file2 file1 file2 otherfile
$eval echo \"\${$n}\"
f* *
$ echo "${!n}"
f* *

evalnão é usado com muita frequência. Em algumas shells, o uso mais comum é obter o valor de uma variável cujo nome não é conhecido até o tempo de execução. No bash, isso não é necessário, graças à ${!VAR}sintaxe. evalainda é útil quando você precisa construir um comando mais longo contendo operadores, palavras reservadas etc.

Gilles 'SO- parar de ser mau'
fonte
Com relação ao meu comentário acima, quantos "passes" o eval faz?
kstratis
@ Konos5 Que comentário? evalrecebe uma string (que pode ser o resultado da análise e avaliação) e a interpreta como um trecho de código.
Gilles 'SO- stop be evil'
Sob a resposta de Raze2dust, deixei um comentário. Agora, acredito que o eval é usado principalmente para fins de desreferenciamento. Se eu digitar eval echo \ $ {$ n}, recebo um. No entanto, se eu digitar echo \ $ {$ n}, recebo \ $ {1}. Acredito que isso esteja acontecendo devido à análise de "duas passagens" de eval. Agora estou imaginando o que aconteceria se eu precisasse triplicar a desreferência usando uma declaração i = n extra. Nesse caso, de acordo com o Raze2dust, eu só preciso colocar uma avaliação extra. Acredito, porém, que deve haver uma maneira melhor ... (ele pode obter facilmente desordenado)
kstratis
@ Konos5 eu não usaria eval eval. Não me lembro de ter sentido a necessidade. Se você realmente precisa de dois evalpasses, use uma variável temporária, vai ser mais fácil de depurar: eval tmp="\${$i}"; eval x="\${$tmp}".
Gilles 'SO- stop be evil'
1
@ Konos5 "Analisado duas vezes" é um pouco enganador. Algumas pessoas podem ser levadas a acreditar nisso devido à dificuldade de especificar um argumento literal de string no Bash que está protegido contra várias expansões. evalapenas pega o código em uma string e o avalia de acordo com as regras usuais. Tecnicamente, isso nem está correto, porque existem alguns casos em que o Bash modifica a análise para nem mesmo fazer expansões nos argumentos de eval - mas esse é um boato muito obscuro do qual duvido que alguém conheça.
Ormaaj
39

Basta pensar em eval como "avaliar sua expressão mais uma vez antes da execução"

eval echo \${$n}torna-se echo $1após a primeira rodada de avaliação. Três alterações a serem observadas:

  • A \$tornou-se $(é necessária a barra invertida, caso contrário ele tenta avaliar ${$n}, o que significa uma variável chamada {$n}, o que não é permitido)
  • $n foi avaliado para 1
  • O evaldesapareceu

Na segunda rodada, é basicamente o echo $1que pode ser executado diretamente.

Então eval <some command>, primeiro avaliaremos <some command>(por avaliar aqui, quero dizer substituir variáveis, substituir caracteres de escape pelos corretos etc.) e, em seguida, executar a expressão resultante novamente.

evalé usado quando você deseja criar variáveis ​​dinamicamente ou ler saídas de programas projetados especificamente para serem lidos assim. Veja http://mywiki.wooledge.org/BashFAQ/048 para exemplos. O link também contém algumas maneiras típicas de evaluso e os riscos associados a ele.

Hari Menon
fonte
3
Como uma observação para o primeiro marcador, a ${VAR}sintaxe é permitida e preferida quando houver alguma ambiguidade (faz $VAR == $V, seguida por ARou $VAR == $VAseguida por R). ${VAR}é equivalente a $VAR. De fato, o nome da variável $ndisso não é permitido.
Jedwards
2
eval eval echo \\\${\${$i}}fará uma desreferência tripla. Não tenho certeza se existe uma maneira mais simples de fazer isso. Além disso, \${$n}funciona bem (impressões one) na minha máquina ..
Hari Menon
2
@ Konos5 echo \\\${\${$i}}imprime \${${n}}. eval echo \\\${\${$i}}é equivalente a echo \${${n}}`` and prints $ {1} . eval eval echo \\\ $ {\ $ {$ i}} `é equivalente a eval echo ${1}e imprime one.
Gilles 'SO- stop be evil'
2
@ Konos5 Pense da mesma maneira - O primeiro ` escapes the second one, and the third `escapa $depois disso. Assim torna-se \${${n}}depois de uma rodada de avaliação
Hari Menon
2
@ Konos5 A esquerda para a direita é a maneira correta de pensar na análise de cotação e barra invertida. Primeiro \\ produzindo uma barra invertida. Então, \$rendendo um dólar. E assim por diante.
Gilles 'SO- stop be evil'
25

Na minha experiência, um uso "típico" de eval é para executar comandos que geram comandos shell para definir variáveis ​​de ambiente.

Talvez você tenha um sistema que use uma coleção de variáveis ​​de ambiente e tenha um script ou programa que determine quais devem ser definidas e seus valores. Sempre que você executa um script ou programa, ele é executado em um processo bifurcado; portanto, tudo o que faz diretamente nas variáveis ​​de ambiente é perdido quando sai. Mas esse script ou programa pode enviar os comandos de exportação para o stdout.

Sem avaliação, você precisaria redirecionar stdout para um arquivo temporário, originar o arquivo temporário e excluí-lo. Com eval, você pode apenas:

eval "$(script-or-program)"

Observe que as aspas são importantes. Veja este exemplo (artificial):

# activate.sh
echo 'I got activated!'

# test.py
print("export foo=bar/baz/womp")
print(". activate.sh")

$ eval $(python test.py)
bash: export: `.': not a valid identifier
bash: export: `activate.sh': not a valid identifier
$ eval "$(python test.py)"
I got activated!
fuligem
fonte
tem algum exemplo de ferramentas comuns que fazem isso? A ferramenta em si tem um meio de produzir um conjunto de comandos do shell que podem ser passados ​​para avaliar?
Joakim Erdfelt
@ Joakim Eu não conheço nenhuma ferramenta de código aberto que faça isso, mas ela foi usada em alguns scripts privados nas empresas em que trabalhei. Eu apenas comecei a usar essa técnica novamente com o xampp. Os arquivos .conf do Apache expandem as variáveis ​​de ambiente gravadas ${varname}. Acho conveniente usar arquivos .conf idênticos em vários servidores diferentes, com apenas algumas coisas parametrizadas por variáveis ​​de ambiente. Eu editei / opt / lampp / xampp (que inicia o apache) para fazer esse tipo de avaliação com um script que vasculha o sistema e gera exportinstruções bash para definir variáveis ​​para os arquivos .conf.
sootsnoot
@ Joakim A alternativa seria ter um script para gerar cada um dos arquivos .conf afetados a partir de um modelo, com base no mesmo bisbilhoteiro. Uma coisa que eu mais gosto no meu caminho é que iniciar o apache sem passar por / opt / lampp / xampp não usa scripts de saída obsoletos, mas falha ao iniciar porque as variáveis ​​de ambiente se expandem para nada e criam diretivas inválidas.
sootsnoot
@ Anthony Sottile Vejo que você editou a resposta para adicionar aspas em torno de $ (script ou programa), dizendo que elas eram importantes ao executar vários comandos. Você pode fornecer um exemplo - o seguinte funciona bem com comandos separados por ponto e vírgula no stdout do foo.sh: echo '#! / Bin / bash'> foo.sh; echo 'echo "echo -na; echo -nb; echo -n c"' >> foo.sh; chmod 755 foo.sh; eval $ (./ foo.sh). Isso produz abc no stdout. Executar ./foo.sh produz: echo -na; eco -nb; echo -nc
sootsnoot,
1
Para um exemplo de uma ferramenta comum que usa eval, consulte pyenv . O pyenv permite alternar facilmente entre várias versões do Python. Você coloca eval "$(pyenv init -)"no seu .bash_profilearquivo de configuração do shell (ou similar). Isso constrói um pequeno script de shell e o avalia no shell atual.
precisa saber é o seguinte
10

A instrução eval diz ao shell para pegar os argumentos de eval como comando e executá-los na linha de comando. É útil em uma situação como abaixo:

No seu script, se você estiver definindo um comando em uma variável e, posteriormente, desejar usar esse comando, use eval:

/home/user1 > a="ls | more"
/home/user1 > $a
bash: command not found: ls | more
/home/user1 > # Above command didn't work as ls tried to list file with name pipe (|) and more. But these files are not there
/home/user1 > eval $a
file.txt
mailids
remote_cmd.sh
sample.txt
tmp
/home/user1 >
Nikhil Gupta
fonte
4

Atualização: algumas pessoas dizem que nunca se deve usar avaliação. Discordo. Eu acho que o risco surge quando uma entrada corrompida pode ser passada para eval. No entanto, existem muitas situações comuns em que isso não representa um risco e, portanto, vale a pena saber como usar o eval em qualquer caso. Essa resposta do stackoverflow explica os riscos da avaliação e as alternativas à avaliação. Por fim, cabe ao usuário determinar se / quando a avaliação é segura e eficiente de usar.


A evalinstrução bash permite executar linhas de código calculadas ou adquiridas pelo seu script bash.

Talvez o exemplo mais direto seja um programa bash que abra outro script bash como um arquivo de texto, leia cada linha de texto e use evalpara executá-los em ordem. Esse é essencialmente o mesmo comportamento da sourceinstrução bash , que é o que se usaria, a menos que fosse necessário realizar algum tipo de transformação (por exemplo, filtragem ou substituição) no conteúdo do script importado.

Raramente precisei eval, mas achei útil ler ou escrever variáveis ​​cujos nomes estavam contidos em cadeias atribuídas a outras variáveis. Por exemplo, para executar ações em conjuntos de variáveis, mantendo o tamanho do código pequeno e evitando redundância.

evalé conceitualmente simples. No entanto, a sintaxe estrita do idioma bash e a ordem de análise do intérprete do bash podem ser diferenciadas e evalparecer enigmáticas e difíceis de usar ou entender. Aqui estão os itens essenciais:

  1. O argumento passado para evalé uma expressão de cadeia que é calculada em tempo de execução. evalexecutará o resultado final analisado de seu argumento como uma linha de código real em seu script.

  2. A sintaxe e a ordem de análise são rigorosas. Se o resultado não for uma linha executável de código bash, no escopo do seu script, o programa falhará na evalinstrução ao tentar executar o lixo.

  3. Ao testar, você pode substituir a evalinstrução echoe ver o que é exibido. Se for um código legítimo no contexto atual, sua execução evalfuncionará.


Os exemplos a seguir podem ajudar a esclarecer como o eval funciona ...

Exemplo 1:

eval A declaração na frente do código 'normal' é um NOP

$ eval a=b
$ eval echo $a
b

No exemplo acima, as primeiras evalinstruções não têm finalidade e podem ser eliminadas. evalé inútil na primeira linha porque não há aspecto dinâmico no código, ou seja, ele já foi analisado nas linhas finais do código bash, portanto, seria idêntico como uma declaração normal de código no script bash. O segundo também evalé inútil, porque, embora exista uma etapa de análise convertida $apara o seu equivalente literal de cadeia de caracteres, não há indireção (por exemplo, nenhuma referência via valor da cadeia de um substantivo bash real ou variável de script realizada por bash), portanto se comportaria de forma idêntica como uma linha de código sem o evalprefixo.



Exemplo 2:

Execute a atribuição de var usando nomes de var passados ​​como valores de sequência.

$ key="mykey"
$ val="myval"
$ eval $key=$val
$ echo $mykey
myval

Se você fosse echo $key=$val, a saída seria:

mykey=myval

Que , sendo o resultado final da análise de cadeia, é o que será executado por eval, portanto, o resultado da instrução echo no final ...



Exemplo 3:

Incluindo mais Indireção no Exemplo 2

$ keyA="keyB"
$ valA="valB"
$ keyB="that"
$ valB="amazing"
$ eval eval \$$keyA=\$$valA
$ echo $that
amazing

O exposto acima é um pouco mais complicado que o exemplo anterior, confiando mais na ordem de análise e nas peculiaridades do bash. A evallinha seria aproximadamente analisada internamente na seguinte ordem (observe que as seguintes instruções são pseudocódigo, não código real, apenas para tentar mostrar como a instrução seria dividida em etapas internamente para chegar ao resultado final) .

 eval eval \$$keyA=\$$valA  # substitution of $keyA and $valA by interpreter
 eval eval \$keyB=\$valB    # convert '$' + name-strings to real vars by eval
 eval $keyB=$valB           # substitution of $keyB and $valB by interpreter
 eval that=amazing          # execute string literal 'that=amazing' by eval

Se a ordem de análise assumida não explica o que a avaliação está fazendo o suficiente, o terceiro exemplo pode descrever a análise com mais detalhes para ajudar a esclarecer o que está acontecendo.



Exemplo 4:

Descubra se os vars, cujos nomes estão contidos em cadeias, eles próprios contêm valores de cadeia.

a="User-provided"
b="Another user-provided optional value"
c=""

myvarname_a="a"
myvarname_b="b"
myvarname_c="c"

for varname in "myvarname_a" "myvarname_b" "myvarname_c"; do
    eval varval=\$$varname
    if [ -z "$varval" ]; then
        read -p "$varname? " $varname
    fi
done

Na primeira iteração:

varname="myvarname_a"

Bash analisa o argumento evale evalvê literalmente isso em tempo de execução:

eval varval=\$$myvarname_a

O pseudocódigo a seguir tenta ilustrar como o bash interpreta a linha de código real acima , para chegar ao valor final executado por eval. (as seguintes linhas descritivas, não o código bash exato):

1. eval varval="\$" + "$varname"      # This substitution resolved in eval statement
2. .................. "$myvarname_a"  # $myvarname_a previously resolved by for-loop
3. .................. "a"             # ... to this value
4. eval "varval=$a"                   # This requires one more parsing step
5. eval varval="User-provided"        # Final result of parsing (eval executes this)

Depois que toda a análise é feita, o resultado é o que é executado, e seu efeito é óbvio, demonstrando que não há nada particularmente misterioso sobre evalsi mesmo, e a complexidade está na análise de seu argumento.

varval="User-provided"

O código restante no exemplo acima simplesmente testa para verificar se o valor atribuído a $ varval é nulo e, nesse caso, solicita que o usuário forneça um valor.

luz clara
fonte
3

Inicialmente, intencionalmente nunca aprendi a usar o eval, porque a maioria das pessoas recomenda ficar longe dele como uma praga. No entanto, descobri recentemente um caso de uso que me fez ficar chateado por não reconhecê-lo antes.

Se você possui tarefas cron que deseja executar interativamente para testar, poderá visualizar o conteúdo do arquivo com cat e copiar e colar a tarefa cron para executá-lo. Infelizmente, isso envolve tocar o mouse, o que é um pecado no meu livro.

Digamos que você tenha um trabalho cron em /etc/cron.d/repeatme com o conteúdo:

*/10 * * * * root program arg1 arg2

Você não pode executar isso como um script com todo o lixo à sua frente, mas podemos usar o cut para se livrar de todo o lixo, envolvê-lo em uma subcama e executar a string com eval

eval $( cut -d ' ' -f 6- /etc/cron.d/repeatme)

O comando recortar imprime apenas o sexto campo do arquivo, delimitado por espaços. Eval então executa esse comando.

Eu usei um trabalho cron aqui como exemplo, mas o conceito é formatar o texto de stdout e depois avaliar esse texto.

O uso de eval neste caso não é inseguro, porque sabemos exatamente o que avaliaremos antes.

Luke Pafford
fonte
2

Recentemente, tive que usar evalpara forçar várias expansões de braçadeiras a serem avaliadas na ordem em que eu precisava. O Bash faz várias expansões de chaves da esquerda para a direita, então

xargs -I_ cat _/{11..15}/{8..5}.jpg

expande para

xargs -I_ cat _/11/8.jpg _/11/7.jpg _/11/6.jpg _/11/5.jpg _/12/8.jpg _/12/7.jpg _/12/6.jpg _/12/5.jpg _/13/8.jpg _/13/7.jpg _/13/6.jpg _/13/5.jpg _/14/8.jpg _/14/7.jpg _/14/6.jpg _/14/5.jpg _/15/8.jpg _/15/7.jpg _/15/6.jpg _/15/5.jpg

mas eu precisava da segunda expansão de braçadeira feita primeiro, produzindo

xargs -I_ cat _/11/8.jpg _/12/8.jpg _/13/8.jpg _/14/8.jpg _/15/8.jpg _/11/7.jpg _/12/7.jpg _/13/7.jpg _/14/7.jpg _/15/7.jpg _/11/6.jpg _/12/6.jpg _/13/6.jpg _/14/6.jpg _/15/6.jpg _/11/5.jpg _/12/5.jpg _/13/5.jpg _/14/5.jpg _/15/5.jpg

O melhor que pude fazer para isso foi

xargs -I_ cat $(eval echo _/'{11..15}'/{8..5}.jpg)

Isso funciona porque as aspas simples protegem o primeiro conjunto de chaves da expansão durante a análise da evallinha de comando, deixando que elas sejam expandidas pelo subshell invocado por eval.

Pode haver algum esquema astuto envolvendo expansões de chaves aninhadas que permita que isso aconteça em uma única etapa, mas se houver, sou velho e estúpido demais para vê-lo.

flabdablet
fonte
1

Você perguntou sobre usos típicos.

Uma queixa comum sobre scripts de shell é que você (supostamente) não pode passar por referência para recuperar valores das funções.

Mas, na verdade, via "eval", você pode passar por referência. O chamado pode devolver uma lista de atribuições de variáveis ​​a serem avaliadas pelo chamador. É passado por referência porque o chamador pode especificar os nomes das variáveis ​​de resultado - veja o exemplo abaixo. Os resultados dos erros podem ser transmitidos de volta aos nomes padrão, como errno e errstr.

Aqui está um exemplo de passagem por referência no bash:

#!/bin/bash
isint()
{
    re='^[-]?[0-9]+$'
    [[ $1 =~ $re ]]
}

#args 1: name of result variable, 2: first addend, 3: second addend 
iadd()
{
    if isint ${2} && isint ${3} ; then
        echo "$1=$((${2}+${3}));errno=0"
        return 0
    else
        echo "errstr=\"Error: non-integer argument to iadd $*\" ; errno=329"
        return 1
    fi
}

var=1
echo "[1] var=$var"

eval $(iadd var A B)
if [[ $errno -ne 0 ]]; then
    echo "errstr=$errstr"
    echo "errno=$errno"
fi
echo "[2] var=$var (unchanged after error)"

eval $(iadd var $var 1)
if [[ $errno -ne 0 ]]; then
    echo "errstr=$errstr"
    echo "errno=$errno"
fi  
echo "[3] var=$var (successfully changed)"

A saída é assim:

[1] var=1
errstr=Error: non-integer argument to iadd var A B
errno=329
[2] var=1 (unchanged after error)
[3] var=2 (successfully changed)

Existe uma largura de banda quase ilimitada nessa saída de texto! E há mais possibilidades se as várias linhas de saída forem usadas: por exemplo, a primeira linha pode ser usada para atribuições de variáveis, a segunda para 'fluxo de pensamento' contínuo, mas isso está além do escopo deste post.

Craig Hicks
fonte
dizer que há "mais possibilidades" é trivial, trivial e redundante para dizer o mínimo.
dotbit
0

Gosto da resposta "avaliando sua expressão mais uma vez antes da execução" e gostaria de esclarecer com outro exemplo.

var="\"par1 par2\""
echo $var # prints nicely "par1 par2"

function cntpars() {
  echo "  > Count: $#"
  echo "  > Pars : $*"
  echo "  > par1 : $1"
  echo "  > par2 : $2"

  if [[ $# = 1 && $1 = "par1 par2" ]]; then
    echo "  > PASS"
  else
    echo "  > FAIL"
    return 1
  fi
}

# Option 1: Will Pass
echo "eval \"cntpars \$var\""
eval "cntpars $var"

# Option 2: Will Fail, with curious results
echo "cntpars \$var"
cntpars $var

Os resultados curiosos da opção 2 são que teríamos passado dois parâmetros da seguinte maneira:

  • Primeiro Parâmetro: "value
  • Segundo parâmetro: content"

Como isso é contra-intuitivo? O adicional evalirá corrigir isso.

Adaptado de https://stackoverflow.com/a/40646371/744133

YoYo
fonte
0

Na pergunta:

who | grep $(tty | sed s:/dev/::)

gera erros alegando que os arquivos a e tty não existem. Entendi que isso significa que tty não está sendo interpretado antes da execução do grep, mas que o bash passou o tty como um parâmetro para o grep, que o interpretou como um nome de arquivo.

Há também uma situação de redirecionamento aninhado, que deve ser tratado por parênteses correspondentes que devem especificar um processo filho, mas o bash é primitivamente um separador de palavras, criando parâmetros a serem enviados para um programa, portanto, os parênteses não são correspondidos primeiro, mas interpretados como visto.

Eu fui específico com o grep e especifiquei o arquivo como um parâmetro em vez de usar um pipe. Também simplifiquei o comando base, passando a saída de um comando como um arquivo, para que a tubulação de E / S não fosse aninhada:

grep $(tty | sed s:/dev/::) <(who)

funciona bem.

who | grep $(echo pts/3)

não é realmente desejado, mas elimina o tubo aninhado e também funciona bem.

Em conclusão, o bash não parece gostar de pipping aninhado. É importante entender que o bash não é um programa de nova onda escrito de maneira recursiva. Em vez disso, o bash é um antigo programa 1,2,3, que foi anexado aos recursos. Para propósitos de garantir compatibilidade com versões anteriores, a maneira inicial de interpretação nunca foi modificada. Se o bash fosse reescrito para coincidir com parênteses pela primeira vez, quantos bugs seriam introduzidos em quantos programas bash? Muitos programadores adoram ser enigmáticos.

Paulo
fonte