Por que não posso especificar uma variável de ambiente e exibi-la na mesma linha de comando?

90

Considere este trecho:

$ SOMEVAR=AAA
$ echo zzz $SOMEVAR zzz
zzz AAA zzz

Aqui eu defini $SOMEVARcomo AAAna primeira linha - e quando eu ecoo na segunda linha, obtenho o AAAconteúdo conforme o esperado.

Mas então, se eu tentar especificar a variável na mesma linha de comando que echo:

$ SOMEVAR=BBB echo zzz $SOMEVAR zzz
zzz AAA zzz

... Não obtenho BBBo que esperava - obtenho o valor antigo ( AAA).

É assim que as coisas deveriam ser? Se sim, como então você pode especificar variáveis ​​como LD_PRELOAD=/... program args ...e fazer com que funcione? o que estou perdendo?

Sdaau
fonte
2
Funciona quando você faz da atribuição uma instrução separada ou quando invoca um script com seu próprio ambiente, mas não ao prefaciar um comando no ambiente atual. Interessante!
Todd A. Jacobs
1
O motivo pelo qual LD_PRELOADfunciona é que a variável é definida no ambiente do programa - não em sua linha de comando.
Pausado até novo aviso.

Respostas:

102

O que você vê é o comportamento esperado. O problema é que o shell pai avalia $SOMEVARna linha de comando antes de invocar o comando com o ambiente modificado. Você precisa obter a avaliação de $SOMEVARadiado até depois que o ambiente for configurado.

Suas opções imediatas incluem:

  1. SOMEVAR=BBB eval echo zzz '$SOMEVAR' zzz.
  2. SOMEVAR=BBB sh -c 'echo zzz $SOMEVAR zzz'.

Ambos usam aspas simples para evitar que o shell pai seja avaliado $SOMEVAR; ele só é avaliado depois de definido no ambiente (temporariamente, durante o único comando).

Outra opção é usar a notação de sub-shell (como também sugerido por Marcus Kuhn em sua resposta ):

(SOMEVAR=BBB; echo zzz $SOMEVAR zzz)

A variável é definida apenas no sub-shell

Jonathan Leffler
fonte
Incrível, @JonathanLeffler - muito obrigado pela explicação; Felicidades!
sdaau
A adição de @ markus-kuhn é difícil de superestimar.
Alex Che
37

O problema, revisitado

Francamente, o manual é confuso neste ponto. O manual do GNU Bash diz:

O ambiente para qualquer comando ou função simples [observe que isso exclui builtins] pode ser aumentado temporariamente adicionando um prefixo a ele com atribuições de parâmetro, conforme descrito em Parâmetros do Shell. Essas instruções de atribuição afetam apenas o ambiente visto por aquele comando.

Se você realmente analisar a frase, o que ela está dizendo é que o ambiente para o comando / função foi modificado, mas não o ambiente para o processo pai. Então, isso vai funcionar:

$ TESTVAR=bbb env | fgrep TESTVAR
TESTVAR=bbb

porque o ambiente para o comando env foi modificado antes de ser executado. No entanto, isso não funcionará:

$ set -x; TESTVAR=bbb echo aaa $TESTVAR ccc
+ TESTVAR=bbb
+ echo aaa ccc
aaa ccc

por causa de quando a expansão do parâmetro é executada pelo shell.

Passos do intérprete

Outra parte do problema é que o Bash define essas etapas para seu interpretador:

  1. Lê sua entrada de um arquivo (consulte Shell Scripts), de uma string fornecida como um argumento para a opção de invocação -c (consulte Chamando Bash) ou do terminal do usuário.
  2. Divide a entrada em palavras e operadores, obedecendo às regras de citação descritas em Citação. Esses tokens são separados por metacaracteres. A expansão de alias é executada por esta etapa (consulte Aliases).
  3. Analisa os tokens em comandos simples e compostos (consulte Comandos do Shell).
  4. Executa as várias expansões de shell (consulte Expansões de shell), dividindo os tokens expandidos em listas de nomes de arquivos (consulte Expansão de nome de arquivo) e comandos e argumentos.
  5. Executa todos os redirecionamentos necessários (consulte Redirecionamentos) e remove os operadores de redirecionamento e seus operandos da lista de argumentos.
  6. Executa o comando (consulte Execução de comandos).
  7. Opcionalmente, aguarda a conclusão do comando e coleta seu status de saída (consulte Status de saída).

