
Como salvar a saída de um comando que modifica o ambiente em uma variável?
Estou usando o bash shell.
Suponha que eu tenha:
function f () { a=3; b=4 ; echo "`date`: $a $b"; }
E agora, posso usar comandos para executar f
:
$ a=0; b=0; f; echo $a; echo $b; echo $c
Sat Jun 28 21:27:08 CEST 2014: 3 4
3
4
mas eu gostaria de salvar a saída f
na variável c
, então tentei:
a=0; b=0; c=""; c=$(f); echo $a; echo $b; echo $c
mas infelizmente, eu tenho:
0
0
Sat Jun 28 21:28:03 CEST 2014: 3 4
então não tenho nenhuma mudança de ambiente aqui.
Como salvar a saída do comando (não apenas a função) em uma variável e salvar as alterações ambientais?
Eu sei que isso $(...)
abre um novo subshell e esse é o problema, mas é possível fazer alguma solução alternativa?
Responder1
Se estiver usando o Bash 4 ou posterior, você pode usarcoprocessos:
function f () { a=3; b=4 ; echo "`date`: $a $b"; }
coproc cat
f >&${COPROC[1]}
exec {COPROC[1]}>&-
read c <&${COPROC[0]}
echo a $a
echo b $b
echo c $c
produzirá
a 3
b 4
c Sun Jun 29 10:08:15 NZST 2014: 3 4
coproc
cria um novo processo executando um determinado comando (aqui, cat
). Ele salva o PID COPROC_PID
e os descritores de arquivo de saída/entrada padrão em uma matriz COPROC
(assim comopipe(2)
, ou vejaaquiouaqui).
Aqui executamos a função com a saída padrão apontada para nosso coprocesso em execução cat
e, em seguida,read
a partir dele. Como cat
apenas cospe sua entrada de volta, obtemos a saída da função em nossa variável. exec {COPROC[1]}>&-
apenas fecha o descritor de arquivo para que cat
não fique esperando para sempre.
Observe que isso read
ocupa apenas uma linha por vez. Você pode usar mapfile
para obter uma matriz de linhas ou apenas usar o descritor de arquivo da maneira que desejar.
exec {COPROC[1]}>&-
funciona nas versões atuais do Bash, mas as versões anteriores da série 4 exigem que você salve primeiro o descritor de arquivo em uma variável simples: fd=${COPROC[1]}; exec {fd}>&-
. Se sua variável não estiver definida, ela fechará a saída padrão.
Se você estiver usando uma versão de 3 séries do Bash, poderá obter o mesmo efeito commkfifo
, mas não é muito melhor do que usar um arquivo real nesse ponto.
Responder2
Se você já está contando com o corpo de f
execução no mesmo shell do chamador, e assim poder modificar variáveis como a
e b
, por que não fazer com que a função seja definida c
também? Em outras palavras:
$ function f () { a=3; b=4 ; c=$(echo "$(date): $a $b"); }
$ a=0; b=0; f; echo $a; echo $b; echo $c
3
4
Mon Jun 30 14:32:00 EDT 2014: 3 4
Uma possível razão pode ser que a variável de saída precise ter nomes diferentes (c1, c2, etc) quando chamada em locais diferentes, mas você pode resolver isso definindo a função c_TEMP e fazendo com que os chamadores façam c1=$c_TEMP
etc.
Responder3
É um kluge, mas tente
f > c.file
c=$(cat c.file)
(e, opcionalmente, rm c.file
).
Responder4
Basta atribuir todas as variáveis e escrever a saída ao mesmo tempo.
f() { c= ; echo "${c:=$(date): $((a=3)) $((b=4))}" ; }
Agora, se você fizer isso:
f ; echo "$a $b $c"
Sua saída é:
Tue Jul 1 04:58:17 PDT 2014: 3 4
3 4 Tue Jul 1 04:58:17 PDT 2014: 3 4
Observe que este é um código totalmente portátil POSIX. Eu inicialmente configurei c=
para a ''
string nula porque a expansão do parâmetro limita a atribuição simultânea de variáveis + avaliação apenas para números(como $((var=num))
)ou de valores nulos ou inexistentes - ou, em outras palavras, você não pode definir simultaneamenteeavaliar uma variável para uma string arbitrária se essa variável já tiver um valor atribuído. Então, apenas garanto que esteja vazio antes de tentar. Se eu não esvaziasse c
antes de tentar atribuí-lo, a expansão retornaria apenas o valor antigo.
Apenas para demonstrar:
sh -c '
c=oldval a=1
echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2
newval
énãoatribuído a $c
inline porque oldval
é expandido para ${word}
, enquanto a atribuição $((
aritmética inline sempre ocorre. Mas se=
))
$c
não tem oldval
e está vazio ou não definido...
sh -c '
c=oldval a=1
echo ${c:=newval} $((a=a+a))
c= a=$((a+a))
echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2
newval 8
... então newval
é imediatamente atribuído e expandido para $c
.
Todas as outras formas de fazer isso envolvem alguma forma de avaliação secundária. Por exemplo, digamos que eu queira atribuir a saída de f()
a uma variável nomeada name
em um ponto e var
em outro. Conforme escrito atualmente, isso não funcionará sem definir var no escopo do chamador. Uma maneira diferente poderia ser assim:
f(){ fout_name= fout= set -- "${1##[0-9]*}" "${1%%*[![:alnum:]]*}"
(: ${2:?invalid or unspecified param - name set to fout}) || set --
export "${fout_name:=${1:-fout}}=${fout:=$(date): $((a=${a:-50}+1)) $((b=${b:-100}-4))}"
printf %s\\n "$fout"
}
f &&
printf %s\\n \
"$fout_name" \
"$fout" \
"$a" "$b"
Forneci um exemplo melhor formatado abaixo, mas, chamado como acima, a saída é:
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul 2 02:27:07 PDT 2014: 51 96
fout
Wed Jul 2 02:27:07 PDT 2014: 51 96
51
96
Ou com $ENV
argumentos ou diferentes:
b=9 f myvar &&
printf %s\\n \
"$fout_name" \
"$fout" \
"$myvar" \
"$a" "$b"
###OUTPUT###
Tue Jul 1 19:56:42 PDT 2014: 52 5
myvar
Tue Jul 1 19:56:42 PDT 2014: 52 5
Tue Jul 1 19:56:42 PDT 2014: 52 5
52
5
Provavelmente, a coisa mais complicada de acertar quando se trata de avaliar duas vezes é garantir que as variáveis não quebrem aspas e executem código aleatório. Quanto mais vezes uma variável é avaliada, mais difícil ela fica. A expansão de parâmetros ajuda muito aqui, e usar export
em vez de eval
é muito mais seguro.
No exemplo acima, f()
primeiro atribui $fout
a ''
string nula e, em seguida, define parâmetros posicionais para testar nomes de variáveis válidos. Se ambos os testes não forem aprovados, uma mensagem será emitida stderr
e o valor padrão fout
será atribuído a $fout_name
. Independentemente dos testes, no entanto, o nome que você especifica $fout_name
é sempre atribuído ao nome especificado e , opcionalmente, o nome especificado é sempre atribuído ao valor de saída da função. Para demonstrar isso, escrevi este pequeno loop:fout
$fout
for
for v in var '' "wr;\' ong"
do sleep 10 &&
a=${a:+$((a*2))} f "$v" || break
echo "${v:-'' #null}"
printf '#\t"$%s" = '"'%s'\n" \
a "$a" b "$b" \
fout_name "$fout_name" \
fout "$fout" \
'(eval '\''echo "$'\''"$fout_name"\")' \
"$(eval 'echo "$'"$fout_name"\")"
done
Ele brinca com nomes de variáveis e expansões de parâmetros. Se tiver alguma questão, basta perguntar. Queapenasexecuta as mesmas poucas linhas na função já representada aqui. Vale a pena mencionar pelo menos que as variáveis $a
e $b
se comportam de maneira diferente dependendo se elas estão definidas na invocação ou já definidas. Ainda assim, ele for
não faz quase nada além de formatar o conjunto de dados fornecido pelo f()
. Dar uma olhada:
###OUTPUT###
Wed Jul 2 02:50:17 PDT 2014: 51 96
var
# "$a" = '51'
# "$b" = '96'
# "$fout_name" = 'var'
# "$fout" = 'Wed Jul 2 02:50:17 PDT 2014: 51 96'
# "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul 2 02:50:17 PDT 2014: 51 96'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul 2 02:50:27 PDT 2014: 103 92
'' #null
# "$a" = '103'
# "$b" = '92'
# "$fout_name" = 'fout'
# "$fout" = 'Wed Jul 2 02:50:27 PDT 2014: 103 92'
# "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul 2 02:50:27 PDT 2014: 103 92'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul 2 02:50:37 PDT 2014: 207 88
wr;\' ong
# "$a" = '207'
# "$b" = '88'
# "$fout_name" = 'fout'
# "$fout" = 'Wed Jul 2 02:50:37 PDT 2014: 207 88'
# "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul 2 02:50:37 PDT 2014: 207 88'