Come modificare una variabile globale all'interno di una funzione in bash?


105

Sto lavorando con questo:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

Ho uno script come di seguito:

#!/bin/bash

e=2

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

test1 
echo "$e"

Che ritorna:

hello
4

Ma se assegno il risultato della funzione a una variabile, la variabile globale enon viene modificata:

#!/bin/bash

e=2

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

ret=$(test1)

echo "$ret"
echo "$e"

Ritorna:

hello
2

Ho sentito parlare dell'uso di eval in questo caso, quindi l'ho fatto in test1:

eval 'e=4'

Ma lo stesso risultato.

Mi spieghi perché non viene modificato? Come posso salvare l'eco della test1funzione rete modificare anche la variabile globale?


Hai bisogno di tornare ciao? Potresti semplicemente echo $ e perché ritorni. O fai eco a tutto quello che vuoi e poi analizza il risultato?

Risposte:


98

Quando si utilizza una sostituzione del comando (cioè il $(...)costrutto), si crea una subshell. Le subshell ereditano le variabili dalle loro shell genitrici, ma questo funziona solo in un modo: una subshell non può modificare l'ambiente della sua shell genitrice. La tua variabile eè impostata all'interno di una subshell, ma non nella shell genitrice. Esistono due modi per passare i valori da una subshell al suo genitore. Innanzitutto, puoi eseguire l'output di qualcosa su stdout, quindi catturarlo con una sostituzione del comando:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Dà:

Hello

Per un valore numerico compreso tra 0 e 255, è possibile utilizzare returnper passare il numero come stato di uscita:

mysecondfunc() {
    echo "Hello"
    return 4
}

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

echo "$var - num is $num_var"

Dà:

Hello - num is 4

Grazie per il punto, ma devo restituire un array di stringhe e all'interno della funzione devo aggiungere elementi a due array di stringhe globali.
harrison4

3
Ti rendi conto che se esegui la funzione senza assegnarla a una variabile, tutte le variabili globali al suo interno si aggiorneranno. Invece di restituire un array di stringhe, perché non aggiornare semplicemente l'array di stringhe nella funzione e quindi assegnarlo a un'altra variabile al termine della funzione?

@JohnDoe: non puoi restituire un "array di stringhe" da una funzione. Tutto quello che puoi fare è stampare una stringa. Tuttavia, puoi fare qualcosa del genere:setarray() { declare -ag "$1=(a b c)"; }
rici

34

Questo richiede bash 4.1 se usi {fd}o local -n.

Il resto dovrebbe funzionare in bash 3.x, spero. Non sono completamente sicuro a causa di printf %q- questa potrebbe essere una funzionalità di bash 4.

Sommario

Il tuo esempio può essere modificato come segue per archiviare l'effetto desiderato:

# 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"

stampa come desiderato:

hello
4

Nota che questa soluzione:

  • Funziona anche per e=1000.
  • Conserve $?se avete bisogno di$?

Gli unici effetti collaterali negativi sono:

  • Ha bisogno di un moderno bash.
  • Si biforca abbastanza più spesso.
  • Ha bisogno dell'annotazione (dal nome della tua funzione, con un'aggiunta _)
  • Sacrifica il descrittore di file 3.
    • Puoi cambiarlo con un altro FD se ne hai bisogno.
      • In _captureappena sostituire tutte le occorrenze di 3con un altro (più alto) numero.

Si spera che quanto segue (che è abbastanza lungo, mi dispiace) spieghi come adattare questa ricetta anche ad altri script.

Il 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

uscite

0 20171129-123521 20171129-123521 20171129-123521 20171129-123521

mentre l'output desiderato è

4 20171129-123521 20171129-123521 20171129-123521 20171129-123521

La causa del problema

