Estou trabalhando com isso:
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Eu tenho um script como abaixo:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
test1
echo "$e"
Que retorna:
hello
4
Mas se eu atribuir o resultado da função a uma variável, a variável global e
não é modificada:
#!/bin/bash
e=2
function test1() {
e=4
echo "hello"
}
ret=$(test1)
echo "$ret"
echo "$e"
Retorna:
hello
2
Já ouvi falar do uso de eval neste caso, então fiz isso em test1
:
eval 'e=4'
Mas o mesmo resultado.
Você poderia me explicar porque não foi modificado? Como posso salvar o eco da test1
função ret
e modificar a variável global também?
bash
variables
global-variables
eval
harrison4
fonte
fonte
Respostas:
Quando você usa uma substituição de comando (ou seja, a
$(...)
construção), está criando um subshell. Os subshells herdam variáveis de seus shells pai, mas isso só funciona de uma maneira - um subshell não pode modificar o ambiente de seu shell pai. Sua variávele
é definida dentro de um subshell, mas não no shell pai. Existem duas maneiras de passar valores de um subshell para seu pai. Primeiro, você pode enviar algo para stdout e, em seguida, capturá-lo com uma substituição de comando:Dá:
Para um valor numérico de 0-255, você pode usar
return
para passar o número como o status de saída:Dá:
fonte
setarray() { declare -ag "$1=(a b c)"; }
Resumo
Seu exemplo pode ser modificado da seguinte maneira para arquivar o efeito desejado:
imprime conforme desejado:
Observe que esta solução:
e=1000
também.$?
se você precisar$?
Os únicos efeitos colaterais ruins são:
bash
._
)_capture
apenas substituir todas as ocorrências de3
com outro número (superior).O que se segue (que é bastante longo, desculpe por isso), espero que explique, como adicionar esta receita a outros scripts também.
O problema
saídas
enquanto a saída desejada é
A causa do problema
Variáveis shell (ou, de modo geral, o ambiente) são passadas de processos pais para processos filhos, mas não vice-versa.
Se você fizer a captura de saída, isso geralmente será executado em um subshell, portanto, é difícil passar de volta as variáveis.
Alguns até dizem que é impossível consertar. Isso está errado, mas é um problema difícil de resolver há muito tempo.
Existem várias maneiras de resolver isso da melhor maneira, dependendo de suas necessidades.
Aqui está um guia passo a passo de como fazer isso.
Passando de volta variáveis para o shell parental
Existe uma maneira de passar as variáveis de volta para um shell parental. Porém este é um caminho perigoso, pois este usa
eval
. Se feito de maneira inadequada, você corre o risco de muitas coisas más. Mas, se feito corretamente, é perfeitamente seguro, desde que não haja nenhum bug nobash
.estampas
Observe que isso também funciona para coisas perigosas:
estampas
Isso se deve a
printf '%q'
, que cita tudo isso, que você pode reutilizá-lo em um contexto de shell com segurança.Mas isso é uma dor na ..
Isso não só parece feio, mas também é muito para digitar, por isso está sujeito a erros. Apenas um único erro e você está condenado, certo?
Bem, estamos no nível do shell, então você pode melhorá-lo. Basta pensar em uma interface que você deseja ver e, em seguida, implementá-la.
Aumentar, como o shell processa as coisas
Vamos dar um passo atrás e pensar em alguma API que nos permite expressar facilmente o que queremos fazer.
Bem, o que queremos fazer com a
d()
função?Queremos capturar a saída em uma variável. OK, então vamos implementar uma API exatamente para isso:
Agora, em vez de escrever
nós podemos escrever
Bem, parece que não mudamos muito, pois, novamente, as variáveis não são passadas de volta
d
para o shell pai e precisamos digitar um pouco mais.No entanto, agora podemos jogar todo o poder do shell nele, já que ele está bem envolvido em uma função.
Pense em uma interface fácil de reutilizar
Uma segunda coisa é que queremos ser DRY (Don't Repeat Yourself). Portanto, definitivamente não queremos digitar algo como
O
x
aqui não é apenas redundante, é propenso a erros, sempre repetindo no contexto correto. E se você usá-lo 1000 vezes em um script e depois adicionar uma variável? Definitivamente, você não deseja alterar todos os 1000 locais em que uma chamada parad
está envolvida.Portanto, deixe de lado
x
, para que possamos escrever:saídas
Isso já parece muito bom. (Mas ainda há o
local -n
que não funciona no oder comumbash
3.x)Evite mudar
d()
A última solução tem algumas grandes falhas:
d()
precisa ser alteradoxcapture
para passar a saída.output
, portanto, nunca podemos passar esta de volta._passback
Podemos nos livrar disso também?
Claro que nós podemos! Estamos em um shell, então há tudo de que precisamos para fazer isso.
Se você olhar um pouco mais de perto para a chamada
eval
, verá que temos 100% de controle neste local. "Por dentro" doeval
estamos em uma subcamada, então podemos fazer tudo o que quisermos sem medo de fazer algo ruim à concha parental.Sim, bom, então vamos adicionar outro invólucro, agora diretamente dentro de
eval
:estampas
No entanto, isso, novamente, tem algumas desvantagens importantes:
!DO NOT USE!
marcadores estão lá, porque há uma condição de corrida muito ruim nisso, que você não pode ver facilmente:>(printf ..)
é um trabalho em segundo plano. Portanto, ele ainda pode ser executado enquanto o_passback x
está em execução.sleep 1;
antesprintf
ou_passback
._xcapture a d; echo
em seguida, produzx
oua
primeiro, respectivamente._passback x
não deve fazer parte_xcapture
, pois isso dificulta a reutilização daquela receita.$(cat)
), mas como essa solução é!DO NOT USE!
, peguei o caminho mais curto.No entanto, isso mostra que podemos fazer isso, sem modificação para
d()
(e semlocal -n
)!Observe que não precisamos de forma
_xcapture
alguma, pois poderíamos ter escrito tudo certo noeval
.No entanto, fazer isso geralmente não é muito legível. E se você voltar ao seu roteiro em alguns anos, provavelmente vai querer lê-lo novamente sem muitos problemas.
Corrija a corrida
Agora vamos corrigir a condição de corrida.
O truque pode ser esperar até que
printf
seja fechado STDOUT e, em seguida, imprimirx
.Existem muitas maneiras de arquivar isso:
Seguir o último caminho pode parecer (observe que é o
printf
último porque funciona melhor aqui):saídas
Por que isso está correto?
_passback x
fala diretamente com STDOUT.>&3
.$("${@:2}" 3<&-; _passback x >&3)
termina após o_passback
, quando o subshell fecha STDOUT.printf
não pode acontecer antes do_passback
, independentemente do quanto_passback
demore.printf
comando não é executado antes que a linha de comando completa seja montada, portanto, não podemos ver os artefatosprintf
, independentemente de comoprintf
é implementado.Portanto, primeiro
_passback
executa e, em seguida, oprintf
.Isso resolve a corrida, sacrificando um descritor de arquivo fixo 3. Você pode, é claro, escolher outro descritor de arquivo no caso, que FD3 não está livre em seu shellscript.
Observe também o
3<&-
que protege o FD3 para ser passado para a função.Torne-o mais genérico
_capture
contém partes, as quais pertencemd()
, o que é ruim, de uma perspectiva de reutilização. Como resolver isso?Bem, faça isso da maneira desesperada, introduzindo mais uma coisa, uma função adicional, que deve retornar as coisas certas, que tem o nome da função original
_
anexada.Esta função é chamada após a função real e pode aumentar as coisas. Desta forma, isso pode ser lido como uma anotação, por isso é muito legível:
ainda imprime
Permitir acesso ao código de retorno
Só falta um pouco:
v=$(fn)
define o$?
quefn
retornou. Então você provavelmente quer isso também. Porém, ele precisa de alguns ajustes maiores:estampas
Ainda há muito espaço para melhorias
_passback()
pode ser eliminado compassback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
pode ser eliminado comcapture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
A solução polui um descritor de arquivo (aqui 3), usando-o internamente. Você precisa manter isso em mente se acontecer de você ser aprovado em FDs.
Observe que a
bash
versão 4.1 e superior deve{fd}
usar algum FD não utilizado.(Talvez eu adicione uma solução aqui quando eu voltar.)
Observe que é por isso que costumo colocá-lo em funções separadas como
_capture
, porque juntar tudo isso em uma linha é possível, mas torna cada vez mais difícil de ler e entenderTalvez você queira capturar STDERR da função chamada também. Ou você deseja até mesmo passar mais de um arquivo descritor de e para variáveis.
Ainda não tenho solução, no entanto, aqui está uma maneira de capturar mais de um FD , portanto, provavelmente podemos passar de volta as variáveis dessa maneira também.
Também não se esqueça:
Isso deve chamar uma função shell, não um comando externo.
Últimas palavras
Esta não é a única solução possível. É um exemplo de solução.
Como sempre, você tem muitas maneiras de expressar as coisas no shell. Portanto, sinta-se à vontade para melhorar e encontrar algo melhor.
A solução apresentada aqui está muito longe de ser perfeita:
bash
, então provavelmente é difícil portar para outros shells.No entanto, acho que é muito fácil de usar:
fonte
Talvez você possa usar um arquivo, escrever no arquivo dentro da função, ler o arquivo depois dele. Eu mudei
e
para uma matriz. Neste exemplo, os espaços em branco são usados como separadores ao reler a matriz.Resultado:
fonte
O que você está fazendo, você está executando test1
$(test1)
em um sub-shell (shell filho) e shells filho não podem modificar nada no pai .
Você pode encontrar no manual do bash
Verifique: Things resulta em um subshell aqui
fonte
Eu tive um problema semelhante, quando quis remover automaticamente os arquivos temporários que havia criado. A solução que encontrei não foi usar a substituição de comandos, mas sim passar o nome da variável, que deveria levar o resultado final, para a função. Por exemplo
Então, no seu caso, isso seria:
Funciona e não tem restrições quanto ao "valor de retorno".
fonte
É porque a substituição do comando é realizada em um subshell, portanto, embora o subshell herde as variáveis, as alterações nelas são perdidas quando o subshell termina.
Referência :
fonte
Uma solução para este problema, sem ter que introduzir funções complexas e modificar pesadamente a original, é armazenar o valor em um arquivo temporário e ler / escrever quando necessário.
Essa abordagem me ajudou muito quando tive que simular uma função bash chamada várias vezes em um caso de teste do bats.
Por exemplo, você poderia ter:
A desvantagem é que você pode precisar de vários arquivos temporários para variáveis diferentes. E também pode ser necessário emitir um
sync
comando para persistir o conteúdo no disco entre uma operação de gravação e leitura.fonte
Você sempre pode usar um alias:
fonte