Salve a saída do comando que modifica o ambiente em uma variável

8

Como salvar a saída de um comando que modifica o ambiente em uma variável?

Estou usando o shell bash.

Suponha que eu tenha:

function f () { a=3; b=4 ; echo "`date`: $a $b"; }

E agora, eu 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 da fvariá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) na variável e salvar as mudanças ambientais?

Eu sei que isso $(...)abre um novo subshell e esse é o problema, mas é possível fazer alguma solução alternativa?

faramir
fonte
Certo, então a questão é essa $ae $bsão variáveis ​​locais em sua ffunção. Você poderia export, mas isso parece incompleto.
HalosGhost
1
@HalosGhost: Não, acho que não. Veja o primeiro exemplo: …; f; echo $a; …resulta em 3eco, assim fcomo modificar uma variável de shell (e não apenas sua própria variável local).
Scott

Respostas:

2

Se você estiver usando o Bash 4 ou posterior, poderá usar coprocesses :

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

irá produzir

a 3
b 4
c Sun Jun 29 10:08:15 NZST 2014: 3 4

coproccria um novo processo executando um determinado comando (aqui, cat). Ele salva o PID COPROC_PIDe os descritores de arquivo de entrada / saída padrão em uma matriz COPROC(assim como pipe(2), ou veja aqui ou aqui ).

Aqui, executamos a função com saída padrão apontada para o nosso co-processo em execução cate depois a readpartir dele. Como catapenas 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 catnão fique esperando para sempre.


Observe que readleva apenas uma linha por vez. Você pode usar mapfilepara obter uma matriz de linhas ou apenas usar o descritor de arquivo da maneira que desejar.

exec {COPROC[1]}>&-trabalhos em versões atuais do Bash, mas versões anteriores da série 4 exigem que você salvar o descritor de arquivo em uma variável simples em primeiro lugar: 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 em série do Bash, poderá obter o mesmo efeito mkfifo, mas não é muito melhor do que usar um arquivo real nesse momento.

Michael Homer
fonte
Eu posso usar apenas o bash 3.1. Os tubos são difíceis de usar. O mktemp e é a única opção? Aqui não há sintaxe ou opção especial para executar comandos e obter resultados, mas com alterações no ambiente?
Famair
Você pode usar o mkfifo para criar um pipe nomeado como um substituto para o coproc, o que não envolve realmente gravar os dados no disco. Com isso, você redirecionaria a saída para o fifo e a entrada dele, em vez do coprocesso. Caso contrário, não.
Michael Homer
+1 para adaptar minha resposta para não usar um arquivo. Mas, em meus testes, o exec {COPROC[1]}>&-comando (pelo menos algumas vezes) termina o coprocesso imediatamente , portanto sua saída não está mais disponível para ser lida. coproc { cat; sleep 1; }parece funcionar melhor. (Você pode precisar aumentar o 1se você está lendo mais de uma linha.)
Scott
1
Existe um significado convencional e bem entendido de "arquivo real" em uso aqui que você desconsidera intencionalmente.
Michael Homer
1
"Arquivo real" significa um arquivo em disco , como entendido por todos aqui, exceto, supostamente, você. Ninguém jamais implicou ou permitiu a um leitor razoável inferir que qualquer um deles era o mesmo que argumentos ou variáveis ​​de ambiente. Não estou interessado em continuar esta discussão.
Michael Homer
2

Se você já está contando com o corpo de fexecutar no mesmo shell que o responsável pela chamada e, portanto, pode modificar variáveis ​​como ae b, por que não definir a função ctambé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 razão possível 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 com a função definida c_TEMP e com os chamadores, c1=$c_TEMPetc.

godlygeek
fonte
1

É um kluge, mas tente

f > c.file
c=$(cat c.file)

(e, opcionalmente rm c.file).

Scott
fonte
Obrigado. Essa é uma resposta simples e funciona, mas tem algumas desvantagens. Eu sei que posso usar mktemp, mas gostaria de não criar arquivos adicionais.
Famair
1

Apenas atribua todas as variáveis ​​e escreva a saída ao mesmo tempo.

f() { c= ; echo "${c:=$(date): $((a=3)) $((b=4))}" ; }

Agora, se você fizer:

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 portátil totalmente POSIX. Inicialmente, defino c=a ''cadeia nula porque a expansão do parâmetro limita a atribuição simultânea de variáveis ​​+ a avaliação a valores numéricos (como $((var=num))) ou nulos ou inexistentes - ou, em outras palavras, você não pode configurar e avaliar simultaneamente uma variável em uma cadeia arbitrária se essa variável já tiver um valor atribuído. Portanto, apenas garanto que ele esteja vazio antes de tentar. Se eu não esvaziasse cantes 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

newvalnão é atribuído ao $cinline porque oldvalé expandido para ${word}, enquanto a atribuição $((aritmética inline sempre ocorre. Mas se não tem e está vazio ou não está definido ...=))$c oldval

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 maneiras de fazer isso envolvem alguma forma de avaliação secundária. Por exemplo, digamos que eu desejasse atribuir a saída de f()a uma variável nomeada nameem um ponto e varem outro. Como está escrito atualmente, isso não funcionará sem definir o 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 diferentes $ENVou argumentos:

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 difícil de acertar quando se trata de avaliar duas vezes é garantir que as variáveis ​​não quebrem aspas e executem códigos aleatórios. Quanto mais vezes uma variável é avaliada, mais difícil fica. A expansão de parâmetros ajuda bastante aqui, e usar exportao contrário evalé muito mais seguro.

No exemplo acima, f()primeiro atribui $fouta ''cadeia nula e, em seguida, define parâmetros posicionais para testar nomes de variáveis ​​válidos. Se ambos os testes não passarem, uma mensagem será emitida stderre o valor padrão de foutserá atribuído $fout_name. Independentemente dos testes, no entanto, $fout_namesempre é atribuído a um foutou ao nome que você especificar $foute, opcionalmente, seu nome especificado sempre recebe o valor de saída da função. Para demonstrar isso, escrevi este pequeno forloop:

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 alguns com nomes de variáveis ​​e expansões de parâmetros. Se tiver alguma questão, basta perguntar. Isso executa apenas as mesmas poucas linhas na função já representada aqui. Vale ressaltar, pelo menos, que as variáveis $ae $bse comportam de maneira diferente, dependendo se elas estão definidas na chamada ou já estão definidas. Ainda assim, forele quase não formata o conjunto de dados e é fornecido por 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'
mikeserv
fonte