Le variabili di shell (o in generale, l'ambiente) vengono trasmesse dai processi parentali ai processi figli, ma non viceversa.

Se acquisisci l'output, di solito viene eseguito in una subshell, quindi restituire le variabili è difficile.

Alcuni addirittura ti dicono che è impossibile rimediare. Questo è sbagliato, ma è un problema noto da tempo difficile da risolvere.

Esistono diversi modi per risolverlo al meglio, questo dipende dalle tue esigenze.

Ecco una guida passo passo su come farlo.

Ritornare le variabili nella shell dei genitori

C'è un modo per restituire le variabili a una shell parentale. Tuttavia questo è un percorso pericoloso, perché utilizza eval. Se fatto in modo improprio, rischi molte cose malvagie. Ma se fatto correttamente, questo è perfettamente sicuro, a condizione che non ci siano bug 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

stampe

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945

Nota che questo funziona anche per cose pericolose:

danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"

stampe

; /bin/echo *

Ciò è dovuto printf '%q', che cita tutto ciò, che puoi riutilizzarlo in modo sicuro in un contesto di shell.

Ma questo è un dolore in a ..

Questo non solo sembra brutto, è anche molto da scrivere, quindi è soggetto a errori. Solo un singolo errore e sei condannato, giusto?

Bene, siamo a livello di shell, quindi puoi migliorarlo. Pensa solo a un'interfaccia che vuoi vedere e poi puoi implementarla.

Aumenta, come la shell elabora le cose

Facciamo un passo indietro e pensiamo ad alcune API che ci permettono di esprimere facilmente ciò che vogliamo fare.

Ebbene, cosa vogliamo fare con la d()funzione?

Vogliamo catturare l'output in una variabile. OK, quindi implementiamo un'API esattamente per questo:

# 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="$("$@")"
}

Ora, invece di scrivere

d1=$(d)

possiamo scrivere

capture d1 d

Bene, sembra che non siamo cambiati molto, dato che, ancora una volta, le variabili non vengono restituite dalla dshell genitrice e dobbiamo digitare un po 'di più.

Tuttavia ora possiamo utilizzare tutta la potenza della shell, poiché è ben racchiusa in una funzione.

Pensa a un'interfaccia facile da riutilizzare

