Como modificar uma variável global dentro de uma função em bash?

104

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 enã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 test1função rete modificar a variável global também?

harrison4
fonte
Você precisa retornar oi? Você pode simplesmente ecoar $ e para que ele retorne. Ou ecoar tudo o que você deseja e analisar o resultado?

Respostas:

98

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ável eé 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:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Dá:

Hello

Para um valor numérico de 0-255, você pode usar returnpara passar o número como o status de saída:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Dá:

Hello - num is 4
Josh Jolly
fonte
Obrigado pelo ponto, mas eu tenho que retornar uma matriz de string e, dentro da função, tenho que adicionar elementos a duas matrizes de string globais.
harrison4
3
Você percebe que se apenas executar a função sem atribuí-la a uma variável, todas as variáveis ​​globais dentro dela serão atualizadas. Em vez de retornar uma matriz de string, por que não apenas atualizar a matriz de string na função e atribuí-la a outra variável após o término da função?
@JohnDoe: Você não pode retornar um "array de strings" de uma função. Tudo o que você pode fazer é imprimir uma string. No entanto, você pode fazer algo assim:setarray() { declare -ag "$1=(a b c)"; }
rici
34

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=1000també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 _captureapenas substituir todas as ocorrências de 3com 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 dpara 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 xaqui 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 destá 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 -nque não funciona no oder comum bash3.x)

Evite mudar d()

A última solução tem algumas grandes falhas:

  • d() precisa ser alterado
  • Ele precisa usar alguns detalhes internos de xcapturepara 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 evalestamos 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 xestá em execução.
    • Você mesmo pode ver isso se adicionar um sleep 1;antes printfou _passback. _xcapture a d; echoem seguida, produz xou aprimeiro, respectivamente.
  • O _passback xnã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 _xcapturealguma, 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 printfseja 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 printfnão pode acontecer antes do _passback, independentemente do quanto _passbackdemore.
  • Observe que o printfcomando 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 _passbackexecuta 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

_captureconté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 fnretornou. 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 bashversã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.
Tino
fonte
2
você é incrível
Eliran Malka
14

Talvez você possa usar um arquivo, escrever no arquivo dentro da função, ler o arquivo depois dele. Eu mudei epara uma matriz. Neste exemplo, os espaços em branco são usados ​​como separadores ao reler a matriz.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Resultado:

hi
first second third
first
second
third
Ashkan
fonte
13

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

PradyJord
fonte
7

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

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

Então, no seu caso, isso seria:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Funciona e não tem restrições quanto ao "valor de retorno".

Elmar Zander
fonte
1

É 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 :

Substituição de comando , comandos agrupados com parênteses e comandos assíncronos são chamados em um ambiente de subshell que é uma duplicata do ambiente de shell

Algum cara programador
fonte
@JohnDoe Não tenho certeza se é possível. Você pode ter que repensar o design do script.
Algum programador de
Oh, mas eu preciso assing um array global dentro de uma função, senão, eu teria que repetir muito código (repita o código da função -30 linhas- 15 vezes -um por chamada-). Não tem outro jeito, né?
harrison4
1

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:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

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 synccomando para persistir o conteúdo no disco entre uma operação de gravação e leitura.

Fabio
fonte
-1

Você sempre pode usar um alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Dino Dini
fonte