O que está acontecendo aqui é que os built-ins não têm seu próprio ambiente de execução, portanto, eles nunca veem o ambiente modificado. Além disso, comandos simples (por exemplo, / bin / echo) não ter uma ennvironment modificada (que é por isso que o exemplo env trabalhou), mas a expansão shell está ocorrendo no atual ambiente na etapa # 4.

Em outras palavras, você não está passando 'aaa $ TESTVAR ccc' para / bin / echo; você está passando a string interpolada (conforme expandida no ambiente atual) para / bin / echo. Nesse caso, como o ambiente atual não tem TESTVAR , você simplesmente passa 'aaa ccc' para o comando.

Resumo

A documentação poderia ser muito mais clara. Ainda bem que existe Stack Overflow!

Veja também

http://www.gnu.org/software/bash/manual/bashref.html#Command-Execution-Environment

Todd A. Jacobs
fonte
Eu já tinha votado a favor - mas acabei de voltar a esta questão, e este post contém exatamente as dicas de que preciso; muito obrigado, @CodeGnome!
sdaau,
Eu não sei se Bash mudou nessa área desde esta resposta foi publicado, mas atribuições de variáveis prefixadas fazer o trabalho com builtins agora. Por exemplo, FOO=foo eval 'echo $FOO'imprime fooconforme o esperado. Isso significa que você pode fazer coisas como IFS="..." read ....
Will Vousden
Acho que o que está acontecendo é que o Bash na verdade modifica seu próprio ambiente temporariamente e o restaura assim que o comando é concluído, o que pode ter efeitos colaterais estranhos.
Will Vousden
22

Para conseguir o que deseja, use

( SOMEVAR=BBB; echo zzz $SOMEVAR zzz )

Razão:

  • Você deve separar a atribuição por ponto-e-vírgula ou nova linha do próximo comando, caso contrário, ele não será executado antes que a expansão do parâmetro aconteça para o próximo comando (eco).

  • Você precisa fazer a atribuição dentro de um ambiente de subshell , para garantir que não persista além da linha atual.

Esta solução é mais curta, mais organizada e mais eficiente do que algumas das outras sugeridas, em particular ela não cria um novo processo.

Markus Kuhn
fonte
3
Para futuros googlers que acabarão aqui: esta é provavelmente a melhor resposta para essa pergunta. Para complicar ainda mais, se você precisa que a atribuição esteja disponível no ambiente do comando, você precisa exportá-la. O subshell ainda impede que a atribuição persista. (export SOMEVAR=BBB; python -c "from os import getenv; print getenv('SOMEVAR')")
dia
@eaj Para exportar uma variável shell para uma única chamada de programa externo, como no seu exemplo, basta usarSOMEVAR=BBB python -c "from os import getenv; print getenv('SOMEVAR')"
Markus Kuhn
10

O motivo é que isso define uma variável de ambiente para uma linha. Mas, echonão faz a expansão, bashfaz. Portanto, sua variável é realmente expandida antes que o comando seja executado, mesmo que SOME_VAResteja BBBno contexto do comando echo.

Para ver o efeito, você pode fazer algo como:

$ SOME_VAR=BBB bash -c 'echo $SOME_VAR'
BBB

Aqui, a variável não é expandida até que o processo filho seja executado, portanto, você vê o valor atualizado. se você verificar SOME_VARIABLEnovamente no shell pai, ainda estará AAA, conforme o esperado.

Erro fatal
fonte
1
+1 para uma explicação correta de porque não funciona como está escrito e para uma solução alternativa viável.
Jonathan Leffler
1
SOMEVAR=BBB; echo zzz $SOMEVAR zzz

Use um ; para separar instruções que estão na mesma linha.

Kyros
fonte
1
Isso funciona, mas não é bem o ponto. A ideia é definir o ambiente para apenas um comando, não permanentemente como faz sua solução.
Jonathan Leffler
Obrigado por isso @Kyros; não sei como é que eu perdi isso agora :) Ainda vagando como LD_PRELOADe tal pode funcionar na frente de um executável sem ponto-e-vírgula, porém ... Muito obrigado novamente - saúde!
sdaau
@JonathanLeffler - de fato, essa era a ideia; Não percebi que o ponto e vírgula torna a alteração permanente - obrigado por notar isso!
sdaau
1

Aqui está uma alternativa:

SOMEVAR=BBB && echo zzz $SOMEVAR zzz
Brian
fonte
Quer você use &&ou ;para separar os comandos, a atribuição persiste, o que não é o comportamento desejado do OP. Markus Kuhn tem a versão correta dessa resposta.
dia