Una seconda cosa è che vogliamo essere DRY (Don't Repeat Yourself). Quindi non vogliamo assolutamente digitare qualcosa di simile

x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4

Il xqui non è solo ridondante, è soggetto a errori da ripetere sempre nel contesto corretto. E se lo usi 1000 volte in uno script e poi aggiungi una variabile? Non vuoi assolutamente modificare tutte le 1000 località in cui dè coinvolta una chiamata .

Quindi lascia xperdere, così possiamo scrivere:

_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

uscite

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414

Sembra già molto buono. (Ma c'è ancora il local -nche non funziona in oder comune bash3.x)

Evita di cambiare d()

L'ultima soluzione ha alcuni grossi difetti:

  • d() deve essere modificato
  • È necessario utilizzare alcuni dettagli interni xcaptureper passare l'output.
    • Nota che questa ombreggia (brucia) una variabile denominata output, quindi non possiamo mai restituirla.
  • Ha bisogno di collaborare con _passback

Possiamo sbarazzarci anche di questo?

Certo che possiamo! Siamo in un guscio, quindi c'è tutto ciò di cui abbiamo bisogno per farlo.

Se guardi un po 'più da vicino alla chiamata evalpuoi vedere che abbiamo il controllo al 100% in questa posizione. "Dentro" evalsiamo in una subshell, quindi possiamo fare tutto ciò che vogliamo senza paura di fare qualcosa di brutto al guscio dei genitori.

Sì, bello, quindi aggiungiamo un altro wrapper, ora direttamente all'interno di 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

stampe

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    

Tuttavia, questo, ancora una volta, presenta alcuni importanti inconvenienti:

  • I !DO NOT USE!marker ci sono, perché c'è una condizione di gara molto brutta in questo, che non puoi vedere facilmente:
    • Il >(printf ..)è un processo in background. Quindi potrebbe ancora essere eseguito mentre _passback xè in esecuzione.
    • Puoi vederlo tu stesso se aggiungi un file sleep 1;prima printfo _passback. _xcapture a d; echoquindi le uscite xo afirst, rispettivamente.
  • Il _passback xnon dovrebbe farne parte _xcapture, perché questo rende difficile riutilizzare quella ricetta.
  • Inoltre abbiamo qualche bivio non segnato qui (il $(cat)), ma poiché questa soluzione è !DO NOT USE!ho preso la via più breve.

Tuttavia, questo dimostra che possiamo farlo, senza modifiche a d()(e senza local -n)!

Tieni presente che non ne abbiamo assolutamente bisogno _xcapture, poiché avremmo potuto scrivere tutto proprio nel file eval.

Tuttavia, questa operazione di solito non è molto leggibile. E se torni al copione tra qualche anno, probabilmente vorrai poterlo leggere di nuovo senza troppi problemi.

Risolvi la gara

Ora sistemiamo la condizione della gara.

Il trucco potrebbe essere quello di aspettare fino a quando non printfè stato chiuso è STDOUT, e quindi l'output x.

Esistono molti modi per archiviarlo:

  • Non è possibile utilizzare le pipe di shell, perché le pipe vengono eseguite in processi diversi.
  • Si possono usare file temporanei,
  • o qualcosa come un file di blocco o un fifo. Questo permette di aspettare la serratura o fifo,
  • o canali diversi, per emettere le informazioni e quindi assemblare l'uscita in una sequenza corretta.

Seguire l'ultimo percorso potrebbe apparire come (nota che fa l' printfultimo perché funziona meglio qui):

_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

uscite

4 20171129-144845 20171129-144845 20171129-144845 20171129-144845

Perché è corretto?

  • _passback x parla direttamente con STDOUT.
  • Tuttavia, poiché STDOUT deve essere catturato nel comando interno, prima lo "salviamo" in FD3 (puoi usarne altri, ovviamente) con '3> & 1' e poi lo riutilizziamo con >&3.
  • Il $("${@:2}" 3<&-; _passback x >&3)finisce dopo _passback, quando la subshell chiude STDOUT.
  • Quindi printfnon può accadere prima del _passback, indipendentemente dal tempo _passbackimpiegato.
  • Nota che il printfcomando non viene eseguito prima che l'intera riga di comando sia assemblata, quindi non possiamo vedere artefatti da printf, indipendentemente da come printfè implementato.

Quindi prima _passbackesegue, poi il file printf.

Questo risolve la gara, sacrificando un descrittore di file fisso 3. Puoi, ovviamente, scegliere un altro descrittore di file nel caso in cui FD3 non sia libero nel tuo shellscript.

Si prega di notare anche il 3<&-che protegge l'FD3 da passare alla funzione.

Rendilo più generico

_capture contiene parti che appartengono a d() , il che è negativo dal punto di vista della riutilizzabilità. Come risolverlo?

Bene, fallo in modo disperato introducendo un'altra cosa, una funzione aggiuntiva, che deve restituire le cose giuste, che prende il nome dalla funzione originale con _allegato.

Questa funzione è chiamata dopo la funzione reale e può aumentare le cose. In questo modo, questo può essere letto come un'annotazione, quindi è molto leggibile:

_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

stampa ancora

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Consenti l'accesso al codice di ritorno

Manca solo un bit:

v=$(fn)imposta $?ciò che è fntornato. Quindi probabilmente lo vuoi anche tu. Tuttavia, ha bisogno di qualche ritocco più grande:

# 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

stampe

23 42 69 FAIL

C'è ancora molto margine di miglioramento

  • _passback() può essere eliminato con passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
  • _capture() può essere eliminato con capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }

  • La soluzione inquina un descrittore di file (qui 3) utilizzandolo internamente. Devi tenerlo a mente se ti capita di superare FD.
    Nota che bash4.1 e versioni successive devono {fd}utilizzare alcuni FD inutilizzati.
    (Forse aggiungerò una soluzione qui quando torno in giro.)
    Nota che questo è il motivo per cui lo metto in funzioni separate come _capture, perché è possibile inserire tutto in una riga, ma rende sempre più difficile leggere e capire

  • Forse vuoi catturare anche STDERR della funzione chiamata. Oppure vuoi anche passare dentro e fuori più di un descrittore di file da e per le variabili.
    Non ho ancora una soluzione, tuttavia qui è un modo per catturare più di un FD , quindi probabilmente possiamo anche restituire le variabili in questo modo.

