Isso precisa do bash 4.1 se você usar {fd}
ou local -n
.
O resto deve funcionar no bash 3.x, espero. Não estou completamente certo devido a printf %q
- este pode ser um recurso do bash 4.
Resumo
Seu exemplo pode ser modificado da seguinte maneira para arquivar o efeito desejado:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
imprime conforme desejado:
hello
4
Observe que esta solução:
- Funciona para
e=1000
também.
- Preserva
$?
se você precisar$?
Os únicos efeitos colaterais ruins são:
- Precisa de um moderno
bash
.
- Ele se bifurca com mais frequência.
- Ele precisa da anotação (com o nome de sua função, com um adicionado
_
)
- Ele sacrifica o descritor de arquivo 3.
- Você pode alterá-lo para outro FD se precisar.
- Em
_capture
apenas substituir todas as ocorrências de 3
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
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
saídas
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
enquanto a saída desejada é
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
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 no bash
.
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
estampas
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Observe que isso também funciona para coisas perigosas:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
estampas
; /bin/echo *
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:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Agora, em vez de escrever
d1=$(d)
nós podemos escrever
capture d1 d
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
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
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 para d
está envolvida.
Portanto, deixe de lado x
, para que possamos escrever:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
saídas
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Isso já parece muito bom. (Mas ainda há o local -n
que não funciona no oder comum bash
3.x)
Evite mudar d()
A última solução tem algumas grandes falhas:
d()
precisa ser alterado
- Ele precisa usar alguns detalhes internos de
xcapture
para passar a saída.
- Observe que isso sombreia (queima) uma variável chamada
output
, portanto, nunca podemos passar esta de volta.
- Precisa cooperar com
_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" do eval
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
:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
estampas
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
No entanto, isso, novamente, tem algumas desvantagens importantes:
- Os
!DO NOT USE!
marcadores estão lá, porque há uma condição de corrida muito ruim nisso, que você não pode ver facilmente:
- Este
>(printf ..)
é um trabalho em segundo plano. Portanto, ele ainda pode ser executado enquanto o _passback x
está em execução.
- Você mesmo pode ver isso se adicionar um
sleep 1;
antes printf
ou _passback
.
_xcapture a d; echo
em seguida, produz x
ou a
primeiro, respectivamente.
- O
_passback x
não deve fazer parte _xcapture
, pois isso dificulta a reutilização daquela receita.
- Também temos algumas bifurcações não marcadas aqui (a
$(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 sem local -n
)!
Observe que não precisamos de forma _xcapture
alguma, pois poderíamos ter escrito tudo certo no eval
.
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, imprimir x
.
Existem muitas maneiras de arquivar isso:
- Você não pode usar canais de shell, porque os tubos são executados em processos diferentes.
- Pode-se usar arquivos temporários,
- ou algo como um arquivo de bloqueio ou um fifo. Isso permite esperar pelo bloqueio ou fifo,
- ou canais diferentes, para emitir as informações e, em seguida, montar a saída em alguma sequência correta.
Seguir o último caminho pode parecer (observe que é o printf
último porque funciona melhor aqui):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
saídas
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Por que isso está correto?
_passback x
fala diretamente com STDOUT.
- No entanto, como STDOUT precisa ser capturado no comando interno, primeiro o "salvamos" no FD3 (você pode usar outros, é claro) com '3> & 1' e depois o reutilizamos com
>&3
.
- O
$("${@:2}" 3<&-; _passback x >&3)
termina após o _passback
, quando o subshell fecha STDOUT.
- Portanto, o
printf
não pode acontecer antes do _passback
, independentemente do quanto _passback
demore.
- Observe que o
printf
comando não é executado antes que a linha de comando completa seja montada, portanto, não podemos ver os artefatos printf
, independentemente de como printf
é implementado.
Portanto, primeiro _passback
executa e, em seguida, o printf
.
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 pertencem d()
, 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:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
ainda imprime
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Permitir acesso ao código de retorno
Só falta um pouco:
v=$(fn)
define o $?
que fn
retornou. Então você provavelmente quer isso também. Porém, ele precisa de alguns ajustes maiores:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
estampas
23 42 69 FAIL
Ainda há muito espaço para melhorias
_passback()
pode ser eliminado com passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
pode ser eliminado com capture() { 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 entender
Talvez 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.
Não há uma maneira fácil de passar variáveis de ambiente de comandos externos. (Com LD_PRELOAD=
ele deveria ser possível, no entanto!) Mas isso então é algo completamente diferente.
Ú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:
- Quase não era testet, então perdoe os erros de digitação.
- Há muito espaço para melhorias, veja acima.
- Ele usa muitos recursos modernos
bash
, então provavelmente é difícil portar para outros shells.
- E pode haver algumas peculiaridades sobre as quais não pensei.
No entanto, acho que é muito fácil de usar:
- Adicione apenas 4 linhas de "biblioteca".
- Adicione apenas 1 linha de "anotação" para sua função de shell.
- Sacrifica apenas um descritor de arquivo temporariamente.
- E cada etapa deve ser fácil de entender, mesmo anos depois.