Inoltre, non dimenticare:

Questo deve chiamare una funzione di shell, non un comando esterno.

Non esiste un modo semplice per passare le variabili di ambiente dai comandi esterni. (Con LD_PRELOAD=dovrebbe essere possibile, però!) Ma questo è qualcosa di completamente diverso.

Ultime parole

Questa non è l'unica soluzione possibile. È un esempio di una soluzione.

Come sempre hai molti modi per esprimere le cose nella shell. Quindi sentiti libero di migliorare e trovare qualcosa di meglio.

La soluzione qui presentata è piuttosto lontana dall'essere perfetta:

  • Non era quasi affatto testato, quindi perdona gli errori di battitura.
  • C'è molto margine di miglioramento, vedi sopra.
  • Utilizza molte funzionalità dal moderno bash, quindi probabilmente è difficile da portare su altre shell.
  • E potrebbero esserci alcune stranezze a cui non ho pensato.

Tuttavia penso che sia abbastanza facile da usare:

  • Aggiungi solo 4 righe di "libreria".
  • Aggiungi solo 1 riga di "annotazione" per la tua funzione di shell.
  • Sacrifica temporaneamente un solo descrittore di file.
  • E ogni passaggio dovrebbe essere facile da capire anche a distanza di anni.

2
sei fantastico
Eliran Malka

14

Forse puoi usare un file, scrivere su file all'interno della funzione, leggere dal file dopo di esso. Sono passato ea un array. In questo esempio gli spazi vengono utilizzati come separatori durante la lettura dell'array.

#!/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]}"

Produzione:

hi
first second third
first
second
third

13

Quello che stai facendo, stai eseguendo test1

$(test1)

in una sub-shell (shell figlio) e le shell figlio non possono modificare nulla in parent .

Puoi trovarlo nel manuale di bash

Si prega di controllare: le cose risultano in una sottoshell qui


7

Ho avuto un problema simile, quando volevo rimuovere automaticamente i file temporanei che avevo creato. La soluzione che ho trovato non è stata quella di utilizzare la sostituzione del comando, ma piuttosto di passare il nome della variabile, che dovrebbe prendere il risultato finale, nella funzione. Per esempio

#! /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

Quindi, nel tuo caso sarebbe:

#!/bin/bash

e=2

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

test1 ret

echo "$ret"
echo "$e"

Funziona e non ha restrizioni sul "valore di ritorno".


1

È perché la sostituzione del comando viene eseguita in una subshell, quindi mentre la subshell eredita le variabili, le modifiche ad esse vengono perse quando termina la subshell.

Riferimento :

Sostituzione di comandi, comandi raggruppati con parentesi e comandi asincroni vengono richiamati in un ambiente subshell che è un duplicato dell'ambiente shell


@JohnDoe Non sono sicuro che sia possibile. Potrebbe essere necessario ripensare al design della sceneggiatura.
Un tizio programmatore

Oh, ma ho bisogno di assegnare un array globale all'interno di una funzione, in caso contrario, dovrei ripetere molto codice (ripetere il codice della funzione -30 righe- 15 volte -una per chiamata-). Non c'è altro modo, non è vero?
harrison4

1

Una soluzione a questo problema, senza dover introdurre funzioni complesse e modificare pesantemente quella originale, consiste nel memorizzare il valore in un file temporaneo e leggerlo / scriverlo quando necessario.

Questo approccio mi ha aiutato molto quando ho dovuto deridere una funzione bash chiamata più volte in un test case di pipistrelli.

Ad esempio, potresti avere:

# 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"

Lo svantaggio è che potresti aver bisogno di più file temporanei per variabili diverse. Inoltre, potresti dover emettere un synccomando per rendere persistente il contenuto sul disco tra un'operazione di scrittura e una di lettura.


-1

Puoi sempre utilizzare un alias:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.